Bound PDF print timeout and block WebSocket SSRF in clean-pdf
This commit is contained in:
@@ -8,8 +8,23 @@ import { buildCleanHtml } from "@/lib/clean-pdf/template";
|
||||
export class EmptyContentError extends Error {}
|
||||
export class RenderError extends Error {}
|
||||
|
||||
/**
|
||||
* Оборачивает промис в app-level таймаут: если `p` не завершится за `ms`,
|
||||
* гонка завершится отказом с RenderError, и внешний `finally` (browser.close())
|
||||
* освободит слот browserless вместо того, чтобы держать его до зависшего вызова.
|
||||
*/
|
||||
function withTimeout<T>(p: Promise<T>, ms: number, message: string): Promise<T> {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const guard = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new RenderError(message)), ms);
|
||||
timer.unref?.();
|
||||
});
|
||||
return Promise.race([p, guard]).finally(() => clearTimeout(timer)) as Promise<T>;
|
||||
}
|
||||
|
||||
const GOTO_TIMEOUT_MS = 30_000;
|
||||
const SETTLE_TIMEOUT_MS = 10_000;
|
||||
const PDF_TIMEOUT_MS = 30_000;
|
||||
const UA =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
|
||||
|
||||
@@ -38,7 +53,7 @@ export async function generateCleanPdf(opts: {
|
||||
await context.route("**/*", (route) => {
|
||||
try {
|
||||
const u = new URL(route.request().url());
|
||||
const host = u.hostname.replace(/^\[|\]$/g, "");
|
||||
const host = u.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
||||
if (
|
||||
(u.protocol !== "http:" && u.protocol !== "https:") ||
|
||||
host === "localhost" || host.endsWith(".local") || host.endsWith(".internal") ||
|
||||
@@ -52,6 +67,26 @@ export async function generateCleanPdf(opts: {
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
// context.route НЕ перехватывает WebSocket — отдельная защита от SSRF через ws://
|
||||
await context.routeWebSocket("**/*", (ws) => {
|
||||
try {
|
||||
const u = new URL(ws.url());
|
||||
const host = u.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
||||
if (
|
||||
(u.protocol !== "ws:" && u.protocol !== "wss:") ||
|
||||
host === "localhost" || host.endsWith(".local") || host.endsWith(".internal") ||
|
||||
(isIP(host) !== 0 && isPrivateAddress(host))
|
||||
) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
ws.connectToServer();
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
await page.goto(target.href, { waitUntil: "domcontentloaded", timeout: GOTO_TIMEOUT_MS }).catch((e) => {
|
||||
throw new RenderError(`Страница не открылась: ${e.message}`);
|
||||
@@ -84,13 +119,18 @@ export async function generateCleanPdf(opts: {
|
||||
const pdfPage = await context.newPage();
|
||||
await pdfPage.setContent(cleanHtml, { waitUntil: "networkidle", timeout: SETTLE_TIMEOUT_MS }).catch(() => {});
|
||||
// page.pdf() в playwright-core не принимает опцию timeout (проверено по
|
||||
// node_modules/playwright-core/types/types.d.ts) — брифовский вариант с
|
||||
// timeout здесь не компилировался, поле убрано.
|
||||
const pdf = await pdfPage.pdf({
|
||||
format: opts.format,
|
||||
printBackground: true,
|
||||
margin: { top: "15mm", bottom: "18mm", left: "15mm", right: "15mm" },
|
||||
});
|
||||
// node_modules/playwright-core/types/types.d.ts) и не имеет implicit-бага —
|
||||
// оборачиваем в app-level withTimeout, чтобы зависание всё же дошло до
|
||||
// finally (browser.close()) и не держало CONCURRENT-слот browserless.
|
||||
const pdf = await withTimeout(
|
||||
pdfPage.pdf({
|
||||
format: opts.format,
|
||||
printBackground: true,
|
||||
margin: { top: "15mm", bottom: "18mm", left: "15mm", right: "15mm" },
|
||||
}),
|
||||
PDF_TIMEOUT_MS,
|
||||
"Печать PDF не завершилась вовремя",
|
||||
);
|
||||
|
||||
return { pdf, title };
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user