@anvia/browser 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/automation-worker.js +4 -20
- package/dist/automation-worker.js.map +1 -1
- package/dist/chunk-XE5WKN5F.js +195 -0
- package/dist/chunk-XE5WKN5F.js.map +1 -0
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -217,7 +217,10 @@ The legacy `not_ready` and `human_controlled` codes remain in the public union.
|
|
|
217
217
|
The tools use ARIA state and strict Playwright locators. They do not expose JavaScript evaluation, raw
|
|
218
218
|
CDP, coordinate input, shell access, hidden action retries, or automatic reconnection. The navigation
|
|
219
219
|
policy is installed across the default browser context, so top-level navigation from links, forms,
|
|
220
|
-
redirects, popups, and direct navigation is checked consistently.
|
|
220
|
+
redirects, popups, and direct navigation is checked consistently. Private and reserved IP literals
|
|
221
|
+
(loopback, link-local, RFC 1918, and the other IANA special-purpose ranges) are rejected in every
|
|
222
|
+
policy mode, including `origins` entries that list them. The guard matches the literal host, so a
|
|
223
|
+
public domain that resolves to a private address is not covered. The policy does not block third-party
|
|
221
224
|
subresources; Docker bridge networking remains outside that policy.
|
|
222
225
|
|
|
223
226
|
The image runs Chromium as a non-root user with Chromium sandboxing, the Playwright seccomp profile,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
enforceNavigationPolicy
|
|
3
|
+
} from "./chunk-XE5WKN5F.js";
|
|
4
|
+
|
|
1
5
|
// src/automation-worker.ts
|
|
2
6
|
import { randomUUID } from "crypto";
|
|
3
7
|
import { chromium } from "playwright-core";
|
|
@@ -222,26 +226,6 @@ async function setNavigationPolicy(policy) {
|
|
|
222
226
|
assertBrowser().contexts().map((context) => context.route("**/*", (route) => enforceNavigationPolicy(route, policy)))
|
|
223
227
|
);
|
|
224
228
|
}
|
|
225
|
-
async function enforceNavigationPolicy(route, policy) {
|
|
226
|
-
const request = route.request();
|
|
227
|
-
const frame = request.frame();
|
|
228
|
-
if (request.isNavigationRequest() && frame === frame.page().mainFrame() && !isNavigationAllowed(request.url(), policy)) {
|
|
229
|
-
await route.abort("blockedbyclient");
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
await route.continue();
|
|
233
|
-
}
|
|
234
|
-
function isNavigationAllowed(value, policy) {
|
|
235
|
-
let url;
|
|
236
|
-
try {
|
|
237
|
-
url = new URL(value);
|
|
238
|
-
} catch {
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
242
|
-
if (url.username.length > 0 || url.password.length > 0) return false;
|
|
243
|
-
return policy.mode === "allow-all-http" || policy.origins.includes(url.origin);
|
|
244
|
-
}
|
|
245
229
|
function locatorFor(page, target) {
|
|
246
230
|
switch (target.by) {
|
|
247
231
|
case "role": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/automation-worker.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Browser, Locator, Page, Route } from \"playwright-core\";\nimport { chromium } from \"playwright-core\";\nimport type {\n AutomationCancel,\n AutomationCommand,\n AutomationRequest,\n AutomationResponse,\n BrowserTarget,\n SerializedError,\n} from \"./automation-protocol\";\nimport type { BrowserNavigationPolicy, BrowserTab } from \"./types\";\n\ntype ActiveOperation = {\n cancelled: boolean;\n page?: Page;\n completed: Promise<void>;\n complete: () => void;\n};\n\nlet browser: Browser | undefined;\nlet selected: Page | undefined;\nconst tabIds = new WeakMap<Page, string>();\nconst pagesById = new Map<string, Page>();\nconst operations = new Map<number, ActiveOperation>();\nlet navigationPolicyKey: string | undefined;\n\nprocess.once(\"disconnect\", () => {\n void closeAfterParentExit();\n});\n\nprocess.on(\"message\", (message: AutomationRequest | AutomationCancel) => {\n if (message.kind === \"cancel\") {\n void cancelOperation(message);\n return;\n }\n void runRequest(message);\n});\n\nasync function runRequest(request: AutomationRequest): Promise<void> {\n let complete: (() => void) | undefined;\n const operation: ActiveOperation = {\n cancelled: false,\n completed: new Promise<void>((resolve) => {\n complete = resolve;\n }),\n complete: () => complete?.(),\n };\n operations.set(request.id, operation);\n try {\n const value = await execute(request, operation);\n send({ kind: \"response\", id: request.id, ok: true, value });\n } catch (error) {\n send({ kind: \"response\", id: request.id, ok: false, error: serializeError(error) });\n } finally {\n operation.complete();\n operations.delete(request.id);\n }\n}\n\nasync function cancelOperation(request: AutomationCancel): Promise<void> {\n const operation = operations.get(request.id);\n if (operation !== undefined) {\n operation.cancelled = true;\n await operation.page?.close().catch(() => undefined);\n await operation.completed;\n }\n send({ kind: \"cancelled\", id: request.id });\n}\n\nasync function execute(request: AutomationRequest, operation: ActiveOperation): Promise<unknown> {\n const command: AutomationCommand = request;\n switch (command.method) {\n case \"connect\": {\n if (browser !== undefined) throw new Error(\"Automation worker is already connected.\");\n const connected = await chromium.connectOverCDP(command.params.endpointUrl, {\n timeout: command.params.timeoutMs,\n });\n try {\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n const context = connected.contexts()[0];\n if (context === undefined) {\n throw new Error(\"CDP connection did not expose a browser context.\");\n }\n const session = await connected.newBrowserCDPSession();\n try {\n await session.send(\"Browser.getVersion\");\n } finally {\n await session.detach().catch(() => undefined);\n }\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n } catch (error) {\n await connected.close().catch(() => undefined);\n throw error;\n }\n browser = connected;\n selected = pages()[0];\n for (const page of pages()) idFor(page);\n connected.once(\"disconnected\", () => {\n browser = undefined;\n send({ kind: \"event\", event: \"disconnected\" });\n });\n return undefined;\n }\n case \"disconnect\": {\n const connected = browser;\n browser = undefined;\n selected = undefined;\n if (connected !== undefined) await connected.close();\n return undefined;\n }\n case \"listTabs\":\n return tabSummaries();\n case \"setNavigationPolicy\":\n await setNavigationPolicy(command.params.policy);\n return undefined;\n case \"openTab\": {\n const context = assertBrowser().contexts()[0];\n if (context === undefined) throw new Error(\"Browser has no CDP context.\");\n const page = await context.newPage();\n operation.page = page;\n if (operation.cancelled) {\n await page.close().catch(() => undefined);\n throw new Error(\"Open-tab operation was cancelled.\");\n }\n selected = page;\n return tabResult(page);\n }\n case \"selectTab\": {\n const page = pageFor(command.params.tabId);\n selected = page;\n return tabResult(page);\n }\n case \"closeTab\": {\n const page = pageFor(command.params.tabId);\n operation.page = page;\n await page.close();\n if (page === selected) selected = pages()[0];\n return { closedTabId: command.params.tabId, tabs: await tabSummaries() };\n }\n case \"navigate\": {\n const page = targetPage(command.params.tabId, operation);\n await page.goto(command.params.url, {\n timeout: command.params.timeoutMs,\n waitUntil: command.params.waitUntil,\n });\n return tabResult(page);\n }\n case \"snapshot\": {\n const page = targetPage(command.params.tabId, operation);\n const snapshot = await page.locator(\"body\").ariaSnapshot({\n timeout: command.params.timeoutMs,\n });\n const truncated = snapshot.length > command.params.maxChars;\n return {\n ...(await tabResult(page)),\n snapshot: truncated ? snapshot.slice(0, command.params.maxChars) : snapshot,\n truncated,\n };\n }\n case \"click\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).click({ timeout: command.params.timeoutMs });\n return tabResult(page);\n }\n case \"type\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).fill(command.params.text, {\n timeout: command.params.timeoutMs,\n });\n return tabResult(page);\n }\n case \"pressKey\": {\n const page = targetPage(command.params.tabId, operation);\n await page.keyboard.press(command.params.key);\n return tabResult(page);\n }\n case \"screenshot\": {\n const page = targetPage(command.params.tabId, operation);\n const png = await page.screenshot({\n type: \"png\",\n fullPage: false,\n timeout: command.params.timeoutMs,\n });\n return { metadata: await tabResult(page), pngBase64: png.toString(\"base64\") };\n }\n }\n}\n\nfunction assertBrowser(): Browser {\n if (browser === undefined || !browser.isConnected())\n throw new Error(\"Browser connection is closed.\");\n return browser;\n}\n\nfunction pages(): Page[] {\n return browser?.contexts().flatMap((context) => context.pages()) ?? [];\n}\n\nfunction idFor(page: Page): string {\n const existing = tabIds.get(page);\n if (existing !== undefined) return existing;\n const id = randomUUID();\n tabIds.set(page, id);\n pagesById.set(id, page);\n page.once(\"close\", () => pagesById.delete(id));\n return id;\n}\n\nfunction pageFor(id: string): Page {\n assertBrowser();\n const page = pagesById.get(id);\n if (page === undefined || page.isClosed() || !pages().includes(page)) {\n throw new Error(`Browser tab does not exist: ${id}`);\n }\n return page;\n}\n\nfunction targetPage(id: string, operation: ActiveOperation): Page {\n const page = pageFor(id);\n operation.page = page;\n return page;\n}\n\nasync function tabSummaries(): Promise<readonly BrowserTab[]> {\n assertBrowser();\n if (selected?.isClosed()) selected = pages()[0];\n return Promise.all(\n pages().map(async (page) => ({\n id: idFor(page),\n title: await page.title(),\n url: page.url(),\n selected: page === selected,\n })),\n );\n}\n\nasync function tabResult(page: Page) {\n return { tabId: idFor(page), title: await page.title(), url: page.url() };\n}\n\nasync function setNavigationPolicy(policy: BrowserNavigationPolicy): Promise<void> {\n const key = JSON.stringify(policy);\n if (navigationPolicyKey !== undefined) {\n if (navigationPolicyKey !== key) {\n throw new TypeError(\"Browser connection already has a different navigation policy.\");\n }\n return;\n }\n navigationPolicyKey = key;\n await Promise.all(\n assertBrowser()\n .contexts()\n .map((context) => context.route(\"**/*\", (route) => enforceNavigationPolicy(route, policy))),\n );\n}\n\nasync function enforceNavigationPolicy(\n route: Route,\n policy: BrowserNavigationPolicy,\n): Promise<void> {\n const request = route.request();\n const frame = request.frame();\n if (\n request.isNavigationRequest() &&\n frame === frame.page().mainFrame() &&\n !isNavigationAllowed(request.url(), policy)\n ) {\n await route.abort(\"blockedbyclient\");\n return;\n }\n await route.continue();\n}\n\nfunction isNavigationAllowed(value: string, policy: BrowserNavigationPolicy): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (url.username.length > 0 || url.password.length > 0) return false;\n return policy.mode === \"allow-all-http\" || policy.origins.includes(url.origin);\n}\n\nfunction locatorFor(page: Page, target: BrowserTarget): Locator {\n switch (target.by) {\n case \"role\": {\n const options: { name?: string; exact?: boolean } = {};\n if (target.name !== undefined) options.name = target.name;\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByRole(target.role as never, options);\n }\n case \"text\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByText(target.text, options);\n }\n case \"label\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByLabel(target.label, options);\n }\n case \"placeholder\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByPlaceholder(target.placeholder, options);\n }\n case \"test-id\":\n return page.getByTestId(target.testId);\n case \"css\":\n return page.locator(target.selector);\n }\n}\n\nfunction serializeError(error: unknown): SerializedError {\n if (error instanceof Error) {\n const value: { name: string; message: string; stack?: string; code?: string } = {\n name: error.name,\n message: error.message,\n };\n if (error.stack !== undefined) value.stack = error.stack;\n if (\"code\" in error && typeof error.code === \"string\") value.code = error.code;\n return value;\n }\n return { name: \"Error\", message: String(error) };\n}\n\nfunction send(message: AutomationResponse): void {\n if (!process.connected || process.send === undefined) return;\n process.send(message, () => undefined);\n}\n\nasync function closeAfterParentExit(): Promise<void> {\n const forceExit = setTimeout(() => process.exit(1), 2_000);\n const connected = browser;\n browser = undefined;\n await connected?.close().catch(() => undefined);\n clearTimeout(forceExit);\n process.exit(0);\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;AAE3B,SAAS,gBAAgB;AAkBzB,IAAI;AACJ,IAAI;AACJ,IAAM,SAAS,oBAAI,QAAsB;AACzC,IAAM,YAAY,oBAAI,IAAkB;AACxC,IAAM,aAAa,oBAAI,IAA6B;AACpD,IAAI;AAEJ,QAAQ,KAAK,cAAc,MAAM;AAC/B,OAAK,qBAAqB;AAC5B,CAAC;AAED,QAAQ,GAAG,WAAW,CAAC,YAAkD;AACvE,MAAI,QAAQ,SAAS,UAAU;AAC7B,SAAK,gBAAgB,OAAO;AAC5B;AAAA,EACF;AACA,OAAK,WAAW,OAAO;AACzB,CAAC;AAED,eAAe,WAAW,SAA2C;AACnE,MAAI;AACJ,QAAM,YAA6B;AAAA,IACjC,WAAW;AAAA,IACX,WAAW,IAAI,QAAc,CAAC,YAAY;AACxC,iBAAW;AAAA,IACb,CAAC;AAAA,IACD,UAAU,MAAM,WAAW;AAAA,EAC7B;AACA,aAAW,IAAI,QAAQ,IAAI,SAAS;AACpC,MAAI;AACF,UAAM,QAAQ,MAAM,QAAQ,SAAS,SAAS;AAC9C,SAAK,EAAE,MAAM,YAAY,IAAI,QAAQ,IAAI,IAAI,MAAM,MAAM,CAAC;AAAA,EAC5D,SAAS,OAAO;AACd,SAAK,EAAE,MAAM,YAAY,IAAI,QAAQ,IAAI,IAAI,OAAO,OAAO,eAAe,KAAK,EAAE,CAAC;AAAA,EACpF,UAAE;AACA,cAAU,SAAS;AACnB,eAAW,OAAO,QAAQ,EAAE;AAAA,EAC9B;AACF;AAEA,eAAe,gBAAgB,SAA0C;AACvE,QAAM,YAAY,WAAW,IAAI,QAAQ,EAAE;AAC3C,MAAI,cAAc,QAAW;AAC3B,cAAU,YAAY;AACtB,UAAM,UAAU,MAAM,MAAM,EAAE,MAAM,MAAM,MAAS;AACnD,UAAM,UAAU;AAAA,EAClB;AACA,OAAK,EAAE,MAAM,aAAa,IAAI,QAAQ,GAAG,CAAC;AAC5C;AAEA,eAAe,QAAQ,SAA4B,WAA8C;AAC/F,QAAM,UAA6B;AACnC,UAAQ,QAAQ,QAAQ;AAAA,IACtB,KAAK,WAAW;AACd,UAAI,YAAY,OAAW,OAAM,IAAI,MAAM,yCAAyC;AACpF,YAAM,YAAY,MAAM,SAAS,eAAe,QAAQ,OAAO,aAAa;AAAA,QAC1E,SAAS,QAAQ,OAAO;AAAA,MAC1B,CAAC;AACD,UAAI;AACF,YAAI,UAAU,UAAW,OAAM,IAAI,MAAM,mCAAmC;AAC5E,cAAM,UAAU,UAAU,SAAS,EAAE,CAAC;AACtC,YAAI,YAAY,QAAW;AACzB,gBAAM,IAAI,MAAM,kDAAkD;AAAA,QACpE;AACA,cAAM,UAAU,MAAM,UAAU,qBAAqB;AACrD,YAAI;AACF,gBAAM,QAAQ,KAAK,oBAAoB;AAAA,QACzC,UAAE;AACA,gBAAM,QAAQ,OAAO,EAAE,MAAM,MAAM,MAAS;AAAA,QAC9C;AACA,YAAI,UAAU,UAAW,OAAM,IAAI,MAAM,mCAAmC;AAAA,MAC9E,SAAS,OAAO;AACd,cAAM,UAAU,MAAM,EAAE,MAAM,MAAM,MAAS;AAC7C,cAAM;AAAA,MACR;AACA,gBAAU;AACV,iBAAW,MAAM,EAAE,CAAC;AACpB,iBAAW,QAAQ,MAAM,EAAG,OAAM,IAAI;AACtC,gBAAU,KAAK,gBAAgB,MAAM;AACnC,kBAAU;AACV,aAAK,EAAE,MAAM,SAAS,OAAO,eAAe,CAAC;AAAA,MAC/C,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IACA,KAAK,cAAc;AACjB,YAAM,YAAY;AAClB,gBAAU;AACV,iBAAW;AACX,UAAI,cAAc,OAAW,OAAM,UAAU,MAAM;AACnD,aAAO;AAAA,IACT;AAAA,IACA,KAAK;AACH,aAAO,aAAa;AAAA,IACtB,KAAK;AACH,YAAM,oBAAoB,QAAQ,OAAO,MAAM;AAC/C,aAAO;AAAA,IACT,KAAK,WAAW;AACd,YAAM,UAAU,cAAc,EAAE,SAAS,EAAE,CAAC;AAC5C,UAAI,YAAY,OAAW,OAAM,IAAI,MAAM,6BAA6B;AACxE,YAAM,OAAO,MAAM,QAAQ,QAAQ;AACnC,gBAAU,OAAO;AACjB,UAAI,UAAU,WAAW;AACvB,cAAM,KAAK,MAAM,EAAE,MAAM,MAAM,MAAS;AACxC,cAAM,IAAI,MAAM,mCAAmC;AAAA,MACrD;AACA,iBAAW;AACX,aAAO,UAAU,IAAI;AAAA,IACvB;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,OAAO,QAAQ,QAAQ,OAAO,KAAK;AACzC,iBAAW;AACX,aAAO,UAAU,IAAI;AAAA,IACvB;AAAA,IACA,KAAK,YAAY;AACf,YAAM,OAAO,QAAQ,QAAQ,OAAO,KAAK;AACzC,gBAAU,OAAO;AACjB,YAAM,KAAK,MAAM;AACjB,UAAI,SAAS,SAAU,YAAW,MAAM,EAAE,CAAC;AAC3C,aAAO,EAAE,aAAa,QAAQ,OAAO,OAAO,MAAM,MAAM,aAAa,EAAE;AAAA,IACzE;AAAA,IACA,KAAK,YAAY;AACf,YAAM,OAAO,WAAW,QAAQ,OAAO,OAAO,SAAS;AACvD,YAAM,KAAK,KAAK,QAAQ,OAAO,KAAK;AAAA,QAClC,SAAS,QAAQ,OAAO;AAAA,QACxB,WAAW,QAAQ,OAAO;AAAA,MAC5B,CAAC;AACD,aAAO,UAAU,IAAI;AAAA,IACvB;AAAA,IACA,KAAK,YAAY;AACf,YAAM,OAAO,WAAW,QAAQ,OAAO,OAAO,SAAS;AACvD,YAAM,WAAW,MAAM,KAAK,QAAQ,MAAM,EAAE,aAAa;AAAA,QACvD,SAAS,QAAQ,OAAO;AAAA,MAC1B,CAAC;AACD,YAAM,YAAY,SAAS,SAAS,QAAQ,OAAO;AACnD,aAAO;AAAA,QACL,GAAI,MAAM,UAAU,IAAI;AAAA,QACxB,UAAU,YAAY,SAAS,MAAM,GAAG,QAAQ,OAAO,QAAQ,IAAI;AAAA,QACnE;AAAA,MACF;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,OAAO,WAAW,QAAQ,OAAO,OAAO,SAAS;AACvD,YAAM,WAAW,MAAM,QAAQ,OAAO,MAAM,EAAE,MAAM,EAAE,SAAS,QAAQ,OAAO,UAAU,CAAC;AACzF,aAAO,UAAU,IAAI;AAAA,IACvB;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,OAAO,WAAW,QAAQ,OAAO,OAAO,SAAS;AACvD,YAAM,WAAW,MAAM,QAAQ,OAAO,MAAM,EAAE,KAAK,QAAQ,OAAO,MAAM;AAAA,QACtE,SAAS,QAAQ,OAAO;AAAA,MAC1B,CAAC;AACD,aAAO,UAAU,IAAI;AAAA,IACvB;AAAA,IACA,KAAK,YAAY;AACf,YAAM,OAAO,WAAW,QAAQ,OAAO,OAAO,SAAS;AACvD,YAAM,KAAK,SAAS,MAAM,QAAQ,OAAO,GAAG;AAC5C,aAAO,UAAU,IAAI;AAAA,IACvB;AAAA,IACA,KAAK,cAAc;AACjB,YAAM,OAAO,WAAW,QAAQ,OAAO,OAAO,SAAS;AACvD,YAAM,MAAM,MAAM,KAAK,WAAW;AAAA,QAChC,MAAM;AAAA,QACN,UAAU;AAAA,QACV,SAAS,QAAQ,OAAO;AAAA,MAC1B,CAAC;AACD,aAAO,EAAE,UAAU,MAAM,UAAU,IAAI,GAAG,WAAW,IAAI,SAAS,QAAQ,EAAE;AAAA,IAC9E;AAAA,EACF;AACF;AAEA,SAAS,gBAAyB;AAChC,MAAI,YAAY,UAAa,CAAC,QAAQ,YAAY;AAChD,UAAM,IAAI,MAAM,+BAA+B;AACjD,SAAO;AACT;AAEA,SAAS,QAAgB;AACvB,SAAO,SAAS,SAAS,EAAE,QAAQ,CAAC,YAAY,QAAQ,MAAM,CAAC,KAAK,CAAC;AACvE;AAEA,SAAS,MAAM,MAAoB;AACjC,QAAM,WAAW,OAAO,IAAI,IAAI;AAChC,MAAI,aAAa,OAAW,QAAO;AACnC,QAAM,KAAK,WAAW;AACtB,SAAO,IAAI,MAAM,EAAE;AACnB,YAAU,IAAI,IAAI,IAAI;AACtB,OAAK,KAAK,SAAS,MAAM,UAAU,OAAO,EAAE,CAAC;AAC7C,SAAO;AACT;AAEA,SAAS,QAAQ,IAAkB;AACjC,gBAAc;AACd,QAAM,OAAO,UAAU,IAAI,EAAE;AAC7B,MAAI,SAAS,UAAa,KAAK,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG;AACpE,UAAM,IAAI,MAAM,+BAA+B,EAAE,EAAE;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,WAAW,IAAY,WAAkC;AAChE,QAAM,OAAO,QAAQ,EAAE;AACvB,YAAU,OAAO;AACjB,SAAO;AACT;AAEA,eAAe,eAA+C;AAC5D,gBAAc;AACd,MAAI,UAAU,SAAS,EAAG,YAAW,MAAM,EAAE,CAAC;AAC9C,SAAO,QAAQ;AAAA,IACb,MAAM,EAAE,IAAI,OAAO,UAAU;AAAA,MAC3B,IAAI,MAAM,IAAI;AAAA,MACd,OAAO,MAAM,KAAK,MAAM;AAAA,MACxB,KAAK,KAAK,IAAI;AAAA,MACd,UAAU,SAAS;AAAA,IACrB,EAAE;AAAA,EACJ;AACF;AAEA,eAAe,UAAU,MAAY;AACnC,SAAO,EAAE,OAAO,MAAM,IAAI,GAAG,OAAO,MAAM,KAAK,MAAM,GAAG,KAAK,KAAK,IAAI,EAAE;AAC1E;AAEA,eAAe,oBAAoB,QAAgD;AACjF,QAAM,MAAM,KAAK,UAAU,MAAM;AACjC,MAAI,wBAAwB,QAAW;AACrC,QAAI,wBAAwB,KAAK;AAC/B,YAAM,IAAI,UAAU,+DAA+D;AAAA,IACrF;AACA;AAAA,EACF;AACA,wBAAsB;AACtB,QAAM,QAAQ;AAAA,IACZ,cAAc,EACX,SAAS,EACT,IAAI,CAAC,YAAY,QAAQ,MAAM,QAAQ,CAAC,UAAU,wBAAwB,OAAO,MAAM,CAAC,CAAC;AAAA,EAC9F;AACF;AAEA,eAAe,wBACb,OACA,QACe;AACf,QAAM,UAAU,MAAM,QAAQ;AAC9B,QAAM,QAAQ,QAAQ,MAAM;AAC5B,MACE,QAAQ,oBAAoB,KAC5B,UAAU,MAAM,KAAK,EAAE,UAAU,KACjC,CAAC,oBAAoB,QAAQ,IAAI,GAAG,MAAM,GAC1C;AACA,UAAM,MAAM,MAAM,iBAAiB;AACnC;AAAA,EACF;AACA,QAAM,MAAM,SAAS;AACvB;AAEA,SAAS,oBAAoB,OAAe,QAA0C;AACpF,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,WAAW,IAAI,aAAa,SAAU,QAAO;AAClE,MAAI,IAAI,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,EAAG,QAAO;AAC/D,SAAO,OAAO,SAAS,oBAAoB,OAAO,QAAQ,SAAS,IAAI,MAAM;AAC/E;AAEA,SAAS,WAAW,MAAY,QAAgC;AAC9D,UAAQ,OAAO,IAAI;AAAA,IACjB,KAAK,QAAQ;AACX,YAAM,UAA8C,CAAC;AACrD,UAAI,OAAO,SAAS,OAAW,SAAQ,OAAO,OAAO;AACrD,UAAI,OAAO,UAAU,OAAW,SAAQ,QAAQ,OAAO;AACvD,aAAO,KAAK,UAAU,OAAO,MAAe,OAAO;AAAA,IACrD;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,UAA+B,CAAC;AACtC,UAAI,OAAO,UAAU,OAAW,SAAQ,QAAQ,OAAO;AACvD,aAAO,KAAK,UAAU,OAAO,MAAM,OAAO;AAAA,IAC5C;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,UAA+B,CAAC;AACtC,UAAI,OAAO,UAAU,OAAW,SAAQ,QAAQ,OAAO;AACvD,aAAO,KAAK,WAAW,OAAO,OAAO,OAAO;AAAA,IAC9C;AAAA,IACA,KAAK,eAAe;AAClB,YAAM,UAA+B,CAAC;AACtC,UAAI,OAAO,UAAU,OAAW,SAAQ,QAAQ,OAAO;AACvD,aAAO,KAAK,iBAAiB,OAAO,aAAa,OAAO;AAAA,IAC1D;AAAA,IACA,KAAK;AACH,aAAO,KAAK,YAAY,OAAO,MAAM;AAAA,IACvC,KAAK;AACH,aAAO,KAAK,QAAQ,OAAO,QAAQ;AAAA,EACvC;AACF;AAEA,SAAS,eAAe,OAAiC;AACvD,MAAI,iBAAiB,OAAO;AAC1B,UAAM,QAA0E;AAAA,MAC9E,MAAM,MAAM;AAAA,MACZ,SAAS,MAAM;AAAA,IACjB;AACA,QAAI,MAAM,UAAU,OAAW,OAAM,QAAQ,MAAM;AACnD,QAAI,UAAU,SAAS,OAAO,MAAM,SAAS,SAAU,OAAM,OAAO,MAAM;AAC1E,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,SAAS,SAAS,OAAO,KAAK,EAAE;AACjD;AAEA,SAAS,KAAK,SAAmC;AAC/C,MAAI,CAAC,QAAQ,aAAa,QAAQ,SAAS,OAAW;AACtD,UAAQ,KAAK,SAAS,MAAM,MAAS;AACvC;AAEA,eAAe,uBAAsC;AACnD,QAAM,YAAY,WAAW,MAAM,QAAQ,KAAK,CAAC,GAAG,GAAK;AACzD,QAAM,YAAY;AAClB,YAAU;AACV,QAAM,WAAW,MAAM,EAAE,MAAM,MAAM,MAAS;AAC9C,eAAa,SAAS;AACtB,UAAQ,KAAK,CAAC;AAChB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/automation-worker.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Browser, Locator, Page } from \"playwright-core\";\nimport { chromium } from \"playwright-core\";\nimport type {\n AutomationCancel,\n AutomationCommand,\n AutomationRequest,\n AutomationResponse,\n BrowserTarget,\n SerializedError,\n} from \"./automation-protocol\";\nimport { enforceNavigationPolicy } from \"./internal/navigation-policy\";\nimport type { BrowserNavigationPolicy, BrowserTab } from \"./types\";\n\ntype ActiveOperation = {\n cancelled: boolean;\n page?: Page;\n completed: Promise<void>;\n complete: () => void;\n};\n\nlet browser: Browser | undefined;\nlet selected: Page | undefined;\nconst tabIds = new WeakMap<Page, string>();\nconst pagesById = new Map<string, Page>();\nconst operations = new Map<number, ActiveOperation>();\nlet navigationPolicyKey: string | undefined;\n\nprocess.once(\"disconnect\", () => {\n void closeAfterParentExit();\n});\n\nprocess.on(\"message\", (message: AutomationRequest | AutomationCancel) => {\n if (message.kind === \"cancel\") {\n void cancelOperation(message);\n return;\n }\n void runRequest(message);\n});\n\nasync function runRequest(request: AutomationRequest): Promise<void> {\n let complete: (() => void) | undefined;\n const operation: ActiveOperation = {\n cancelled: false,\n completed: new Promise<void>((resolve) => {\n complete = resolve;\n }),\n complete: () => complete?.(),\n };\n operations.set(request.id, operation);\n try {\n const value = await execute(request, operation);\n send({ kind: \"response\", id: request.id, ok: true, value });\n } catch (error) {\n send({ kind: \"response\", id: request.id, ok: false, error: serializeError(error) });\n } finally {\n operation.complete();\n operations.delete(request.id);\n }\n}\n\nasync function cancelOperation(request: AutomationCancel): Promise<void> {\n const operation = operations.get(request.id);\n if (operation !== undefined) {\n operation.cancelled = true;\n await operation.page?.close().catch(() => undefined);\n await operation.completed;\n }\n send({ kind: \"cancelled\", id: request.id });\n}\n\nasync function execute(request: AutomationRequest, operation: ActiveOperation): Promise<unknown> {\n const command: AutomationCommand = request;\n switch (command.method) {\n case \"connect\": {\n if (browser !== undefined) throw new Error(\"Automation worker is already connected.\");\n const connected = await chromium.connectOverCDP(command.params.endpointUrl, {\n timeout: command.params.timeoutMs,\n });\n try {\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n const context = connected.contexts()[0];\n if (context === undefined) {\n throw new Error(\"CDP connection did not expose a browser context.\");\n }\n const session = await connected.newBrowserCDPSession();\n try {\n await session.send(\"Browser.getVersion\");\n } finally {\n await session.detach().catch(() => undefined);\n }\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n } catch (error) {\n await connected.close().catch(() => undefined);\n throw error;\n }\n browser = connected;\n selected = pages()[0];\n for (const page of pages()) idFor(page);\n connected.once(\"disconnected\", () => {\n browser = undefined;\n send({ kind: \"event\", event: \"disconnected\" });\n });\n return undefined;\n }\n case \"disconnect\": {\n const connected = browser;\n browser = undefined;\n selected = undefined;\n if (connected !== undefined) await connected.close();\n return undefined;\n }\n case \"listTabs\":\n return tabSummaries();\n case \"setNavigationPolicy\":\n await setNavigationPolicy(command.params.policy);\n return undefined;\n case \"openTab\": {\n const context = assertBrowser().contexts()[0];\n if (context === undefined) throw new Error(\"Browser has no CDP context.\");\n const page = await context.newPage();\n operation.page = page;\n if (operation.cancelled) {\n await page.close().catch(() => undefined);\n throw new Error(\"Open-tab operation was cancelled.\");\n }\n selected = page;\n return tabResult(page);\n }\n case \"selectTab\": {\n const page = pageFor(command.params.tabId);\n selected = page;\n return tabResult(page);\n }\n case \"closeTab\": {\n const page = pageFor(command.params.tabId);\n operation.page = page;\n await page.close();\n if (page === selected) selected = pages()[0];\n return { closedTabId: command.params.tabId, tabs: await tabSummaries() };\n }\n case \"navigate\": {\n const page = targetPage(command.params.tabId, operation);\n await page.goto(command.params.url, {\n timeout: command.params.timeoutMs,\n waitUntil: command.params.waitUntil,\n });\n return tabResult(page);\n }\n case \"snapshot\": {\n const page = targetPage(command.params.tabId, operation);\n const snapshot = await page.locator(\"body\").ariaSnapshot({\n timeout: command.params.timeoutMs,\n });\n const truncated = snapshot.length > command.params.maxChars;\n return {\n ...(await tabResult(page)),\n snapshot: truncated ? snapshot.slice(0, command.params.maxChars) : snapshot,\n truncated,\n };\n }\n case \"click\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).click({ timeout: command.params.timeoutMs });\n return tabResult(page);\n }\n case \"type\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).fill(command.params.text, {\n timeout: command.params.timeoutMs,\n });\n return tabResult(page);\n }\n case \"pressKey\": {\n const page = targetPage(command.params.tabId, operation);\n await page.keyboard.press(command.params.key);\n return tabResult(page);\n }\n case \"screenshot\": {\n const page = targetPage(command.params.tabId, operation);\n const png = await page.screenshot({\n type: \"png\",\n fullPage: false,\n timeout: command.params.timeoutMs,\n });\n return { metadata: await tabResult(page), pngBase64: png.toString(\"base64\") };\n }\n }\n}\n\nfunction assertBrowser(): Browser {\n if (browser === undefined || !browser.isConnected())\n throw new Error(\"Browser connection is closed.\");\n return browser;\n}\n\nfunction pages(): Page[] {\n return browser?.contexts().flatMap((context) => context.pages()) ?? [];\n}\n\nfunction idFor(page: Page): string {\n const existing = tabIds.get(page);\n if (existing !== undefined) return existing;\n const id = randomUUID();\n tabIds.set(page, id);\n pagesById.set(id, page);\n page.once(\"close\", () => pagesById.delete(id));\n return id;\n}\n\nfunction pageFor(id: string): Page {\n assertBrowser();\n const page = pagesById.get(id);\n if (page === undefined || page.isClosed() || !pages().includes(page)) {\n throw new Error(`Browser tab does not exist: ${id}`);\n }\n return page;\n}\n\nfunction targetPage(id: string, operation: ActiveOperation): Page {\n const page = pageFor(id);\n operation.page = page;\n return page;\n}\n\nasync function tabSummaries(): Promise<readonly BrowserTab[]> {\n assertBrowser();\n if (selected?.isClosed()) selected = pages()[0];\n return Promise.all(\n pages().map(async (page) => ({\n id: idFor(page),\n title: await page.title(),\n url: page.url(),\n selected: page === selected,\n })),\n );\n}\n\nasync function tabResult(page: Page) {\n return { tabId: idFor(page), title: await page.title(), url: page.url() };\n}\n\nasync function setNavigationPolicy(policy: BrowserNavigationPolicy): Promise<void> {\n const key = JSON.stringify(policy);\n if (navigationPolicyKey !== undefined) {\n if (navigationPolicyKey !== key) {\n throw new TypeError(\"Browser connection already has a different navigation policy.\");\n }\n return;\n }\n navigationPolicyKey = key;\n await Promise.all(\n assertBrowser()\n .contexts()\n .map((context) => context.route(\"**/*\", (route) => enforceNavigationPolicy(route, policy))),\n );\n}\n\nfunction locatorFor(page: Page, target: BrowserTarget): Locator {\n switch (target.by) {\n case \"role\": {\n const options: { name?: string; exact?: boolean } = {};\n if (target.name !== undefined) options.name = target.name;\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByRole(target.role as never, options);\n }\n case \"text\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByText(target.text, options);\n }\n case \"label\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByLabel(target.label, options);\n }\n case \"placeholder\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByPlaceholder(target.placeholder, options);\n }\n case \"test-id\":\n return page.getByTestId(target.testId);\n case \"css\":\n return page.locator(target.selector);\n }\n}\n\nfunction serializeError(error: unknown): SerializedError {\n if (error instanceof Error) {\n const value: { name: string; message: string; stack?: string; code?: string } = {\n name: error.name,\n message: error.message,\n };\n if (error.stack !== undefined) value.stack = error.stack;\n if (\"code\" in error && typeof error.code === \"string\") value.code = error.code;\n return value;\n }\n return { name: \"Error\", message: String(error) };\n}\n\nfunction send(message: AutomationResponse): void {\n if (!process.connected || process.send === undefined) return;\n process.send(message, () => undefined);\n}\n\nasync function closeAfterParentExit(): Promise<void> {\n const forceExit = setTimeout(() => process.exit(1), 2_000);\n const connected = browser;\n browser = undefined;\n await connected?.close().catch(() => undefined);\n clearTimeout(forceExit);\n process.exit(0);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/internal/ip-check.ts
|
|
2
|
+
var BLOCKED_IPV4 = [
|
|
3
|
+
["0.0.0.0", 8],
|
|
4
|
+
// "this network"
|
|
5
|
+
["10.0.0.0", 8],
|
|
6
|
+
// private network
|
|
7
|
+
["100.64.0.0", 10],
|
|
8
|
+
// shared address space (carrier-grade NAT)
|
|
9
|
+
["127.0.0.0", 8],
|
|
10
|
+
// loopback
|
|
11
|
+
["169.254.0.0", 16],
|
|
12
|
+
// link-local, includes cloud metadata at 169.254.169.254
|
|
13
|
+
["172.16.0.0", 12],
|
|
14
|
+
// private network
|
|
15
|
+
["192.0.0.0", 24],
|
|
16
|
+
// IETF protocol assignments
|
|
17
|
+
["192.0.2.0", 24],
|
|
18
|
+
// documentation (TEST-NET-1)
|
|
19
|
+
["192.88.99.0", 24],
|
|
20
|
+
// 6to4 relay anycast (deprecated)
|
|
21
|
+
["192.168.0.0", 16],
|
|
22
|
+
// private network
|
|
23
|
+
["198.18.0.0", 15],
|
|
24
|
+
// benchmarking
|
|
25
|
+
["198.51.100.0", 24],
|
|
26
|
+
// documentation (TEST-NET-2)
|
|
27
|
+
["203.0.113.0", 24],
|
|
28
|
+
// documentation (TEST-NET-3)
|
|
29
|
+
["224.0.0.0", 4],
|
|
30
|
+
// multicast
|
|
31
|
+
["240.0.0.0", 4]
|
|
32
|
+
// reserved, includes 255.255.255.255
|
|
33
|
+
];
|
|
34
|
+
var BLOCKED_IPV6 = [
|
|
35
|
+
["::", 96],
|
|
36
|
+
// unspecified, loopback, and deprecated IPv4-compatible addresses
|
|
37
|
+
["::ffff:0:0", 96],
|
|
38
|
+
// IPv4-mapped (alternate spelling of an IPv4 destination)
|
|
39
|
+
["64:ff9b:1::", 48],
|
|
40
|
+
// NAT64 local-use prefix
|
|
41
|
+
["100::", 64],
|
|
42
|
+
// discard-only
|
|
43
|
+
["100:0:0:1::", 64],
|
|
44
|
+
// dummy IPv6 prefix
|
|
45
|
+
["2001::", 32],
|
|
46
|
+
// Teredo
|
|
47
|
+
["2001:2::", 48],
|
|
48
|
+
// benchmarking
|
|
49
|
+
["2001:10::", 28],
|
|
50
|
+
// ORCHID (deprecated)
|
|
51
|
+
["2001:db8::", 32],
|
|
52
|
+
// documentation
|
|
53
|
+
["2002::", 16],
|
|
54
|
+
// 6to4
|
|
55
|
+
["3fff::", 20],
|
|
56
|
+
// documentation
|
|
57
|
+
["5f00::", 16],
|
|
58
|
+
// segment routing SIDs
|
|
59
|
+
["fc00::", 7],
|
|
60
|
+
// unique local
|
|
61
|
+
["fe80::", 10],
|
|
62
|
+
// link-local
|
|
63
|
+
["fec0::", 10],
|
|
64
|
+
// site-local (deprecated)
|
|
65
|
+
["ff00::", 8]
|
|
66
|
+
// multicast
|
|
67
|
+
];
|
|
68
|
+
var GLOBALLY_REACHABLE_IPV4 = /* @__PURE__ */ new Set([3221225481, 3221225482]);
|
|
69
|
+
var BLOCKED_IPV4_RANGES = compileIpv4Ranges(BLOCKED_IPV4);
|
|
70
|
+
var BLOCKED_IPV6_RANGES = compileIpv6Ranges(BLOCKED_IPV6);
|
|
71
|
+
function isPrivateOrReservedHost(hostname) {
|
|
72
|
+
const host = stripBrackets(hostname).toLowerCase().replace(/\.+$/, "");
|
|
73
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
74
|
+
const ipv4 = parseIpv4(host);
|
|
75
|
+
if (ipv4 !== null) return isBlockedIpv4(ipv4);
|
|
76
|
+
const ipv6 = parseIpv6(host);
|
|
77
|
+
if (ipv6 !== null) return isBlockedIpv6(ipv6);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function stripBrackets(hostname) {
|
|
81
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
82
|
+
}
|
|
83
|
+
function isBlockedIpv4(value) {
|
|
84
|
+
if (GLOBALLY_REACHABLE_IPV4.has(value)) return false;
|
|
85
|
+
for (const [network, prefix] of BLOCKED_IPV4_RANGES) {
|
|
86
|
+
if (isInIpv4Range(value, network, prefix)) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
function isBlockedIpv6(value) {
|
|
91
|
+
for (const [network, prefix] of BLOCKED_IPV6_RANGES) {
|
|
92
|
+
if (isInIpv6Range(value, network, prefix)) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function isInIpv4Range(value, network, prefix) {
|
|
97
|
+
if (prefix === 0) return true;
|
|
98
|
+
const shift = 32 - prefix;
|
|
99
|
+
return value >>> shift === network >>> shift;
|
|
100
|
+
}
|
|
101
|
+
function isInIpv6Range(value, network, prefix) {
|
|
102
|
+
if (prefix === 0) return true;
|
|
103
|
+
const shift = BigInt(128 - prefix);
|
|
104
|
+
return value >> shift === network >> shift;
|
|
105
|
+
}
|
|
106
|
+
function parseIpv4(value) {
|
|
107
|
+
const parts = value.split(".");
|
|
108
|
+
if (parts.length !== 4) return null;
|
|
109
|
+
let result = 0;
|
|
110
|
+
for (const part of parts) {
|
|
111
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
112
|
+
const octet = Number(part);
|
|
113
|
+
if (octet > 255) return null;
|
|
114
|
+
result = result * 256 + octet;
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function parseIpv6(value) {
|
|
119
|
+
const zone = value.indexOf("%");
|
|
120
|
+
const address = zone === -1 ? value : value.slice(0, zone);
|
|
121
|
+
let text = address;
|
|
122
|
+
const embedded = /(\d{1,3}(?:\.\d{1,3}){3})$/.exec(address);
|
|
123
|
+
if (embedded !== null) {
|
|
124
|
+
const ipv4 = parseIpv4(embedded[1] ?? "");
|
|
125
|
+
if (ipv4 === null) return null;
|
|
126
|
+
const groups = `${(ipv4 >>> 16).toString(16)}:${(ipv4 & 65535).toString(16)}`;
|
|
127
|
+
text = `${address.slice(0, embedded.index)}${groups}`;
|
|
128
|
+
}
|
|
129
|
+
const halves = text.split("::");
|
|
130
|
+
if (halves.length > 2) return null;
|
|
131
|
+
if (halves.length === 1) {
|
|
132
|
+
const groups = text.split(":");
|
|
133
|
+
return groups.length === 8 ? foldHexGroups(groups) : null;
|
|
134
|
+
}
|
|
135
|
+
const [head = "", tail = ""] = halves;
|
|
136
|
+
const leading = head === "" ? [] : head.split(":");
|
|
137
|
+
const trailing = tail === "" ? [] : tail.split(":");
|
|
138
|
+
const missing = 8 - leading.length - trailing.length;
|
|
139
|
+
if (missing < 1) return null;
|
|
140
|
+
const foldedHead = foldHexGroups(leading);
|
|
141
|
+
const foldedTail = foldHexGroups(trailing);
|
|
142
|
+
if (foldedHead === null || foldedTail === null) return null;
|
|
143
|
+
return foldedHead << BigInt(16 * (missing + trailing.length)) | foldedTail;
|
|
144
|
+
}
|
|
145
|
+
function foldHexGroups(groups) {
|
|
146
|
+
let value = 0n;
|
|
147
|
+
for (const group of groups) {
|
|
148
|
+
if (!/^[0-9a-f]{1,4}$/.test(group)) return null;
|
|
149
|
+
value = value << 16n | BigInt(parseInt(group, 16));
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function compileIpv4Ranges(ranges) {
|
|
154
|
+
return ranges.map(([network, prefix]) => {
|
|
155
|
+
const value = parseIpv4(network);
|
|
156
|
+
if (value === null) throw new TypeError(`Invalid IPv4 range: ${network}/${prefix}`);
|
|
157
|
+
return [value, prefix];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function compileIpv6Ranges(ranges) {
|
|
161
|
+
return ranges.map(([network, prefix]) => {
|
|
162
|
+
const value = parseIpv6(network);
|
|
163
|
+
if (value === null) throw new TypeError(`Invalid IPv6 range: ${network}/${prefix}`);
|
|
164
|
+
return [value, prefix];
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/internal/navigation-policy.ts
|
|
169
|
+
function isNavigationAllowed(value, policy) {
|
|
170
|
+
let url;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(value);
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
177
|
+
if (url.username.length > 0 || url.password.length > 0) return false;
|
|
178
|
+
if (isPrivateOrReservedHost(url.hostname)) return false;
|
|
179
|
+
return policy.mode === "allow-all-http" || policy.origins.includes(url.origin);
|
|
180
|
+
}
|
|
181
|
+
async function enforceNavigationPolicy(route, policy) {
|
|
182
|
+
const request = route.request();
|
|
183
|
+
const frame = request.frame();
|
|
184
|
+
if (request.isNavigationRequest() && frame === frame.page().mainFrame() && !isNavigationAllowed(request.url(), policy)) {
|
|
185
|
+
await route.abort("blockedbyclient");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await route.continue();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
isNavigationAllowed,
|
|
193
|
+
enforceNavigationPolicy
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=chunk-XE5WKN5F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/ip-check.ts","../src/internal/navigation-policy.ts"],"sourcesContent":["/**\n * Shared SSRF guard: classify a hostname as private, reserved, or public.\n *\n * Called from the navigation policy checks in ./navigation-policy.ts, which run both\n * before the tool dispatches a navigation and inside the worker's route handler for\n * every top-level navigation request, including redirects.\n *\n * Blocked ranges follow the IANA IPv4/IPv6 special-purpose address registries.\n * Addresses that IANA marks globally reachable inside otherwise special-purpose space\n * stay reachable.\n *\n * IPv4 spellings are normalized by the WHATWG URL parser before this guard runs\n * (`127.1`, `2130706433`, `0x7f.0.0.1`, and `0177.0.0.1` all arrive as `127.0.0.1`),\n * and hosts that are not valid literals are left to DNS.\n */\n\ntype AddressRange = readonly [network: string, prefix: number];\n\n/** Non-global IPv4 ranges plus transition mechanisms that can encode another address. */\nconst BLOCKED_IPV4: readonly AddressRange[] = [\n [\"0.0.0.0\", 8], // \"this network\"\n [\"10.0.0.0\", 8], // private network\n [\"100.64.0.0\", 10], // shared address space (carrier-grade NAT)\n [\"127.0.0.0\", 8], // loopback\n [\"169.254.0.0\", 16], // link-local, includes cloud metadata at 169.254.169.254\n [\"172.16.0.0\", 12], // private network\n [\"192.0.0.0\", 24], // IETF protocol assignments\n [\"192.0.2.0\", 24], // documentation (TEST-NET-1)\n [\"192.88.99.0\", 24], // 6to4 relay anycast (deprecated)\n [\"192.168.0.0\", 16], // private network\n [\"198.18.0.0\", 15], // benchmarking\n [\"198.51.100.0\", 24], // documentation (TEST-NET-2)\n [\"203.0.113.0\", 24], // documentation (TEST-NET-3)\n [\"224.0.0.0\", 4], // multicast\n [\"240.0.0.0\", 4], // reserved, includes 255.255.255.255\n];\n\n/** Non-global IPv6 ranges plus transition mechanisms that can encode another address. */\nconst BLOCKED_IPV6: readonly AddressRange[] = [\n [\"::\", 96], // unspecified, loopback, and deprecated IPv4-compatible addresses\n [\"::ffff:0:0\", 96], // IPv4-mapped (alternate spelling of an IPv4 destination)\n [\"64:ff9b:1::\", 48], // NAT64 local-use prefix\n [\"100::\", 64], // discard-only\n [\"100:0:0:1::\", 64], // dummy IPv6 prefix\n [\"2001::\", 32], // Teredo\n [\"2001:2::\", 48], // benchmarking\n [\"2001:10::\", 28], // ORCHID (deprecated)\n [\"2001:db8::\", 32], // documentation\n [\"2002::\", 16], // 6to4\n [\"3fff::\", 20], // documentation\n [\"5f00::\", 16], // segment routing SIDs\n [\"fc00::\", 7], // unique local\n [\"fe80::\", 10], // link-local\n [\"fec0::\", 10], // site-local (deprecated)\n [\"ff00::\", 8], // multicast\n];\n\n/** IANA marks these two /32s globally reachable although 192.0.0.0/24 is not. */\nconst GLOBALLY_REACHABLE_IPV4: ReadonlySet<number> = new Set([0xc0000009, 0xc000000a]);\n\nconst BLOCKED_IPV4_RANGES = compileIpv4Ranges(BLOCKED_IPV4);\nconst BLOCKED_IPV6_RANGES = compileIpv6Ranges(BLOCKED_IPV6);\n\nexport function isPrivateOrReservedHost(hostname: string): boolean {\n const host = stripBrackets(hostname).toLowerCase().replace(/\\.+$/, \"\");\n\n if (host === \"localhost\" || host.endsWith(\".localhost\")) return true;\n\n const ipv4 = parseIpv4(host);\n if (ipv4 !== null) return isBlockedIpv4(ipv4);\n\n const ipv6 = parseIpv6(host);\n if (ipv6 !== null) return isBlockedIpv6(ipv6);\n\n return false;\n}\n\nfunction stripBrackets(hostname: string): string {\n return hostname.startsWith(\"[\") && hostname.endsWith(\"]\") ? hostname.slice(1, -1) : hostname;\n}\n\nfunction isBlockedIpv4(value: number): boolean {\n if (GLOBALLY_REACHABLE_IPV4.has(value)) return false;\n for (const [network, prefix] of BLOCKED_IPV4_RANGES) {\n if (isInIpv4Range(value, network, prefix)) return true;\n }\n return false;\n}\n\nfunction isBlockedIpv6(value: bigint): boolean {\n for (const [network, prefix] of BLOCKED_IPV6_RANGES) {\n if (isInIpv6Range(value, network, prefix)) return true;\n }\n return false;\n}\n\nfunction isInIpv4Range(value: number, network: number, prefix: number): boolean {\n if (prefix === 0) return true;\n const shift = 32 - prefix;\n return value >>> shift === network >>> shift;\n}\n\nfunction isInIpv6Range(value: bigint, network: bigint, prefix: number): boolean {\n if (prefix === 0) return true;\n const shift = BigInt(128 - prefix);\n return value >> shift === network >> shift;\n}\n\n/** Parse a dotted quad into its unsigned 32-bit value, or null when it is not one. */\nfunction parseIpv4(value: string): number | null {\n const parts = value.split(\".\");\n if (parts.length !== 4) return null;\n let result = 0;\n for (const part of parts) {\n if (!/^\\d{1,3}$/.test(part)) return null;\n const octet = Number(part);\n if (octet > 255) return null;\n result = result * 256 + octet;\n }\n return result;\n}\n\n/** Parse an IPv6 address (with optional embedded IPv4) into its 128-bit value. */\nfunction parseIpv6(value: string): bigint | null {\n // Zone identifiers (`fe80::1%eth0`) are invalid inside URLs; strip the zone so a\n // caller passing one directly still gets the address classified.\n const zone = value.indexOf(\"%\");\n const address = zone === -1 ? value : value.slice(0, zone);\n\n let text = address;\n const embedded = /(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(address);\n if (embedded !== null) {\n const ipv4 = parseIpv4(embedded[1] ?? \"\");\n if (ipv4 === null) return null;\n const groups = `${(ipv4 >>> 16).toString(16)}:${(ipv4 & 0xffff).toString(16)}`;\n text = `${address.slice(0, embedded.index)}${groups}`;\n }\n\n const halves = text.split(\"::\");\n if (halves.length > 2) return null;\n if (halves.length === 1) {\n const groups = text.split(\":\");\n return groups.length === 8 ? foldHexGroups(groups) : null;\n }\n\n const [head = \"\", tail = \"\"] = halves;\n const leading = head === \"\" ? [] : head.split(\":\");\n const trailing = tail === \"\" ? [] : tail.split(\":\");\n const missing = 8 - leading.length - trailing.length;\n if (missing < 1) return null;\n const foldedHead = foldHexGroups(leading);\n const foldedTail = foldHexGroups(trailing);\n if (foldedHead === null || foldedTail === null) return null;\n return (foldedHead << BigInt(16 * (missing + trailing.length))) | foldedTail;\n}\n\nfunction foldHexGroups(groups: readonly string[]): bigint | null {\n let value = 0n;\n for (const group of groups) {\n if (!/^[0-9a-f]{1,4}$/.test(group)) return null;\n value = (value << 16n) | BigInt(parseInt(group, 16));\n }\n return value;\n}\n\nfunction compileIpv4Ranges(\n ranges: readonly AddressRange[],\n): ReadonlyArray<readonly [number, number]> {\n return ranges.map(([network, prefix]) => {\n const value = parseIpv4(network);\n if (value === null) throw new TypeError(`Invalid IPv4 range: ${network}/${prefix}`);\n return [value, prefix] as const;\n });\n}\n\nfunction compileIpv6Ranges(\n ranges: readonly AddressRange[],\n): ReadonlyArray<readonly [bigint, number]> {\n return ranges.map(([network, prefix]) => {\n const value = parseIpv6(network);\n if (value === null) throw new TypeError(`Invalid IPv6 range: ${network}/${prefix}`);\n return [value, prefix] as const;\n });\n}\n","/**\n * Shared navigation policy checks.\n *\n * Used by the tool layer (tools.ts) to reject a navigation before it is dispatched and\n * by the worker's route handler (automation-worker.ts) to re-check every top-level\n * navigation request, so a redirect cannot reach a destination the policy forbids.\n */\nimport type { Route } from \"playwright-core\";\nimport type { BrowserNavigationPolicy } from \"../types\";\nimport { isPrivateOrReservedHost } from \"./ip-check\";\n\nexport function isNavigationAllowed(value: string, policy: BrowserNavigationPolicy): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (url.username.length > 0 || url.password.length > 0) return false;\n\n // Private and reserved literal destinations stay blocked under every policy mode.\n if (isPrivateOrReservedHost(url.hostname)) return false;\n\n return policy.mode === \"allow-all-http\" || policy.origins.includes(url.origin);\n}\n\n/**\n * Abort top-level navigation requests that the policy rejects, including each redirect\n * hop Chromium follows after an allowed initial navigation.\n */\nexport async function enforceNavigationPolicy(\n route: Route,\n policy: BrowserNavigationPolicy,\n): Promise<void> {\n const request = route.request();\n const frame = request.frame();\n if (\n request.isNavigationRequest() &&\n frame === frame.page().mainFrame() &&\n !isNavigationAllowed(request.url(), policy)\n ) {\n await route.abort(\"blockedbyclient\");\n return;\n }\n await route.continue();\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNavigationAllowed
|
|
3
|
+
} from "./chunk-XE5WKN5F.js";
|
|
4
|
+
|
|
1
5
|
// src/docker-browser-client.ts
|
|
2
6
|
import { fileURLToPath } from "url";
|
|
3
7
|
|
|
@@ -2439,15 +2443,6 @@ function assertNavigationAllowed(value, policy) {
|
|
|
2439
2443
|
throw new BrowserError(`Browser navigation is blocked: ${value}`, "navigation_blocked");
|
|
2440
2444
|
}
|
|
2441
2445
|
}
|
|
2442
|
-
function isNavigationAllowed(value, policy) {
|
|
2443
|
-
let url;
|
|
2444
|
-
try {
|
|
2445
|
-
url = parseHttpUrl(value);
|
|
2446
|
-
} catch {
|
|
2447
|
-
return false;
|
|
2448
|
-
}
|
|
2449
|
-
return policy.mode === "allow-all-http" || policy.origins.includes(url.origin);
|
|
2450
|
-
}
|
|
2451
2446
|
function parseHttpUrl(value) {
|
|
2452
2447
|
const url = new URL(value);
|
|
2453
2448
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|