mirror of
https://github.com/germondai/trawl.git
synced 2026-08-17 12:11:23 +02:00
Merge pull request #43 from germondai/40-bug-authenticated-proxy-fails-on-firefoxcamoufox-credentials-embedded-in-proxyserver-are-ignored-tier-34-ns_error_proxy_connection_refused
fix(proxy): support authenticated browser proxies
This commit is contained in:
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Reject non-object request bodies and missing, non-string, or blank `url` values with HTTP 400 before scraper-tier execution (#34).
|
||||
- Recover stalled browser checkouts and bound browser close/launch operations so wedged Firefox processes cannot silently exhaust the pool (#36, #37).
|
||||
- Report HTTP 503 from `/health` whenever the browser pool has no live capacity.
|
||||
- Pass authenticated proxy credentials to Firefox separately from the proxy server URL in Tier 3 and Tier 4 (#40).
|
||||
|
||||
## [1.2.0] - 2026-07-26
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ export {
|
||||
} from "./persistentContextCache"
|
||||
export type { BrowserHandle } from "./pool"
|
||||
export { BrowserPool, newFreshContext, PoolExhaustedError } from "./pool"
|
||||
export { type PlaywrightProxy, toPlaywrightProxy } from "./proxy"
|
||||
export { SessionCache } from "./session"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BrowserHandle, PoolBrowser, PoolStats } from "@trawl/types"
|
||||
import { Camoufox } from "camoufox-js"
|
||||
import { FINGERPRINT_POOL } from "./fingerprint"
|
||||
import { toPlaywrightProxy } from "./proxy"
|
||||
|
||||
// camoufox-js wraps Playwright but doesn't re-export Browser/BrowserContext types.
|
||||
// The pool accepts any structurally-compatible browser (Playwright OR patchright) —
|
||||
@@ -589,7 +590,7 @@ export class BrowserPool {
|
||||
export const newFreshContext = async (browser: any, options?: { proxy?: string }): Promise<any> => {
|
||||
const context = await browser.newContext({
|
||||
viewport: null,
|
||||
...(options?.proxy ? { proxy: { server: options.proxy } } : {}),
|
||||
...(options?.proxy ? { proxy: toPlaywrightProxy(options.proxy) } : {}),
|
||||
})
|
||||
await context.addInitScript(() => {
|
||||
window.onerror = () => true
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface PlaywrightProxy {
|
||||
server: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
const decodeCredential = (value: string): string => {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export const toPlaywrightProxy = (proxyUrl: string): PlaywrightProxy => {
|
||||
const url = new URL(proxyUrl)
|
||||
const username = url.username ? decodeCredential(url.username) : undefined
|
||||
const password = url.password ? decodeCredential(url.password) : undefined
|
||||
return {
|
||||
server: `${url.protocol}//${url.host}`,
|
||||
...(username ? { username } : {}),
|
||||
...(password ? { password } : {}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { newFreshContext } from "../src/pool"
|
||||
import { toPlaywrightProxy } from "../src/proxy"
|
||||
|
||||
describe("newFreshContext", () => {
|
||||
test("passes authenticated proxy credentials separately from the server URL", async () => {
|
||||
let receivedOptions: unknown
|
||||
const context = { addInitScript: async () => {} }
|
||||
const browser = {
|
||||
newContext: async (options: unknown) => {
|
||||
receivedOptions = options
|
||||
return context
|
||||
},
|
||||
}
|
||||
|
||||
expect(await newFreshContext(browser, { proxy: "http://user:p%40ss@proxy.example.com:8080" })).toBe(context)
|
||||
expect(receivedOptions).toMatchObject({
|
||||
proxy: {
|
||||
server: "http://proxy.example.com:8080",
|
||||
username: "user",
|
||||
password: "p@ss",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps unauthenticated proxy URLs as server-only options", () => {
|
||||
expect(toPlaywrightProxy("https://proxy.example.com:8443")).toEqual({
|
||||
server: "https://proxy.example.com:8443",
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves SOCKS5 and IPv6 endpoints while decoding credentials", () => {
|
||||
expect(toPlaywrightProxy("socks5://user%40org:p%3Aass@[2001:db8::1]:1080")).toEqual({
|
||||
server: "socks5://[2001:db8::1]:1080",
|
||||
username: "user@org",
|
||||
password: "p:ass",
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { BrowserHandle } from "@trawl/browser"
|
||||
import { FINGERPRINT } from "@trawl/browser"
|
||||
import { FINGERPRINT, newFreshContext } from "@trawl/browser"
|
||||
import type { Cookie, TierResult } from "@trawl/types"
|
||||
import { solvePageCaptchas } from "../solvers"
|
||||
import { waitForAkamaiResolution } from "../utils/akamaiWait"
|
||||
@@ -47,32 +47,11 @@ export async function runTier4(
|
||||
// Proxies must be set at context creation time in Playwright — they cannot be
|
||||
// applied per-request. We create a fresh context here and close it when done,
|
||||
// leaving the pool's shared context untouched.
|
||||
const state: { proxyContext?: Awaited<ReturnType<typeof handle.browser.newContext>> } = {}
|
||||
const state: { proxyContext?: Awaited<ReturnType<typeof newFreshContext>> } = {}
|
||||
|
||||
try {
|
||||
// Camoufox handles fingerprinting at the C++ level — only the proxy needs to
|
||||
// be set at context creation (Playwright requires proxy at context init time).
|
||||
const proxyContext = await handle.browser.newContext({
|
||||
proxy: { server: proxyUrl },
|
||||
viewport: null,
|
||||
})
|
||||
const proxyContext = await newFreshContext(handle.browser, { proxy: proxyUrl })
|
||||
state.proxyContext = proxyContext
|
||||
await proxyContext.addInitScript(() => {
|
||||
window.onerror = () => true
|
||||
window.addEventListener(
|
||||
"unhandledrejection",
|
||||
(e: PromiseRejectionEvent) => {
|
||||
e.preventDefault()
|
||||
},
|
||||
true,
|
||||
)
|
||||
const _orig = Element.prototype.attachShadow
|
||||
Element.prototype.attachShadow = function (init: ShadowRootInit) {
|
||||
const r = _orig.call(this, init)
|
||||
Object.defineProperty(this, "shadowRootUnl", { configurable: true, value: r })
|
||||
return r
|
||||
}
|
||||
})
|
||||
|
||||
const page = await proxyContext.newPage()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user