@open-mercato/web-research-browser 0.9.1-develop.7547.1.ade91a3f6b → 0.9.1-develop.7552.1.4acf199f7a
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/.turbo/turbo-build.log +1 -1
- package/dist/dispatch.js +61 -14
- package/dist/dispatch.js.map +2 -2
- package/dist/egressProxy.js +291 -0
- package/dist/egressProxy.js.map +7 -0
- package/dist/playwright.js +9 -7
- package/dist/playwright.js.map +2 -2
- package/package.json +3 -3
- package/src/__tests__/egressProxy.test.ts +481 -0
- package/src/__tests__/launchWiring.test.ts +222 -0
- package/src/__tests__/navigationGuard.test.ts +200 -0
- package/src/dispatch.ts +79 -15
- package/src/egressProxy.ts +345 -0
- package/src/playwright.ts +42 -17
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:web-research-browser] found
|
|
1
|
+
[build:web-research-browser] found 8 entry points
|
|
2
2
|
[build:web-research-browser] built successfully
|
package/dist/dispatch.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { assertPublicUrl } from "@open-mercato/web-research";
|
|
2
|
+
import { startEgressProxy } from "./egressProxy.js";
|
|
2
3
|
import {
|
|
3
4
|
DEFAULT_USER_AGENT,
|
|
5
|
+
RESOLVER_LOCKDOWN_ARGS,
|
|
4
6
|
isSandboxDisabled,
|
|
5
7
|
resolveLaunchArgs,
|
|
6
8
|
importChromium,
|
|
@@ -9,33 +11,64 @@ import {
|
|
|
9
11
|
} from "./playwright.js";
|
|
10
12
|
import { SidecarError } from "./protocol.js";
|
|
11
13
|
const LEASE_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
14
|
+
const FILTER_SUBRESOURCES_ENV = "OM_WEB_RESEARCH_BROWSER_FILTER_SUBRESOURCES";
|
|
12
15
|
const DEFAULT_GOTO_TIMEOUT_MS = 3e4;
|
|
13
16
|
const MAX_GOTO_TIMEOUT_MS = 12e4;
|
|
14
17
|
function createState(overrides = {}) {
|
|
15
18
|
return {
|
|
16
19
|
browser: null,
|
|
17
20
|
leases: /* @__PURE__ */ new Map(),
|
|
18
|
-
filterSubresources:
|
|
21
|
+
filterSubresources: true,
|
|
19
22
|
warn: (message) => process.stderr.write(`om-web-research-sidecar: ${message}
|
|
20
23
|
`),
|
|
21
24
|
now: () => Date.now(),
|
|
22
25
|
...overrides
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
function lookupOption(state) {
|
|
29
|
+
return state.lookup ? { lookup: state.lookup } : {};
|
|
30
|
+
}
|
|
31
|
+
async function launchBrowser(state) {
|
|
32
|
+
const browserType = await (state.loadBrowserType ?? importChromium)();
|
|
28
33
|
if (isSandboxDisabled()) {
|
|
29
34
|
state.warn(
|
|
30
35
|
"SECURITY: Chromium sandbox is disabled by OM_WEB_RESEARCH_BROWSER_NO_SANDBOX=1 \u2014 a renderer compromise reaches this host"
|
|
31
36
|
);
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (process.env[FILTER_SUBRESOURCES_ENV] !== void 0) {
|
|
39
|
+
state.warn(`subresource filtering is now always on; ${FILTER_SUBRESOURCES_ENV} is ignored`);
|
|
40
|
+
}
|
|
41
|
+
const proxy = await startEgressProxy({
|
|
42
|
+
...lookupOption(state),
|
|
43
|
+
...state.connect ? { connect: state.connect } : {}
|
|
44
|
+
});
|
|
45
|
+
let browser;
|
|
46
|
+
try {
|
|
47
|
+
browser = await launchChromium(
|
|
48
|
+
browserType,
|
|
49
|
+
{
|
|
50
|
+
headless: true,
|
|
51
|
+
args: [...resolveLaunchArgs(), ...RESOLVER_LOCKDOWN_ARGS],
|
|
52
|
+
proxy: { server: proxy.url, bypass: "<-loopback>" }
|
|
53
|
+
},
|
|
54
|
+
{ onProgress: state.warn, ...state.installRunner ? { install: state.installRunner } : {} }
|
|
55
|
+
);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
await proxy.close();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
state.egressProxy = proxy;
|
|
61
|
+
state.browser = browser;
|
|
62
|
+
return browser;
|
|
63
|
+
}
|
|
64
|
+
async function ensureBrowser(state) {
|
|
65
|
+
if (state.browser) return state.browser;
|
|
66
|
+
if (!state.launching) {
|
|
67
|
+
state.launching = launchBrowser(state).finally(() => {
|
|
68
|
+
state.launching = null;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return state.launching;
|
|
39
72
|
}
|
|
40
73
|
async function openLease(state, leaseId, userAgent) {
|
|
41
74
|
await reapIdleLeases(state);
|
|
@@ -45,8 +78,8 @@ async function openLease(state, leaseId, userAgent) {
|
|
|
45
78
|
return existing;
|
|
46
79
|
}
|
|
47
80
|
const browser = await ensureBrowser(state);
|
|
48
|
-
const context = await browser.newContext({ userAgent, locale: "en-US" });
|
|
49
|
-
await installNavigationGuard(context, {
|
|
81
|
+
const context = await browser.newContext({ userAgent, locale: "en-US", serviceWorkers: "block" });
|
|
82
|
+
await installNavigationGuard(context, { warn: state.warn, ...lookupOption(state) });
|
|
50
83
|
const page = await context.newPage();
|
|
51
84
|
const lease = { context, page, touchedAt: state.now() };
|
|
52
85
|
state.leases.set(leaseId, lease);
|
|
@@ -70,15 +103,19 @@ async function closeLease(state, leaseId) {
|
|
|
70
103
|
}
|
|
71
104
|
}
|
|
72
105
|
async function teardown(state) {
|
|
106
|
+
if (state.launching) await state.launching.catch(() => null);
|
|
73
107
|
for (const leaseId of [...state.leases.keys()]) await closeLease(state, leaseId);
|
|
74
108
|
const browser = state.browser;
|
|
109
|
+
const proxy = state.egressProxy;
|
|
75
110
|
state.browser = null;
|
|
111
|
+
state.egressProxy = null;
|
|
76
112
|
if (browser) {
|
|
77
113
|
try {
|
|
78
114
|
await browser.close();
|
|
79
115
|
} catch {
|
|
80
116
|
}
|
|
81
117
|
}
|
|
118
|
+
if (proxy) await proxy.close();
|
|
82
119
|
}
|
|
83
120
|
function stringParam(request, key) {
|
|
84
121
|
const value = request.params?.[key];
|
|
@@ -95,7 +132,7 @@ async function dispatchInner(state, request) {
|
|
|
95
132
|
const url = stringParam(request, "url");
|
|
96
133
|
const leaseId = stringParam(request, "leaseId");
|
|
97
134
|
try {
|
|
98
|
-
await assertPublicUrl(url, "browser render", { failClosed: true });
|
|
135
|
+
await assertPublicUrl(url, "browser render", { failClosed: true, ...lookupOption(state) });
|
|
99
136
|
} catch (error) {
|
|
100
137
|
return {
|
|
101
138
|
id: request.id,
|
|
@@ -119,8 +156,18 @@ async function dispatchInner(state, request) {
|
|
|
119
156
|
error: { message: error instanceof Error ? error.message : String(error), kind: "navigation" }
|
|
120
157
|
};
|
|
121
158
|
}
|
|
159
|
+
const finalUrl = lease.page.url();
|
|
160
|
+
try {
|
|
161
|
+
await assertPublicUrl(finalUrl, "browser final URL", { failClosed: true, ...lookupOption(state) });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return {
|
|
164
|
+
id: request.id,
|
|
165
|
+
ok: false,
|
|
166
|
+
error: { message: error instanceof Error ? error.message : String(error), kind: "navigation" }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
122
169
|
const html = await lease.page.content();
|
|
123
|
-
return { id: request.id, ok: true, result: { url:
|
|
170
|
+
return { id: request.id, ok: true, result: { url: finalUrl, html } };
|
|
124
171
|
}
|
|
125
172
|
case "release":
|
|
126
173
|
await closeLease(state, stringParam(request, "leaseId"));
|
package/dist/dispatch.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/dispatch.ts"],
|
|
4
|
-
"sourcesContent": ["import { assertPublicUrl } from '@open-mercato/web-research'\nimport {\n DEFAULT_USER_AGENT,\n isSandboxDisabled,\n resolveLaunchArgs,\n importChromium,\n installNavigationGuard,\n launchChromium,\n type InstallRunner,\n type BrowserHandle,\n type ContextHandle,\n type PageHandle,\n} from './playwright'\nimport { SidecarError, type SidecarReply, type SidecarRequest } from './protocol'\n\nexport type Lease = { context: ContextHandle; page: PageHandle; touchedAt: number }\n\n/**\n * How long an untouched lease may live.\n *\n * `release` normally closes a context, but the caller can die between `render`\n * and `release` \u2014 a killed parent, a dropped socket \u2014 and every orphan is a live\n * BrowserContext holding memory for as long as the sidecar runs.\n */\nexport const LEASE_IDLE_TIMEOUT_MS = 5 * 60_000\n\nexport type SidecarState = {\n browser: BrowserHandle | null\n leases: Map<string, Lease>\n filterSubresources: boolean\n warn: (message: string) => void\n /** Injectable so a lease-expiry test does not have to wait five minutes. */\n now: () => number\n /** Injectable so tests never shell out to `npx playwright install`. */\n installRunner?: InstallRunner\n}\n\nexport const DEFAULT_GOTO_TIMEOUT_MS = 30_000\nconst MAX_GOTO_TIMEOUT_MS = 120_000\n\nexport function createState(overrides: Partial<SidecarState> = {}): SidecarState {\n return {\n browser: null,\n leases: new Map(),\n filterSubresources:
|
|
5
|
-
"mappings": "AAAA,SAAS,
|
|
4
|
+
"sourcesContent": ["import { assertPublicUrl, type LookupFn } from '@open-mercato/web-research'\nimport { startEgressProxy, type EgressConnect, type EgressProxy } from './egressProxy'\nimport {\n DEFAULT_USER_AGENT,\n RESOLVER_LOCKDOWN_ARGS,\n isSandboxDisabled,\n resolveLaunchArgs,\n importChromium,\n installNavigationGuard,\n launchChromium,\n type InstallRunner,\n type BrowserHandle,\n type BrowserType,\n type ContextHandle,\n type PageHandle,\n} from './playwright'\nimport { SidecarError, type SidecarReply, type SidecarRequest } from './protocol'\n\nexport type Lease = { context: ContextHandle; page: PageHandle; touchedAt: number }\n\n/**\n * How long an untouched lease may live.\n *\n * `release` normally closes a context, but the caller can die between `render`\n * and `release` \u2014 a killed parent, a dropped socket \u2014 and every orphan is a live\n * BrowserContext holding memory for as long as the sidecar runs.\n */\nexport const LEASE_IDLE_TIMEOUT_MS = 5 * 60_000\n\nexport type SidecarState = {\n browser: BrowserHandle | null\n leases: Map<string, Lease>\n /** @deprecated Subresource filtering is always on; this field is ignored. */\n filterSubresources: boolean\n warn: (message: string) => void\n /** Injectable so a lease-expiry test does not have to wait five minutes. */\n now: () => number\n /** Injectable so tests never shell out to `npx playwright install`. */\n installRunner?: InstallRunner\n /** Injectable DNS for the URL checks and the egress proxy, so tests never resolve real names. */\n lookup?: LookupFn\n /** Injectable dial for the egress proxy, so tests can stand in for public hosts. */\n connect?: EgressConnect\n /** Injectable so tests never import Playwright. */\n loadBrowserType?: () => Promise<BrowserType>\n egressProxy?: EgressProxy | null\n launching?: Promise<BrowserHandle> | null\n}\n\nconst FILTER_SUBRESOURCES_ENV = 'OM_WEB_RESEARCH_BROWSER_FILTER_SUBRESOURCES'\n\nexport const DEFAULT_GOTO_TIMEOUT_MS = 30_000\nconst MAX_GOTO_TIMEOUT_MS = 120_000\n\nexport function createState(overrides: Partial<SidecarState> = {}): SidecarState {\n return {\n browser: null,\n leases: new Map(),\n filterSubresources: true,\n warn: (message) => process.stderr.write(`om-web-research-sidecar: ${message}\\n`),\n now: () => Date.now(),\n ...overrides,\n }\n}\n\nfunction lookupOption(state: SidecarState): { lookup?: LookupFn } {\n return state.lookup ? { lookup: state.lookup } : {}\n}\n\nasync function launchBrowser(state: SidecarState): Promise<BrowserHandle> {\n const browserType = await (state.loadBrowserType ?? importChromium)()\n if (isSandboxDisabled()) {\n // Said once per browser, not once per render, but said: this process renders\n // pages chosen by a search engine or a model with the one mitigation\n // Chromium offers against a hostile renderer turned off.\n state.warn(\n 'SECURITY: Chromium sandbox is disabled by OM_WEB_RESEARCH_BROWSER_NO_SANDBOX=1 \u2014 a renderer compromise reaches this host',\n )\n }\n if (process.env[FILTER_SUBRESOURCES_ENV] !== undefined) {\n state.warn(`subresource filtering is now always on; ${FILTER_SUBRESOURCES_ENV} is ignored`)\n }\n const proxy = await startEgressProxy({\n ...lookupOption(state),\n ...(state.connect ? { connect: state.connect } : {}),\n })\n let browser: BrowserHandle\n try {\n browser = await launchChromium(\n browserType,\n {\n headless: true,\n args: [...resolveLaunchArgs(), ...RESOLVER_LOCKDOWN_ARGS],\n proxy: { server: proxy.url, bypass: '<-loopback>' },\n },\n { onProgress: state.warn, ...(state.installRunner ? { install: state.installRunner } : {}) },\n )\n } catch (error) {\n await proxy.close()\n throw error\n }\n state.egressProxy = proxy\n state.browser = browser\n return browser\n}\n\n/**\n * Single-flight: two leases opening at once share one launch, so neither a\n * browser nor its egress proxy is orphaned outside `teardown`.\n */\nasync function ensureBrowser(state: SidecarState): Promise<BrowserHandle> {\n if (state.browser) return state.browser\n if (!state.launching) {\n state.launching = launchBrowser(state).finally(() => {\n state.launching = null\n })\n }\n return state.launching\n}\n\n/**\n * One BrowserContext per lease. Contexts are Playwright's real isolation boundary\n * (cookies, storage, cache) and cost ~10ms, so two concurrent searches never share\n * a cookie jar the way a single shared page would.\n */\nasync function openLease(state: SidecarState, leaseId: string, userAgent: string): Promise<Lease> {\n await reapIdleLeases(state)\n const existing = state.leases.get(leaseId)\n if (existing) {\n existing.touchedAt = state.now()\n return existing\n }\n const browser = await ensureBrowser(state)\n const context = await browser.newContext({ userAgent, locale: 'en-US', serviceWorkers: 'block' })\n await installNavigationGuard(context, { warn: state.warn, ...lookupOption(state) })\n const page = (await context.newPage()) as unknown as PageHandle\n const lease: Lease = { context, page, touchedAt: state.now() }\n state.leases.set(leaseId, lease)\n return lease\n}\n\n/** Closes leases nobody released. Swept on open, so an idle sidecar stays idle. */\nexport async function reapIdleLeases(state: SidecarState): Promise<void> {\n const cutoff = state.now() - LEASE_IDLE_TIMEOUT_MS\n for (const [leaseId, lease] of [...state.leases]) {\n if (lease.touchedAt > cutoff) continue\n state.warn(`reaping lease ${leaseId} idle for over ${LEASE_IDLE_TIMEOUT_MS}ms`)\n await closeLease(state, leaseId)\n }\n}\n\nasync function closeLease(state: SidecarState, leaseId: string): Promise<void> {\n const lease = state.leases.get(leaseId)\n if (!lease) return\n state.leases.delete(leaseId)\n try {\n await lease.context.close()\n } catch {\n // A context that already died takes nothing with it.\n }\n}\n\nexport async function teardown(state: SidecarState): Promise<void> {\n if (state.launching) await state.launching.catch(() => null)\n for (const leaseId of [...state.leases.keys()]) await closeLease(state, leaseId)\n const browser = state.browser\n const proxy = state.egressProxy\n state.browser = null\n state.egressProxy = null\n if (browser) {\n try {\n await browser.close()\n } catch {\n // Best effort; the parent escalates to SIGKILL if we hang.\n }\n }\n if (proxy) await proxy.close()\n}\n\nfunction stringParam(request: SidecarRequest, key: string): string {\n const value = request.params?.[key]\n if (typeof value !== 'string' || value.length === 0) {\n throw new SidecarError(`missing \"${key}\" parameter`, 'runtime')\n }\n return value\n}\n\nasync function dispatchInner(state: SidecarState, request: SidecarRequest): Promise<SidecarReply> {\n switch (request.method) {\n case 'ping':\n return { id: request.id, ok: true, result: { ready: true } }\n\n case 'render': {\n const url = stringParam(request, 'url')\n const leaseId = stringParam(request, 'leaseId')\n // Re-checked here, in a distinct process from the caller, and BEFORE\n // Playwright is imported \u2014 a blocked URL never launches a browser.\n try {\n await assertPublicUrl(url, 'browser render', { failClosed: true, ...lookupOption(state) })\n } catch (error) {\n return {\n id: request.id,\n ok: false,\n error: { message: error instanceof Error ? error.message : String(error), kind: 'navigation' },\n }\n }\n\n const userAgent = typeof request.params?.userAgent === 'string' ? request.params.userAgent : DEFAULT_USER_AGENT\n const timeout = Math.min(\n typeof request.params?.timeoutMs === 'number' ? request.params.timeoutMs : DEFAULT_GOTO_TIMEOUT_MS,\n MAX_GOTO_TIMEOUT_MS,\n )\n const waitUntil = typeof request.params?.waitUntil === 'string' ? request.params.waitUntil : 'domcontentloaded'\n\n const lease = await openLease(state, leaseId, userAgent)\n try {\n await lease.page.goto(url, { waitUntil, timeout })\n } catch (error) {\n return {\n id: request.id,\n ok: false,\n error: { message: error instanceof Error ? error.message : String(error), kind: 'navigation' },\n }\n }\n const finalUrl = lease.page.url()\n try {\n await assertPublicUrl(finalUrl, 'browser final URL', { failClosed: true, ...lookupOption(state) })\n } catch (error) {\n return {\n id: request.id,\n ok: false,\n error: { message: error instanceof Error ? error.message : String(error), kind: 'navigation' },\n }\n }\n const html = await lease.page.content()\n return { id: request.id, ok: true, result: { url: finalUrl, html } }\n }\n\n case 'release':\n await closeLease(state, stringParam(request, 'leaseId'))\n return { id: request.id, ok: true }\n\n case 'close':\n await teardown(state)\n return { id: request.id, ok: true }\n\n default:\n return {\n id: request.id,\n ok: false,\n error: { message: `unknown method: ${request.method}`, kind: 'runtime' },\n }\n }\n}\n\nexport async function dispatch(state: SidecarState, request: SidecarRequest): Promise<SidecarReply> {\n try {\n return await dispatchInner(state, request)\n } catch (error) {\n return {\n id: request.id,\n ok: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n kind: error instanceof SidecarError ? error.kind : 'unknown',\n },\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createServer,
|
|
3
|
+
request as sendHttp
|
|
4
|
+
} from "node:http";
|
|
5
|
+
import { connect as netConnect } from "node:net";
|
|
6
|
+
import { assertPublicUrl, createPinnedLookup } from "@open-mercato/web-research";
|
|
7
|
+
const EGRESS_VET_TIMEOUT_MS = 1e4;
|
|
8
|
+
const EGRESS_DIAL_TIMEOUT_MS = 1e4;
|
|
9
|
+
const EGRESS_IDLE_TIMEOUT_MS = 12e4;
|
|
10
|
+
const AUTHORITY_PATTERN = /^(\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9._-]+):(\d{1,5})$/;
|
|
11
|
+
const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
12
|
+
"connection",
|
|
13
|
+
"keep-alive",
|
|
14
|
+
"proxy-authenticate",
|
|
15
|
+
"proxy-authorization",
|
|
16
|
+
"proxy-connection",
|
|
17
|
+
"te",
|
|
18
|
+
"trailer",
|
|
19
|
+
"transfer-encoding",
|
|
20
|
+
"upgrade",
|
|
21
|
+
"host"
|
|
22
|
+
]);
|
|
23
|
+
const STATUS_TEXT = {
|
|
24
|
+
400: "Bad Request",
|
|
25
|
+
403: "Forbidden",
|
|
26
|
+
502: "Bad Gateway",
|
|
27
|
+
504: "Gateway Timeout"
|
|
28
|
+
};
|
|
29
|
+
const pinnedConnect = ({ host, port, addresses }) => netConnect({ host, port, lookup: createPinnedLookup(addresses), autoSelectFamily: true });
|
|
30
|
+
function parseConnectAuthority(raw) {
|
|
31
|
+
const match = AUTHORITY_PATTERN.exec(raw);
|
|
32
|
+
if (!match) return null;
|
|
33
|
+
const port = Number(match[2]);
|
|
34
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
|
|
35
|
+
return { host: match[1], port };
|
|
36
|
+
}
|
|
37
|
+
function parseAbsoluteForm(raw) {
|
|
38
|
+
if (!/^http:\/\//i.test(raw)) return null;
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(raw);
|
|
41
|
+
if (url.protocol !== "http:" || url.hostname === "") return null;
|
|
42
|
+
return url;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function forwardableHeaders(headers) {
|
|
48
|
+
const listed = new Set(
|
|
49
|
+
String(headers.connection ?? "").split(",").map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0)
|
|
50
|
+
);
|
|
51
|
+
const result = {};
|
|
52
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
53
|
+
if (value === void 0) continue;
|
|
54
|
+
const key = name.toLowerCase();
|
|
55
|
+
if (HOP_BY_HOP_HEADERS.has(key) || key.startsWith("proxy-") || listed.has(key)) continue;
|
|
56
|
+
result[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function dialHost(vetted) {
|
|
61
|
+
return vetted.url.hostname.replace(/^\[|\]$/g, "");
|
|
62
|
+
}
|
|
63
|
+
function refuseSocket(socket, status) {
|
|
64
|
+
if (socket.destroyed || !socket.writable) {
|
|
65
|
+
socket.destroy();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
socket.end(
|
|
69
|
+
`HTTP/1.1 ${status} ${STATUS_TEXT[status] ?? "Error"}\r
|
|
70
|
+
Connection: close\r
|
|
71
|
+
Content-Length: 0\r
|
|
72
|
+
\r
|
|
73
|
+
`,
|
|
74
|
+
() => socket.destroy()
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
function refuseResponse(res, status, reason) {
|
|
78
|
+
if (res.headersSent || res.destroyed) {
|
|
79
|
+
res.destroy();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
res.writeHead(status, { "content-type": "text/plain; charset=utf-8", connection: "close" });
|
|
83
|
+
res.end(`om-web-research egress proxy: ${reason}
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
async function startEgressProxy(options = {}) {
|
|
87
|
+
const connect = options.connect ?? pinnedConnect;
|
|
88
|
+
const vetTimeoutMs = options.vetTimeoutMs ?? EGRESS_VET_TIMEOUT_MS;
|
|
89
|
+
const dialTimeoutMs = options.dialTimeoutMs ?? EGRESS_DIAL_TIMEOUT_MS;
|
|
90
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? EGRESS_IDLE_TIMEOUT_MS;
|
|
91
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
92
|
+
let closed = false;
|
|
93
|
+
const track = (socket) => {
|
|
94
|
+
sockets.add(socket);
|
|
95
|
+
socket.once("close", () => sockets.delete(socket));
|
|
96
|
+
};
|
|
97
|
+
const vet = (rawUrl) => new Promise((resolve) => {
|
|
98
|
+
const timer = setTimeout(() => resolve("timeout"), vetTimeoutMs);
|
|
99
|
+
assertPublicUrl(rawUrl, "browser egress", {
|
|
100
|
+
failClosed: true,
|
|
101
|
+
...options.lookup ? { lookup: options.lookup } : {}
|
|
102
|
+
}).then(
|
|
103
|
+
(vetted) => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
resolve(vetted);
|
|
106
|
+
},
|
|
107
|
+
() => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
resolve("blocked");
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
const dial = (target) => new Promise((resolve, reject) => {
|
|
114
|
+
let socket;
|
|
115
|
+
try {
|
|
116
|
+
socket = connect(target);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
reject(error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
track(socket);
|
|
122
|
+
socket.on("error", () => socket.destroy());
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
socket.destroy();
|
|
125
|
+
reject(new Error(`dial to ${target.host}:${target.port} timed out`));
|
|
126
|
+
}, dialTimeoutMs);
|
|
127
|
+
socket.once("close", () => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
reject(new Error(`dial to ${target.host}:${target.port} failed`));
|
|
130
|
+
});
|
|
131
|
+
socket.once("connect", () => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
if (closed) {
|
|
134
|
+
socket.destroy();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
resolve(socket);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const handleConnect = async (req, client, head) => {
|
|
141
|
+
track(client);
|
|
142
|
+
client.on("error", () => client.destroy());
|
|
143
|
+
let clientLeft = false;
|
|
144
|
+
const markLeft = () => {
|
|
145
|
+
clientLeft = true;
|
|
146
|
+
};
|
|
147
|
+
client.once("end", markLeft);
|
|
148
|
+
client.once("close", markLeft);
|
|
149
|
+
const authority = parseConnectAuthority(req.url ?? "");
|
|
150
|
+
if (!authority) return refuseSocket(client, 400);
|
|
151
|
+
const vetted = await vet(`http://${authority.host}:${authority.port}/`);
|
|
152
|
+
if (vetted === "timeout") return refuseSocket(client, 504);
|
|
153
|
+
if (vetted === "blocked") return refuseSocket(client, 403);
|
|
154
|
+
if (closed || clientLeft) {
|
|
155
|
+
client.destroy();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let upstream;
|
|
159
|
+
try {
|
|
160
|
+
upstream = await dial({ host: dialHost(vetted), port: authority.port, addresses: vetted.addresses });
|
|
161
|
+
} catch {
|
|
162
|
+
return refuseSocket(client, 502);
|
|
163
|
+
}
|
|
164
|
+
if (clientLeft) {
|
|
165
|
+
upstream.destroy();
|
|
166
|
+
client.destroy();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
upstream.setTimeout(idleTimeoutMs, () => {
|
|
170
|
+
upstream.destroy();
|
|
171
|
+
client.destroy();
|
|
172
|
+
});
|
|
173
|
+
upstream.once("close", () => client.destroy());
|
|
174
|
+
client.once("close", () => upstream.destroy());
|
|
175
|
+
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
176
|
+
if (head.length > 0) upstream.write(head);
|
|
177
|
+
client.pipe(upstream);
|
|
178
|
+
upstream.pipe(client);
|
|
179
|
+
};
|
|
180
|
+
const handleRequest = async (req, res) => {
|
|
181
|
+
req.on("error", () => res.destroy());
|
|
182
|
+
res.on("error", () => res.destroy());
|
|
183
|
+
const target = parseAbsoluteForm(req.url ?? "");
|
|
184
|
+
if (!target) return refuseResponse(res, 400, "only absolute-form http:// requests are proxied");
|
|
185
|
+
if (req.headers.upgrade !== void 0) return refuseResponse(res, 400, "upgrade requests must use CONNECT");
|
|
186
|
+
const vetted = await vet(target.href);
|
|
187
|
+
if (vetted === "timeout") return refuseResponse(res, 504, "destination could not be vetted in time");
|
|
188
|
+
if (vetted === "blocked") return refuseResponse(res, 403, "destination is not a public address");
|
|
189
|
+
if (closed || res.destroyed) return;
|
|
190
|
+
let upstreamSocket;
|
|
191
|
+
try {
|
|
192
|
+
upstreamSocket = await dial({
|
|
193
|
+
host: dialHost(vetted),
|
|
194
|
+
port: Number(vetted.url.port || 80),
|
|
195
|
+
addresses: vetted.addresses
|
|
196
|
+
});
|
|
197
|
+
} catch {
|
|
198
|
+
return refuseResponse(res, 502, "destination unreachable");
|
|
199
|
+
}
|
|
200
|
+
if (res.destroyed) {
|
|
201
|
+
upstreamSocket.destroy();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
upstreamSocket.setTimeout(idleTimeoutMs, () => upstreamSocket.destroy());
|
|
205
|
+
const headers = forwardableHeaders(req.headers);
|
|
206
|
+
headers.host = vetted.url.host;
|
|
207
|
+
headers.connection = "close";
|
|
208
|
+
const forwardResponse = (upstreamResponse) => {
|
|
209
|
+
upstreamResponse.on("error", () => res.destroy());
|
|
210
|
+
const status = upstreamResponse.statusCode ?? 0;
|
|
211
|
+
if (status < 200 || status > 999) {
|
|
212
|
+
upstreamResponse.destroy();
|
|
213
|
+
refuseResponse(res, 502, "upstream sent an invalid status");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
res.writeHead(status, forwardableHeaders(upstreamResponse.headers));
|
|
218
|
+
} catch {
|
|
219
|
+
upstreamResponse.destroy();
|
|
220
|
+
res.destroy();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
upstreamResponse.pipe(res);
|
|
224
|
+
};
|
|
225
|
+
let upstreamRequest;
|
|
226
|
+
try {
|
|
227
|
+
upstreamRequest = sendHttp(
|
|
228
|
+
{
|
|
229
|
+
method: req.method,
|
|
230
|
+
path: `${vetted.url.pathname}${vetted.url.search}`,
|
|
231
|
+
headers,
|
|
232
|
+
setHost: false,
|
|
233
|
+
createConnection: () => upstreamSocket
|
|
234
|
+
},
|
|
235
|
+
forwardResponse
|
|
236
|
+
);
|
|
237
|
+
} catch {
|
|
238
|
+
upstreamSocket.destroy();
|
|
239
|
+
return refuseResponse(res, 502, "upstream request could not be built");
|
|
240
|
+
}
|
|
241
|
+
upstreamRequest.on("error", () => refuseResponse(res, 502, "upstream failed"));
|
|
242
|
+
upstreamRequest.on("upgrade", (_response, socket) => {
|
|
243
|
+
socket.destroy();
|
|
244
|
+
refuseResponse(res, 502, "upstream attempted a protocol upgrade");
|
|
245
|
+
});
|
|
246
|
+
res.once("close", () => upstreamRequest.destroy());
|
|
247
|
+
req.pipe(upstreamRequest);
|
|
248
|
+
};
|
|
249
|
+
const server = createServer();
|
|
250
|
+
server.timeout = idleTimeoutMs;
|
|
251
|
+
server.on("connection", (socket) => track(socket));
|
|
252
|
+
server.on("request", (req, res) => {
|
|
253
|
+
void handleRequest(req, res).catch(() => refuseResponse(res, 502, "proxy failure"));
|
|
254
|
+
});
|
|
255
|
+
server.on("connect", (req, socket, head) => {
|
|
256
|
+
void handleConnect(req, socket, head).catch(() => socket.destroy());
|
|
257
|
+
});
|
|
258
|
+
server.on("upgrade", (_req, socket) => {
|
|
259
|
+
socket.on("error", () => socket.destroy());
|
|
260
|
+
refuseSocket(socket, 400);
|
|
261
|
+
});
|
|
262
|
+
server.on("clientError", (_error, socket) => {
|
|
263
|
+
refuseSocket(socket, 400);
|
|
264
|
+
});
|
|
265
|
+
await new Promise((resolve, reject) => {
|
|
266
|
+
server.once("error", reject);
|
|
267
|
+
server.listen(0, "127.0.0.1", () => {
|
|
268
|
+
server.removeListener("error", reject);
|
|
269
|
+
resolve();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
server.on("error", () => void 0);
|
|
273
|
+
const address = server.address();
|
|
274
|
+
if (!address || typeof address === "string") {
|
|
275
|
+
server.close();
|
|
276
|
+
throw new Error("[internal] egress proxy did not bind a TCP port");
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
280
|
+
close: () => new Promise((resolve) => {
|
|
281
|
+
closed = true;
|
|
282
|
+
server.close(() => resolve());
|
|
283
|
+
for (const socket of sockets) socket.destroy();
|
|
284
|
+
})
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
pinnedConnect,
|
|
289
|
+
startEgressProxy
|
|
290
|
+
};
|
|
291
|
+
//# sourceMappingURL=egressProxy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/egressProxy.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n createServer,\n request as sendHttp,\n type ClientRequest,\n type IncomingHttpHeaders,\n type IncomingMessage,\n type OutgoingHttpHeaders,\n type ServerResponse,\n} from 'node:http'\nimport { connect as netConnect, type Socket } from 'node:net'\nimport type { Duplex } from 'node:stream'\nimport { assertPublicUrl, createPinnedLookup, type LookupFn, type VettedUrl } from '@open-mercato/web-research'\n\nexport type EgressDialTarget = {\n readonly host: string\n readonly port: number\n /** The addresses `assertPublicUrl` vetted for this connection; the only ones a dial may use. */\n readonly addresses: readonly string[]\n}\n\nexport type EgressConnect = (target: EgressDialTarget) => Socket\n\nexport type EgressProxyOptions = {\n readonly lookup?: LookupFn\n readonly connect?: EgressConnect\n readonly vetTimeoutMs?: number\n readonly dialTimeoutMs?: number\n readonly idleTimeoutMs?: number\n}\n\nexport type EgressProxy = {\n readonly url: string\n close(): Promise<void>\n}\n\nconst EGRESS_VET_TIMEOUT_MS = 10_000\nconst EGRESS_DIAL_TIMEOUT_MS = 10_000\nconst EGRESS_IDLE_TIMEOUT_MS = 120_000\n\nconst AUTHORITY_PATTERN = /^(\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9._-]+):(\\d{1,5})$/\n\nconst HOP_BY_HOP_HEADERS = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'proxy-connection',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n 'host',\n])\n\nconst STATUS_TEXT: Record<number, string> = {\n 400: 'Bad Request',\n 403: 'Forbidden',\n 502: 'Bad Gateway',\n 504: 'Gateway Timeout',\n}\n\n/** Dials only the vetted addresses: the hostname is never handed to DNS. */\nexport const pinnedConnect: EgressConnect = ({ host, port, addresses }) =>\n netConnect({ host, port, lookup: createPinnedLookup(addresses), autoSelectFamily: true })\n\nfunction parseConnectAuthority(raw: string): { host: string; port: number } | null {\n const match = AUTHORITY_PATTERN.exec(raw)\n if (!match) return null\n const port = Number(match[2])\n if (!Number.isInteger(port) || port < 1 || port > 65535) return null\n return { host: match[1], port }\n}\n\nfunction parseAbsoluteForm(raw: string): URL | null {\n if (!/^http:\\/\\//i.test(raw)) return null\n try {\n const url = new URL(raw)\n if (url.protocol !== 'http:' || url.hostname === '') return null\n return url\n } catch {\n return null\n }\n}\n\nfunction forwardableHeaders(headers: IncomingHttpHeaders): OutgoingHttpHeaders {\n const listed = new Set(\n String(headers.connection ?? '')\n .split(',')\n .map((token) => token.trim().toLowerCase())\n .filter((token) => token.length > 0),\n )\n const result: OutgoingHttpHeaders = {}\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined) continue\n const key = name.toLowerCase()\n if (HOP_BY_HOP_HEADERS.has(key) || key.startsWith('proxy-') || listed.has(key)) continue\n result[key] = value\n }\n return result\n}\n\nfunction dialHost(vetted: VettedUrl): string {\n return vetted.url.hostname.replace(/^\\[|\\]$/g, '')\n}\n\nfunction refuseSocket(socket: Duplex, status: number): void {\n if (socket.destroyed || !socket.writable) {\n socket.destroy()\n return\n }\n socket.end(`HTTP/1.1 ${status} ${STATUS_TEXT[status] ?? 'Error'}\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n`, () =>\n socket.destroy(),\n )\n}\n\nfunction refuseResponse(res: ServerResponse, status: number, reason: string): void {\n if (res.headersSent || res.destroyed) {\n res.destroy()\n return\n }\n res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8', connection: 'close' })\n res.end(`om-web-research egress proxy: ${reason}\\n`)\n}\n\n/**\n * Loopback HTTP proxy that every connection of the sidecar's Chromium goes through.\n *\n * Each CONNECT tunnel and each absolute-form request is vetted with\n * `assertPublicUrl` on its own, then dialled only to the addresses that check\n * returned, so redirect hops, WebSockets, workers and a host whose DNS answer\n * changes between connections are all held to the same rule. Nothing is cached.\n */\nexport async function startEgressProxy(options: EgressProxyOptions = {}): Promise<EgressProxy> {\n const connect = options.connect ?? pinnedConnect\n const vetTimeoutMs = options.vetTimeoutMs ?? EGRESS_VET_TIMEOUT_MS\n const dialTimeoutMs = options.dialTimeoutMs ?? EGRESS_DIAL_TIMEOUT_MS\n const idleTimeoutMs = options.idleTimeoutMs ?? EGRESS_IDLE_TIMEOUT_MS\n const sockets = new Set<Duplex>()\n let closed = false\n\n const track = (socket: Duplex): void => {\n sockets.add(socket)\n socket.once('close', () => sockets.delete(socket))\n }\n\n const vet = (rawUrl: string): Promise<VettedUrl | 'blocked' | 'timeout'> =>\n new Promise((resolve) => {\n const timer = setTimeout(() => resolve('timeout'), vetTimeoutMs)\n assertPublicUrl(rawUrl, 'browser egress', {\n failClosed: true,\n ...(options.lookup ? { lookup: options.lookup } : {}),\n }).then(\n (vetted) => {\n clearTimeout(timer)\n resolve(vetted)\n },\n () => {\n clearTimeout(timer)\n resolve('blocked')\n },\n )\n })\n\n const dial = (target: EgressDialTarget): Promise<Socket> =>\n new Promise((resolve, reject) => {\n let socket: Socket\n try {\n socket = connect(target)\n } catch (error) {\n reject(error)\n return\n }\n track(socket)\n socket.on('error', () => socket.destroy())\n const timer = setTimeout(() => {\n socket.destroy()\n reject(new Error(`dial to ${target.host}:${target.port} timed out`))\n }, dialTimeoutMs)\n socket.once('close', () => {\n clearTimeout(timer)\n reject(new Error(`dial to ${target.host}:${target.port} failed`))\n })\n socket.once('connect', () => {\n clearTimeout(timer)\n if (closed) {\n socket.destroy()\n return\n }\n resolve(socket)\n })\n })\n\n const handleConnect = async (req: IncomingMessage, client: Duplex, head: Buffer): Promise<void> => {\n track(client)\n client.on('error', () => client.destroy())\n let clientLeft = false\n const markLeft = (): void => {\n clientLeft = true\n }\n client.once('end', markLeft)\n client.once('close', markLeft)\n const authority = parseConnectAuthority(req.url ?? '')\n if (!authority) return refuseSocket(client, 400)\n const vetted = await vet(`http://${authority.host}:${authority.port}/`)\n if (vetted === 'timeout') return refuseSocket(client, 504)\n if (vetted === 'blocked') return refuseSocket(client, 403)\n if (closed || clientLeft) {\n client.destroy()\n return\n }\n let upstream: Socket\n try {\n upstream = await dial({ host: dialHost(vetted), port: authority.port, addresses: vetted.addresses })\n } catch {\n return refuseSocket(client, 502)\n }\n if (clientLeft) {\n upstream.destroy()\n client.destroy()\n return\n }\n upstream.setTimeout(idleTimeoutMs, () => {\n upstream.destroy()\n client.destroy()\n })\n upstream.once('close', () => client.destroy())\n client.once('close', () => upstream.destroy())\n client.write('HTTP/1.1 200 Connection Established\\r\\n\\r\\n')\n if (head.length > 0) upstream.write(head)\n client.pipe(upstream)\n upstream.pipe(client)\n }\n\n const handleRequest = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n req.on('error', () => res.destroy())\n res.on('error', () => res.destroy())\n const target = parseAbsoluteForm(req.url ?? '')\n if (!target) return refuseResponse(res, 400, 'only absolute-form http:// requests are proxied')\n if (req.headers.upgrade !== undefined) return refuseResponse(res, 400, 'upgrade requests must use CONNECT')\n const vetted = await vet(target.href)\n if (vetted === 'timeout') return refuseResponse(res, 504, 'destination could not be vetted in time')\n if (vetted === 'blocked') return refuseResponse(res, 403, 'destination is not a public address')\n if (closed || res.destroyed) return\n let upstreamSocket: Socket\n try {\n upstreamSocket = await dial({\n host: dialHost(vetted),\n port: Number(vetted.url.port || 80),\n addresses: vetted.addresses,\n })\n } catch {\n return refuseResponse(res, 502, 'destination unreachable')\n }\n if (res.destroyed) {\n upstreamSocket.destroy()\n return\n }\n upstreamSocket.setTimeout(idleTimeoutMs, () => upstreamSocket.destroy())\n\n const headers = forwardableHeaders(req.headers)\n headers.host = vetted.url.host\n headers.connection = 'close'\n const forwardResponse = (upstreamResponse: IncomingMessage): void => {\n upstreamResponse.on('error', () => res.destroy())\n const status = upstreamResponse.statusCode ?? 0\n if (status < 200 || status > 999) {\n upstreamResponse.destroy()\n refuseResponse(res, 502, 'upstream sent an invalid status')\n return\n }\n try {\n res.writeHead(status, forwardableHeaders(upstreamResponse.headers))\n } catch {\n upstreamResponse.destroy()\n res.destroy()\n return\n }\n upstreamResponse.pipe(res)\n }\n let upstreamRequest: ClientRequest\n try {\n upstreamRequest = sendHttp(\n {\n method: req.method,\n path: `${vetted.url.pathname}${vetted.url.search}`,\n headers,\n setHost: false,\n createConnection: () => upstreamSocket,\n },\n forwardResponse,\n )\n } catch {\n upstreamSocket.destroy()\n return refuseResponse(res, 502, 'upstream request could not be built')\n }\n upstreamRequest.on('error', () => refuseResponse(res, 502, 'upstream failed'))\n upstreamRequest.on('upgrade', (_response, socket) => {\n socket.destroy()\n refuseResponse(res, 502, 'upstream attempted a protocol upgrade')\n })\n res.once('close', () => upstreamRequest.destroy())\n req.pipe(upstreamRequest)\n }\n\n const server = createServer()\n server.timeout = idleTimeoutMs\n server.on('connection', (socket) => track(socket))\n server.on('request', (req, res) => {\n void handleRequest(req, res).catch(() => refuseResponse(res, 502, 'proxy failure'))\n })\n server.on('connect', (req, socket, head) => {\n void handleConnect(req, socket, head).catch(() => socket.destroy())\n })\n server.on('upgrade', (_req, socket) => {\n socket.on('error', () => socket.destroy())\n refuseSocket(socket, 400)\n })\n server.on('clientError', (_error, socket) => {\n refuseSocket(socket, 400)\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', () => {\n server.removeListener('error', reject)\n resolve()\n })\n })\n server.on('error', () => undefined)\n const address = server.address()\n if (!address || typeof address === 'string') {\n server.close()\n throw new Error('[internal] egress proxy did not bind a TCP port')\n }\n\n return {\n url: `http://127.0.0.1:${address.port}`,\n close: () =>\n new Promise<void>((resolve) => {\n closed = true\n server.close(() => resolve())\n for (const socket of sockets) socket.destroy()\n }),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/playwright.js
CHANGED
|
@@ -75,10 +75,8 @@ async function launchChromium(browserType, options, deps = {}) {
|
|
|
75
75
|
const MAX_VERDICT_CACHE = 2048;
|
|
76
76
|
async function installNavigationGuard(context, options = {}) {
|
|
77
77
|
if (typeof context.route !== "function") {
|
|
78
|
-
|
|
79
|
-
return;
|
|
78
|
+
throw new SidecarError("context.route() is unavailable, so the request guard cannot be installed", "init");
|
|
80
79
|
}
|
|
81
|
-
const filterSubresources = options.filterSubresources ?? false;
|
|
82
80
|
const verdicts = /* @__PURE__ */ new Map();
|
|
83
81
|
const remember = (host, blocked) => {
|
|
84
82
|
if (verdicts.size >= MAX_VERDICT_CACHE) {
|
|
@@ -90,20 +88,20 @@ async function installNavigationGuard(context, options = {}) {
|
|
|
90
88
|
await context.route("**/*", async (route) => {
|
|
91
89
|
const request = route.request();
|
|
92
90
|
const isNavigation = request.isNavigationRequest();
|
|
93
|
-
if (!isNavigation && !filterSubresources) return route.continue();
|
|
94
91
|
const url = request.url();
|
|
95
92
|
let host;
|
|
96
93
|
try {
|
|
97
94
|
host = new URL(url).host;
|
|
98
95
|
} catch {
|
|
99
|
-
return
|
|
96
|
+
return route.abort("blockedbyclient");
|
|
100
97
|
}
|
|
101
98
|
const cached = isNavigation ? void 0 : verdicts.get(host);
|
|
102
99
|
if (cached === true) return route.abort("blockedbyclient");
|
|
103
100
|
if (cached === false) return route.continue();
|
|
104
101
|
try {
|
|
105
102
|
await assertPublicUrl(url, isNavigation ? "browser navigation" : "browser subresource", {
|
|
106
|
-
failClosed: true
|
|
103
|
+
failClosed: true,
|
|
104
|
+
...options.lookup ? { lookup: options.lookup } : {}
|
|
107
105
|
});
|
|
108
106
|
remember(host, false);
|
|
109
107
|
} catch {
|
|
@@ -113,9 +111,12 @@ async function installNavigationGuard(context, options = {}) {
|
|
|
113
111
|
return route.continue();
|
|
114
112
|
});
|
|
115
113
|
}
|
|
114
|
+
const RESOLVER_LOCKDOWN_ARGS = ["--host-resolver-rules=MAP * ~NOTFOUND , EXCLUDE 127.0.0.1"];
|
|
116
115
|
const LAUNCH_ARGS = [
|
|
117
116
|
"--disable-blink-features=AutomationControlled",
|
|
118
|
-
"--disable-dev-shm-usage"
|
|
117
|
+
"--disable-dev-shm-usage",
|
|
118
|
+
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp",
|
|
119
|
+
"--disable-quic"
|
|
119
120
|
];
|
|
120
121
|
const SANDBOX_OPT_OUT_ENV = "OM_WEB_RESEARCH_BROWSER_NO_SANDBOX";
|
|
121
122
|
function resolveLaunchArgs(env = process.env) {
|
|
@@ -132,6 +133,7 @@ const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Appl
|
|
|
132
133
|
export {
|
|
133
134
|
DEFAULT_USER_AGENT,
|
|
134
135
|
LAUNCH_ARGS,
|
|
136
|
+
RESOLVER_LOCKDOWN_ARGS,
|
|
135
137
|
SANDBOX_OPT_OUT_ENV,
|
|
136
138
|
importChromium,
|
|
137
139
|
installNavigationGuard,
|