mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: support proxy dict with bypass field (#24)
The `proxy` parameter now accepts a Playwright proxy dict
({server, bypass, username, password}) in addition to URL strings.
Dict proxies are passed directly to Playwright, enabling bypass
lists and other advanced proxy options.
- Add ProxySettings TypedDict for Python type safety
- Extract server URL from dict proxies for geoip resolution
- Handle dict proxy args/auth in Puppeteer wrapper
- Strip inline credentials from dict proxy server URL in Puppeteer
- Fix JS launchContext() double-setting timezone (binary flag + context)
- Remove unnecessary non-null assertions in TS geoip helpers
- Use nullish coalescing for password fallbacks
- Add unit tests for geoip with dict proxy input
This commit is contained in:
+21
-1
@@ -67,6 +67,11 @@ const browser = await launch({
|
||||
proxy: 'http://user:pass@proxy:8080',
|
||||
});
|
||||
|
||||
// With proxy object (bypass, separate auth fields)
|
||||
const browser = await launch({
|
||||
proxy: { server: 'http://proxy:8080', bypass: '.google.com', username: 'user', password: 'pass' },
|
||||
});
|
||||
|
||||
// Headed mode (visible browser window)
|
||||
const browser = await launch({ headless: false });
|
||||
|
||||
@@ -95,7 +100,7 @@ const context = await launchContext({
|
||||
timezoneId: 'America/New_York',
|
||||
});
|
||||
|
||||
// Persistent profile — cookies/localStorage survive restarts, avoids incognito detection
|
||||
// Persistent profile — stay logged in, bypass incognito detection, load extensions
|
||||
const ctx = await launchPersistentContext({
|
||||
userDataDir: './chrome-profile',
|
||||
headless: false,
|
||||
@@ -195,6 +200,21 @@ const page = await browser.newPage();
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Site detects incognito / private browsing mode**
|
||||
|
||||
By default, `launch()` opens an incognito context. Some sites (like BrowserScan) detect this. Use `launchPersistentContext()` instead — it runs with a real user profile:
|
||||
|
||||
```javascript
|
||||
import { launchPersistentContext } from 'cloakbrowser';
|
||||
|
||||
const ctx = await launchPersistentContext({
|
||||
userDataDir: './my-profile',
|
||||
headless: false,
|
||||
});
|
||||
```
|
||||
|
||||
This also gives you cookie and localStorage persistence across sessions.
|
||||
|
||||
**reCAPTCHA v3 scores are low (0.1–0.3)**
|
||||
|
||||
Avoid `page.waitForTimeout()` — it sends CDP protocol commands that reCAPTCHA detects. Use native sleep instead:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloakbrowser",
|
||||
"version": "0.3.5",
|
||||
"version": "0.3.6",
|
||||
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
+13
-4
@@ -34,7 +34,9 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
||||
headless: options.headless ?? true,
|
||||
args,
|
||||
ignoreDefaultArgs: ["--enable-automation"],
|
||||
...(options.proxy ? { proxy: parseProxyUrl(options.proxy) } : {}),
|
||||
...(options.proxy
|
||||
? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy }
|
||||
: {}),
|
||||
...options.launchOptions,
|
||||
});
|
||||
|
||||
@@ -62,7 +64,10 @@ export async function launchContext(
|
||||
): Promise<BrowserContext> {
|
||||
// Resolve geoip BEFORE launch() to avoid double-resolution
|
||||
const resolved = await maybeResolveGeoip(options);
|
||||
const browser = await launch({ ...options, ...resolved, geoip: false });
|
||||
// Skip --fingerprint-timezone binary flag: it only applies to the default
|
||||
// context and interferes with Playwright's timezoneId on new contexts.
|
||||
// Timezone is set via browser.newContext(timezoneId: ...) below instead.
|
||||
const browser = await launch({ ...options, ...resolved, geoip: false, timezone: undefined });
|
||||
|
||||
let context: BrowserContext;
|
||||
try {
|
||||
@@ -123,7 +128,9 @@ export async function launchPersistentContext(
|
||||
headless: options.headless ?? true,
|
||||
args,
|
||||
ignoreDefaultArgs: ["--enable-automation"],
|
||||
...(options.proxy ? { proxy: parseProxyUrl(options.proxy) } : {}),
|
||||
...(options.proxy
|
||||
? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy }
|
||||
: {}),
|
||||
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
||||
viewport: options.viewport ?? DEFAULT_VIEWPORT,
|
||||
...(resolved.locale ? { locale: resolved.locale } : {}),
|
||||
@@ -146,7 +153,9 @@ async function maybeResolveGeoip(
|
||||
if (options.timezone && options.locale) return { timezone: options.timezone, locale: options.locale };
|
||||
|
||||
const { resolveProxyGeo } = await import("./geoip.js");
|
||||
const { timezone: geoTz, locale: geoLocale } = await resolveProxyGeo(options.proxy);
|
||||
const proxyUrl = typeof options.proxy === "string" ? options.proxy : options.proxy.server;
|
||||
if (!proxyUrl) return { timezone: options.timezone, locale: options.locale };
|
||||
const { timezone: geoTz, locale: geoLocale } = await resolveProxyGeo(proxyUrl);
|
||||
return {
|
||||
timezone: options.timezone ?? geoTz ?? undefined,
|
||||
locale: options.locale ?? geoLocale ?? undefined,
|
||||
|
||||
+23
-5
@@ -34,10 +34,26 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
||||
// so we strip them and use page.authenticate() instead.
|
||||
let proxyAuth: { username: string; password: string } | undefined;
|
||||
if (options.proxy) {
|
||||
const { server, username, password } = parseProxyUrl(options.proxy);
|
||||
args.push(`--proxy-server=${server}`);
|
||||
if (username) {
|
||||
proxyAuth = { username, password: password || "" };
|
||||
if (typeof options.proxy === "string") {
|
||||
const { server, username, password } = parseProxyUrl(options.proxy);
|
||||
args.push(`--proxy-server=${server}`);
|
||||
if (username) {
|
||||
proxyAuth = { username, password: password ?? "" };
|
||||
}
|
||||
} else {
|
||||
// Strip any inline credentials from the server URL — Chromium's
|
||||
// --proxy-server doesn't support them; use page.authenticate() instead.
|
||||
const parsed = parseProxyUrl(options.proxy.server);
|
||||
args.push(`--proxy-server=${parsed.server}`);
|
||||
if (options.proxy.bypass) {
|
||||
args.push(`--proxy-bypass-list=${options.proxy.bypass}`);
|
||||
}
|
||||
// Explicit username/password fields take precedence over inline creds
|
||||
const username = options.proxy.username ?? parsed.username;
|
||||
const password = options.proxy.password ?? parsed.password;
|
||||
if (username) {
|
||||
proxyAuth = { username, password: password ?? "" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +90,9 @@ async function maybeResolveGeoip(
|
||||
if (options.timezone && options.locale) return { timezone: options.timezone, locale: options.locale };
|
||||
|
||||
const { resolveProxyGeo } = await import("./geoip.js");
|
||||
const { timezone: geoTz, locale: geoLocale } = await resolveProxyGeo(options.proxy);
|
||||
const proxyUrl = typeof options.proxy === "string" ? options.proxy : options.proxy.server;
|
||||
if (!proxyUrl) return { timezone: options.timezone, locale: options.locale };
|
||||
const { timezone: geoTz, locale: geoLocale } = await resolveProxyGeo(proxyUrl);
|
||||
return {
|
||||
timezone: options.timezone ?? geoTz ?? undefined,
|
||||
locale: options.locale ?? geoLocale ?? undefined,
|
||||
|
||||
+7
-2
@@ -5,8 +5,13 @@
|
||||
export interface LaunchOptions {
|
||||
/** Run in headless mode (default: true). */
|
||||
headless?: boolean;
|
||||
/** Proxy server URL, e.g. 'http://proxy:8080' or 'socks5://proxy:1080'. */
|
||||
proxy?: string;
|
||||
/**
|
||||
* Proxy server — URL string or Playwright proxy object.
|
||||
* String: 'http://user:pass@proxy:8080' (credentials auto-extracted).
|
||||
* Object: { server: "http://proxy:8080", bypass: ".google.com", ... }
|
||||
* — passed directly to Playwright.
|
||||
*/
|
||||
proxy?: string | { server: string; bypass?: string; username?: string; password?: string };
|
||||
/** Additional Chromium CLI arguments. */
|
||||
args?: string[];
|
||||
/** Include default stealth fingerprint args (default: true). Set false to use custom --fingerprint flags. */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseProxyUrl } from "../src/proxy.js";
|
||||
import type { LaunchOptions } from "../src/types.js";
|
||||
|
||||
describe("parseProxyUrl", () => {
|
||||
it("passes through URL without credentials", () => {
|
||||
@@ -47,3 +48,37 @@ describe("parseProxyUrl", () => {
|
||||
expect(parseProxyUrl("not-a-url")).toEqual({ server: "not-a-url" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("proxy dict type", () => {
|
||||
it("accepts string proxy in LaunchOptions", () => {
|
||||
const opts: LaunchOptions = { proxy: "http://proxy:8080" };
|
||||
expect(typeof opts.proxy).toBe("string");
|
||||
});
|
||||
|
||||
it("accepts dict proxy with bypass in LaunchOptions", () => {
|
||||
const opts: LaunchOptions = {
|
||||
proxy: { server: "http://proxy:8080", bypass: ".google.com,localhost" },
|
||||
};
|
||||
expect(typeof opts.proxy).toBe("object");
|
||||
if (typeof opts.proxy === "object") {
|
||||
expect(opts.proxy.server).toBe("http://proxy:8080");
|
||||
expect(opts.proxy.bypass).toBe(".google.com,localhost");
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts dict proxy with auth and bypass in LaunchOptions", () => {
|
||||
const opts: LaunchOptions = {
|
||||
proxy: {
|
||||
server: "http://proxy:8080",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
bypass: ".example.com",
|
||||
},
|
||||
};
|
||||
if (typeof opts.proxy === "object") {
|
||||
expect(opts.proxy.username).toBe("user");
|
||||
expect(opts.proxy.password).toBe("pass");
|
||||
expect(opts.proxy.bypass).toBe(".example.com");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user