mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: native SOCKS5 proxy support in proxy= parameter
Route SOCKS5/SOCKS5h proxies via --proxy-server Chrome arg instead of Playwright's proxy dict (which rejects SOCKS5 with credentials). Handles string URLs, Playwright dicts, IPv6, bypass lists. SOCKS5 geoip exit IP resolution uses socks-proxy-agent (optional peer dep). Falls back to DNS if not installed.
This commit is contained in:
@@ -242,8 +242,9 @@ browser = launch()
|
|||||||
# Headed mode (see the browser window)
|
# Headed mode (see the browser window)
|
||||||
browser = launch(headless=False)
|
browser = launch(headless=False)
|
||||||
|
|
||||||
# With proxy
|
# With proxy (HTTP or SOCKS5)
|
||||||
browser = launch(proxy="http://user:pass@proxy:8080")
|
browser = launch(proxy="http://user:pass@proxy:8080")
|
||||||
|
browser = launch(proxy="socks5://user:pass@proxy:1080")
|
||||||
|
|
||||||
# With proxy dict (bypass, separate auth fields)
|
# With proxy dict (bypass, separate auth fields)
|
||||||
browser = launch(proxy={"server": "http://proxy:8080", "bypass": ".google.com", "username": "user", "password": "pass"})
|
browser = launch(proxy={"server": "http://proxy:8080", "bypass": ".google.com", "username": "user", "password": "pass"})
|
||||||
@@ -1028,7 +1029,7 @@ A: Camoufox patches Firefox. We patch Chromium. Chromium means native Playwright
|
|||||||
A: Possibly. Bot detection is an arms race. Source-level patches are harder to detect than config-level patches, but not impossible. We actively monitor and update when detection evolves.
|
A: Possibly. Bot detection is an arms race. Source-level patches are harder to detect than config-level patches, but not impossible. We actively monitor and update when detection evolves.
|
||||||
|
|
||||||
**Q: Can I use my own proxy?**
|
**Q: Can I use my own proxy?**
|
||||||
A: Yes. Pass `proxy="http://user:pass@host:port"` to `launch()`.
|
A: Yes. Pass `proxy="http://user:pass@host:port"` or `proxy="socks5://user:pass@host:port"` to `launch()`. Both HTTP and SOCKS5 proxies are supported natively.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
+85
-21
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Literal, TypedDict
|
from typing import Any, Literal, TypedDict
|
||||||
from urllib.parse import unquote, urlparse, urlunparse
|
from urllib.parse import quote, unquote, urlparse, urlunparse
|
||||||
|
|
||||||
from .config import DEFAULT_VIEWPORT, IGNORE_DEFAULT_ARGS, get_default_stealth_args
|
from .config import DEFAULT_VIEWPORT, IGNORE_DEFAULT_ARGS, get_default_stealth_args
|
||||||
from .download import ensure_binary
|
from .download import ensure_binary
|
||||||
@@ -105,11 +105,12 @@ def launch(
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
|
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy)
|
||||||
args = _resolve_webrtc_args(args, proxy)
|
args = _resolve_webrtc_args(args, proxy)
|
||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
|
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ def launch(
|
|||||||
headless=headless,
|
headless=headless,
|
||||||
args=chrome_args,
|
args=chrome_args,
|
||||||
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
||||||
**_build_proxy_kwargs(proxy),
|
**proxy_kwargs,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -194,11 +195,12 @@ async def launch_async( # noqa: C901
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
|
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy)
|
||||||
args = _resolve_webrtc_args(args, proxy)
|
args = _resolve_webrtc_args(args, proxy)
|
||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
|
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ async def launch_async( # noqa: C901
|
|||||||
headless=headless,
|
headless=headless,
|
||||||
args=chrome_args,
|
args=chrome_args,
|
||||||
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
||||||
**_build_proxy_kwargs(proxy),
|
**proxy_kwargs,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -297,11 +299,12 @@ def launch_persistent_context(
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
|
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy)
|
||||||
args = _resolve_webrtc_args(args, proxy)
|
args = _resolve_webrtc_args(args, proxy)
|
||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
||||||
@@ -331,7 +334,7 @@ def launch_persistent_context(
|
|||||||
headless=headless,
|
headless=headless,
|
||||||
args=chrome_args,
|
args=chrome_args,
|
||||||
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
||||||
**_build_proxy_kwargs(proxy),
|
**proxy_kwargs,
|
||||||
**context_kwargs,
|
**context_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -422,11 +425,12 @@ async def launch_persistent_context_async(
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
|
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy)
|
||||||
args = _resolve_webrtc_args(args, proxy)
|
args = _resolve_webrtc_args(args, proxy)
|
||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
||||||
@@ -456,7 +460,7 @@ async def launch_persistent_context_async(
|
|||||||
headless=headless,
|
headless=headless,
|
||||||
args=chrome_args,
|
args=chrome_args,
|
||||||
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
||||||
**_build_proxy_kwargs(proxy),
|
**proxy_kwargs,
|
||||||
**context_kwargs,
|
**context_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -631,14 +635,42 @@ def _ensure_proxy_scheme(proxy_url: str) -> str:
|
|||||||
return proxy_url if "://" in proxy_url else f"http://{proxy_url}"
|
return proxy_url if "://" in proxy_url else f"http://{proxy_url}"
|
||||||
|
|
||||||
|
|
||||||
|
def _reconstruct_socks_url(proxy: ProxySettings) -> str:
|
||||||
|
"""Reconstruct a SOCKS5 URL with inline credentials from a Playwright proxy dict."""
|
||||||
|
server = proxy.get("server", "")
|
||||||
|
username = proxy.get("username", "")
|
||||||
|
password = proxy.get("password", "")
|
||||||
|
if not username:
|
||||||
|
return server
|
||||||
|
parsed = urlparse(server)
|
||||||
|
creds = quote(username, safe="")
|
||||||
|
if password:
|
||||||
|
creds += f":{quote(password, safe='')}"
|
||||||
|
host = parsed.hostname or ""
|
||||||
|
if ":" in host: # IPv6 literal — re-add brackets
|
||||||
|
host = f"[{host}]"
|
||||||
|
netloc = f"{creds}@{host}"
|
||||||
|
if parsed.port:
|
||||||
|
netloc += f":{parsed.port}"
|
||||||
|
return urlunparse((parsed.scheme, netloc, parsed.path, "", "", ""))
|
||||||
|
|
||||||
|
|
||||||
def _extract_proxy_url(proxy: str | ProxySettings | None) -> str | None:
|
def _extract_proxy_url(proxy: str | ProxySettings | None) -> str | None:
|
||||||
"""Extract and normalize proxy URL string from proxy param."""
|
"""Extract and normalize proxy URL string from proxy param.
|
||||||
|
|
||||||
|
For SOCKS5 dicts with separate username/password fields, reconstructs
|
||||||
|
the full URL with inline credentials so SOCKS5 auth works.
|
||||||
|
"""
|
||||||
if proxy is None:
|
if proxy is None:
|
||||||
return None
|
return None
|
||||||
raw = proxy.get("server") if isinstance(proxy, dict) else proxy
|
if isinstance(proxy, dict):
|
||||||
if not raw:
|
server = proxy.get("server", "")
|
||||||
return None
|
if not server:
|
||||||
return _ensure_proxy_scheme(raw)
|
return None
|
||||||
|
if _is_socks_proxy(proxy):
|
||||||
|
return _reconstruct_socks_url(proxy)
|
||||||
|
return _ensure_proxy_scheme(server)
|
||||||
|
return _ensure_proxy_scheme(proxy)
|
||||||
|
|
||||||
|
|
||||||
def maybe_resolve_geoip(
|
def maybe_resolve_geoip(
|
||||||
@@ -694,7 +726,7 @@ def _resolve_webrtc_args(
|
|||||||
return args
|
return args
|
||||||
proxy_url = _extract_proxy_url(proxy)
|
proxy_url = _extract_proxy_url(proxy)
|
||||||
if not proxy_url:
|
if not proxy_url:
|
||||||
logger.debug("--fingerprint-webrtc-ip=auto but no proxy set — removing flag")
|
logger.warning("--fingerprint-webrtc-ip=auto requires a proxy; removing flag")
|
||||||
args = list(args)
|
args = list(args)
|
||||||
del args[idx]
|
del args[idx]
|
||||||
return args
|
return args
|
||||||
@@ -702,7 +734,7 @@ def _resolve_webrtc_args(
|
|||||||
from .geoip import _resolve_exit_ip
|
from .geoip import _resolve_exit_ip
|
||||||
exit_ip = _resolve_exit_ip(proxy_url)
|
exit_ip = _resolve_exit_ip(proxy_url)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("WebRTC IP resolution failed — removing flag")
|
logger.warning("Failed to resolve proxy exit IP for WebRTC spoofing; removing --fingerprint-webrtc-ip=auto")
|
||||||
args = list(args)
|
args = list(args)
|
||||||
del args[idx]
|
del args[idx]
|
||||||
return args
|
return args
|
||||||
@@ -710,6 +742,7 @@ def _resolve_webrtc_args(
|
|||||||
args = list(args)
|
args = list(args)
|
||||||
args[idx] = f"--fingerprint-webrtc-ip={exit_ip}"
|
args[idx] = f"--fingerprint-webrtc-ip={exit_ip}"
|
||||||
else:
|
else:
|
||||||
|
logger.warning("Could not resolve proxy exit IP for WebRTC spoofing; removing --fingerprint-webrtc-ip=auto")
|
||||||
args = list(args)
|
args = list(args)
|
||||||
del args[idx]
|
del args[idx]
|
||||||
return args
|
return args
|
||||||
@@ -799,10 +832,41 @@ def _parse_proxy_url(proxy: str) -> dict[str, Any]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _build_proxy_kwargs(proxy: str | ProxySettings | None) -> dict[str, Any]:
|
def _is_socks_proxy(proxy: str | ProxySettings | None) -> bool:
|
||||||
"""Build proxy kwargs for Playwright launch."""
|
"""Check if the proxy uses SOCKS5 protocol."""
|
||||||
if proxy is None:
|
if proxy is None:
|
||||||
return {}
|
return False
|
||||||
|
url = proxy.get("server", "") if isinstance(proxy, dict) else proxy
|
||||||
|
return url.lower().startswith(("socks5://", "socks5h://"))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_proxy_config(
|
||||||
|
proxy: str | ProxySettings | None,
|
||||||
|
) -> tuple[dict[str, Any], list[str]]:
|
||||||
|
"""Resolve proxy into Playwright kwargs and Chrome args.
|
||||||
|
|
||||||
|
Playwright rejects SOCKS5 proxies with credentials in its proxy dict,
|
||||||
|
so SOCKS5 is passed via --proxy-server Chrome arg instead.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(proxy_kwargs, extra_chrome_args) — one or both will be empty.
|
||||||
|
"""
|
||||||
|
if proxy is None:
|
||||||
|
return {}, []
|
||||||
|
|
||||||
|
if _is_socks_proxy(proxy):
|
||||||
|
# SOCKS5: bypass Playwright, pass directly to Chrome via --proxy-server.
|
||||||
|
# Chrome handles SOCKS5 auth natively from the URL.
|
||||||
|
if isinstance(proxy, dict):
|
||||||
|
url = _reconstruct_socks_url(proxy)
|
||||||
|
extra_args = [f"--proxy-server={url}"]
|
||||||
|
if proxy.get("bypass"):
|
||||||
|
extra_args.append(f"--proxy-bypass-list={proxy['bypass']}")
|
||||||
|
return {}, extra_args
|
||||||
|
# String URL — pass as-is (Chrome handles user:pass@ in the URL)
|
||||||
|
return {}, [f"--proxy-server={proxy}"]
|
||||||
|
|
||||||
|
# HTTP/HTTPS: use Playwright's proxy dict as before
|
||||||
if isinstance(proxy, dict):
|
if isinstance(proxy, dict):
|
||||||
return {"proxy": proxy}
|
return {"proxy": proxy}, []
|
||||||
return {"proxy": _parse_proxy_url(proxy)}
|
return {"proxy": _parse_proxy_url(proxy)}, []
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ def resolve_proxy_geo_with_ip(
|
|||||||
)
|
)
|
||||||
return timezone, locale, ip
|
return timezone, locale, ip
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("GeoIP lookup failed for %s: %s", ip, exc)
|
logger.warning("GeoIP lookup failed for %s: %s", ip, exc)
|
||||||
return None, None, ip
|
return None, None, ip
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ def _resolve_proxy_ip(proxy_url: str) -> str | None:
|
|||||||
return ip
|
return ip
|
||||||
return None
|
return None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Failed to resolve proxy hostname: %s", exc)
|
logger.warning("Failed to resolve proxy hostname: %s", exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -165,9 +165,14 @@ def _resolve_exit_ip(proxy_url: str) -> str | None:
|
|||||||
ipaddress.ip_address(ip)
|
ipaddress.ip_address(ip)
|
||||||
logger.debug("Exit IP via %s: %s", url, ip)
|
logger.debug("Exit IP via %s: %s", url, ip)
|
||||||
return ip
|
return ip
|
||||||
|
except httpx.UnsupportedProtocol:
|
||||||
|
logger.warning(
|
||||||
|
"SOCKS5 proxy requires socksio: pip install cloakbrowser[geoip]"
|
||||||
|
)
|
||||||
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
logger.debug("Failed to discover exit IP through proxy")
|
logger.warning("Failed to discover exit IP through proxy")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -63,10 +63,13 @@ await browser.close();
|
|||||||
```javascript
|
```javascript
|
||||||
import { launch, launchContext, launchPersistentContext } from 'cloakbrowser';
|
import { launch, launchContext, launchPersistentContext } from 'cloakbrowser';
|
||||||
|
|
||||||
// With proxy
|
// With proxy (HTTP or SOCKS5)
|
||||||
const browser = await launch({
|
const browser = await launch({
|
||||||
proxy: 'http://user:pass@proxy:8080',
|
proxy: 'http://user:pass@proxy:8080',
|
||||||
});
|
});
|
||||||
|
const browser = await launch({
|
||||||
|
proxy: 'socks5://user:pass@proxy:1080',
|
||||||
|
});
|
||||||
|
|
||||||
// With proxy object (bypass, separate auth fields)
|
// With proxy object (bypass, separate auth fields)
|
||||||
const browser = await launch({
|
const browser = await launch({
|
||||||
@@ -211,7 +214,7 @@ const page = await browser.newPage();
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js >= 18
|
- Node.js >= 20
|
||||||
- One of: `playwright-core` >= 1.40 or `puppeteer-core` >= 21
|
- One of: `playwright-core` >= 1.40 or `puppeteer-core` >= 21
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
Generated
+57
-9
@@ -1,31 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "cloakbrowser",
|
"name": "cloakbrowser",
|
||||||
"version": "0.3.9",
|
"version": "0.3.23",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cloakbrowser",
|
"name": "cloakbrowser",
|
||||||
"version": "0.3.9",
|
"version": "0.3.23",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tar": "^7.0.0"
|
"tar": "^7.0.0"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"cloakbrowser": "dist/cli.js"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"mmdb-lib": "^3.0.2",
|
"mmdb-lib": "^3.0.2",
|
||||||
"playwright-core": "^1.40.0",
|
"playwright-core": "^1.40.0",
|
||||||
"puppeteer-core": "^21.0.0",
|
"puppeteer-core": "^21.0.0",
|
||||||
|
"socks-proxy-agent": "^10.0.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vitest": "^1.0.0"
|
"vitest": "^1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"mmdb-lib": ">=2.0.0",
|
"mmdb-lib": ">=2.0.0",
|
||||||
"playwright-core": ">=1.40.0",
|
"playwright-core": ">=1.40.0",
|
||||||
"puppeteer-core": ">=21.0.0"
|
"puppeteer-core": ">=21.0.0",
|
||||||
|
"socks-proxy-agent": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"mmdb-lib": {
|
"mmdb-lib": {
|
||||||
@@ -36,6 +41,9 @@
|
|||||||
},
|
},
|
||||||
"puppeteer-core": {
|
"puppeteer-core": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"socks-proxy-agent": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2003,6 +2011,21 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": {
|
||||||
|
"version": "8.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||||
|
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"socks": "^2.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pac-resolver": {
|
"node_modules/pac-resolver": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
|
||||||
@@ -2164,6 +2187,21 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-agent/node_modules/socks-proxy-agent": {
|
||||||
|
"version": "8.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||||
|
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"socks": "^2.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -2332,18 +2370,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socks-proxy-agent": {
|
"node_modules/socks-proxy-agent": {
|
||||||
"version": "8.0.5",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.0.0.tgz",
|
||||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
"integrity": "sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "^7.1.2",
|
"agent-base": "9.0.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"socks": "^2.8.3"
|
"socks": "^2.8.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent/node_modules/agent-base": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
|
|||||||
+7
-2
@@ -55,12 +55,13 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/CloakHQ/cloakbrowser#javascript--nodejs",
|
"homepage": "https://github.com/CloakHQ/cloakbrowser#javascript--nodejs",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"mmdb-lib": ">=2.0.0",
|
"mmdb-lib": ">=2.0.0",
|
||||||
"playwright-core": ">=1.40.0",
|
"playwright-core": ">=1.40.0",
|
||||||
"puppeteer-core": ">=21.0.0"
|
"puppeteer-core": ">=21.0.0",
|
||||||
|
"socks-proxy-agent": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"playwright-core": {
|
"playwright-core": {
|
||||||
@@ -71,6 +72,9 @@
|
|||||||
},
|
},
|
||||||
"mmdb-lib": {
|
"mmdb-lib": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"socks-proxy-agent": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -79,6 +83,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"mmdb-lib": "^3.0.2",
|
"mmdb-lib": "^3.0.2",
|
||||||
|
"socks-proxy-agent": "^10.0.0",
|
||||||
"playwright-core": "^1.40.0",
|
"playwright-core": "^1.40.0",
|
||||||
"puppeteer-core": "^21.0.0",
|
"puppeteer-core": "^21.0.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
|
|||||||
+60
-8
@@ -15,7 +15,7 @@ import dns from "node:dns/promises";
|
|||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
import { getCacheDir } from "./config.js";
|
import { getCacheDir } from "./config.js";
|
||||||
import type { LaunchOptions } from "./types.js";
|
import type { LaunchOptions } from "./types.js";
|
||||||
import { ensureProxyScheme } from "./proxy.js";
|
import { ensureProxyScheme, isSocksProxy, reconstructSocksUrl, type ProxyDict } from "./proxy.js";
|
||||||
|
|
||||||
// P3TERX mirror of MaxMind GeoLite2-City — no license key needed
|
// P3TERX mirror of MaxMind GeoLite2-City — no license key needed
|
||||||
const GEOIP_DB_URL =
|
const GEOIP_DB_URL =
|
||||||
@@ -129,9 +129,44 @@ const IP_ECHO_URLS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
async function resolveExitIp(proxyUrl: string): Promise<string | null> {
|
async function resolveExitIp(proxyUrl: string): Promise<string | null> {
|
||||||
// Node.js fetch doesn't support proxy natively — use a CONNECT tunnel via http
|
const isSocks = isSocksProxy(proxyUrl);
|
||||||
// For simplicity, use a direct HTTP request to a plain-text IP echo service
|
|
||||||
// through the proxy using Node's http module
|
// SOCKS5: tunnel through the SOCKS5 proxy via socks-proxy-agent
|
||||||
|
if (isSocks) {
|
||||||
|
let SocksProxyAgent: typeof import("socks-proxy-agent").SocksProxyAgent;
|
||||||
|
try {
|
||||||
|
({ SocksProxyAgent } = await import("socks-proxy-agent"));
|
||||||
|
} catch {
|
||||||
|
console.warn("[cloakbrowser] socks-proxy-agent not installed — cannot resolve exit IP through SOCKS5 proxy. Install it: npm install socks-proxy-agent");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { default: https } = await import("node:https");
|
||||||
|
const agent = new SocksProxyAgent(proxyUrl);
|
||||||
|
|
||||||
|
for (const echoUrl of IP_ECHO_URLS) {
|
||||||
|
try {
|
||||||
|
const ip = await new Promise<string | null>((resolve) => {
|
||||||
|
const req = https.request(echoUrl, { agent, timeout: 10_000 }, (res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk: Buffer) => (data += chunk.toString()));
|
||||||
|
res.on("end", () => {
|
||||||
|
const ip = data.trim();
|
||||||
|
resolve(net.isIP(ip) ? ip : null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on("error", () => resolve(null));
|
||||||
|
req.on("timeout", () => { req.destroy(); resolve(null); });
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
if (ip) return ip;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP/HTTPS: use a CONNECT tunnel via http
|
||||||
try {
|
try {
|
||||||
const { default: http } = await import("node:http");
|
const { default: http } = await import("node:http");
|
||||||
const { default: https } = await import("node:https");
|
const { default: https } = await import("node:https");
|
||||||
@@ -264,6 +299,22 @@ function maybeTriggerUpdate(dbPath: string): void {
|
|||||||
downloadGeoipDb(dbPath).catch(() => {});
|
downloadGeoipDb(dbPath).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a usable proxy URL from LaunchOptions.proxy.
|
||||||
|
* For SOCKS5 dicts with separate credentials, reconstructs the full URL
|
||||||
|
* with inline credentials so SOCKS5 auth works.
|
||||||
|
*/
|
||||||
|
function extractProxyUrl(proxy: string | ProxyDict | undefined): string | null {
|
||||||
|
if (!proxy) return null;
|
||||||
|
if (typeof proxy === "string") return ensureProxyScheme(proxy);
|
||||||
|
const p = proxy as ProxyDict;
|
||||||
|
if (!p.server) return null;
|
||||||
|
if (p.username && isSocksProxy(p)) {
|
||||||
|
return reconstructSocksUrl(p);
|
||||||
|
}
|
||||||
|
return ensureProxyScheme(p.server);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-fill timezone/locale from proxy IP when geoip is enabled.
|
* Auto-fill timezone/locale from proxy IP when geoip is enabled.
|
||||||
* Also returns exitIp as a free bonus (reused for WebRTC spoofing).
|
* Also returns exitIp as a free bonus (reused for WebRTC spoofing).
|
||||||
@@ -273,9 +324,8 @@ export async function maybeResolveGeoip(
|
|||||||
): Promise<{ timezone?: string; locale?: string; exitIp?: string }> {
|
): Promise<{ timezone?: string; locale?: string; exitIp?: string }> {
|
||||||
if (!options.geoip || !options.proxy) return { timezone: options.timezone, locale: options.locale };
|
if (!options.geoip || !options.proxy) return { timezone: options.timezone, locale: options.locale };
|
||||||
|
|
||||||
let proxyUrl = typeof options.proxy === "string" ? options.proxy : options.proxy.server;
|
const proxyUrl = extractProxyUrl(options.proxy);
|
||||||
if (!proxyUrl) return { timezone: options.timezone, locale: options.locale };
|
if (!proxyUrl) return { timezone: options.timezone, locale: options.locale };
|
||||||
proxyUrl = ensureProxyScheme(proxyUrl);
|
|
||||||
|
|
||||||
// When both tz/locale are explicit, still resolve exit IP for WebRTC
|
// When both tz/locale are explicit, still resolve exit IP for WebRTC
|
||||||
if (options.timezone && options.locale) {
|
if (options.timezone && options.locale) {
|
||||||
@@ -304,13 +354,13 @@ export async function resolveWebrtcArgs(
|
|||||||
const idx = args.findIndex(a => a === "--fingerprint-webrtc-ip=auto");
|
const idx = args.findIndex(a => a === "--fingerprint-webrtc-ip=auto");
|
||||||
if (idx === -1) return args;
|
if (idx === -1) return args;
|
||||||
|
|
||||||
let proxyUrl = typeof options.proxy === "string" ? options.proxy : options.proxy?.server;
|
const proxyUrl = extractProxyUrl(options.proxy);
|
||||||
if (!proxyUrl) {
|
if (!proxyUrl) {
|
||||||
|
console.warn("[cloakbrowser] --fingerprint-webrtc-ip=auto requires a proxy; removing flag");
|
||||||
const result = [...args];
|
const result = [...args];
|
||||||
result.splice(idx, 1);
|
result.splice(idx, 1);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
proxyUrl = ensureProxyScheme(proxyUrl);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ip = await resolveExitIp(proxyUrl);
|
const ip = await resolveExitIp(proxyUrl);
|
||||||
@@ -318,10 +368,12 @@ export async function resolveWebrtcArgs(
|
|||||||
if (ip) {
|
if (ip) {
|
||||||
result[idx] = `--fingerprint-webrtc-ip=${ip}`;
|
result[idx] = `--fingerprint-webrtc-ip=${ip}`;
|
||||||
} else {
|
} else {
|
||||||
|
console.warn("[cloakbrowser] Could not resolve proxy exit IP for WebRTC spoofing; removing --fingerprint-webrtc-ip=auto");
|
||||||
result.splice(idx, 1);
|
result.splice(idx, 1);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch {
|
} catch {
|
||||||
|
console.warn("[cloakbrowser] Failed to resolve proxy exit IP for WebRTC spoofing; removing --fingerprint-webrtc-ip=auto");
|
||||||
const result = [...args];
|
const result = [...args];
|
||||||
result.splice(idx, 1);
|
result.splice(idx, 1);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { LaunchOptions, LaunchContextOptions, LaunchPersistentContextOption
|
|||||||
import { DEFAULT_VIEWPORT, IGNORE_DEFAULT_ARGS } from "./config.js";
|
import { DEFAULT_VIEWPORT, IGNORE_DEFAULT_ARGS } from "./config.js";
|
||||||
import { buildArgs } from "./args.js";
|
import { buildArgs } from "./args.js";
|
||||||
import { ensureBinary } from "./download.js";
|
import { ensureBinary } from "./download.js";
|
||||||
import { parseProxyUrl } from "./proxy.js";
|
import { resolveProxyConfig } from "./proxy.js";
|
||||||
import { maybeResolveGeoip, resolveWebrtcArgs } from "./geoip.js";
|
import { maybeResolveGeoip, resolveWebrtcArgs } from "./geoip.js";
|
||||||
|
|
||||||
/** @internal Accept both timezone and timezoneId — either works, no warning. Exported for testing. */
|
/** @internal Accept both timezone and timezoneId — either works, no warning. Exported for testing. */
|
||||||
@@ -39,20 +39,19 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
|||||||
|
|
||||||
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
||||||
const { exitIp, ...resolved } = await maybeResolveGeoip(options);
|
const { exitIp, ...resolved } = await maybeResolveGeoip(options);
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig(options.proxy);
|
||||||
let resolvedArgs = await resolveWebrtcArgs(options);
|
let resolvedArgs = await resolveWebrtcArgs(options);
|
||||||
if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) {
|
if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) {
|
||||||
resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`];
|
resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`];
|
||||||
}
|
}
|
||||||
const args = buildArgs({ ...options, ...resolved, args: resolvedArgs });
|
const args = buildArgs({ ...options, ...resolved, args: [...(resolvedArgs ?? []), ...proxyArgs] });
|
||||||
|
|
||||||
const browser = await chromium.launch({
|
const browser = await chromium.launch({
|
||||||
executablePath: binaryPath,
|
executablePath: binaryPath,
|
||||||
headless: options.headless ?? true,
|
headless: options.headless ?? true,
|
||||||
args,
|
args,
|
||||||
ignoreDefaultArgs: IGNORE_DEFAULT_ARGS,
|
ignoreDefaultArgs: IGNORE_DEFAULT_ARGS,
|
||||||
...(options.proxy
|
...(proxyOption ? { proxy: proxyOption } : {}),
|
||||||
? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy }
|
|
||||||
: {}),
|
|
||||||
...options.launchOptions,
|
...options.launchOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,11 +163,12 @@ export async function launchPersistentContext(
|
|||||||
|
|
||||||
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
||||||
const { exitIp, ...resolved } = await maybeResolveGeoip(options);
|
const { exitIp, ...resolved } = await maybeResolveGeoip(options);
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig(options.proxy);
|
||||||
let resolvedArgs = await resolveWebrtcArgs(options);
|
let resolvedArgs = await resolveWebrtcArgs(options);
|
||||||
if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) {
|
if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) {
|
||||||
resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`];
|
resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`];
|
||||||
}
|
}
|
||||||
const args = buildArgs({ ...options, ...resolved, args: resolvedArgs });
|
const args = buildArgs({ ...options, ...resolved, args: [...(resolvedArgs ?? []), ...proxyArgs] });
|
||||||
|
|
||||||
// locale and timezone are set via binary flags (--lang, --fingerprint-timezone)
|
// locale and timezone are set via binary flags (--lang, --fingerprint-timezone)
|
||||||
// — NOT via Playwright context kwargs which use detectable CDP emulation.
|
// — NOT via Playwright context kwargs which use detectable CDP emulation.
|
||||||
@@ -177,9 +177,7 @@ export async function launchPersistentContext(
|
|||||||
headless: options.headless ?? true,
|
headless: options.headless ?? true,
|
||||||
args,
|
args,
|
||||||
ignoreDefaultArgs: IGNORE_DEFAULT_ARGS,
|
ignoreDefaultArgs: IGNORE_DEFAULT_ARGS,
|
||||||
...(options.proxy
|
...(proxyOption ? { proxy: proxyOption } : {}),
|
||||||
? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy }
|
|
||||||
: {}),
|
|
||||||
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
||||||
viewport: options.viewport === undefined ? DEFAULT_VIEWPORT : options.viewport,
|
viewport: options.viewport === undefined ? DEFAULT_VIEWPORT : options.viewport,
|
||||||
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
||||||
|
|||||||
@@ -23,6 +23,65 @@ export function ensureProxyScheme(proxyUrl: string): string {
|
|||||||
* Also handles: no credentials, URL-encoded special chars, socks5://, missing port,
|
* Also handles: no credentials, URL-encoded special chars, socks5://, missing port,
|
||||||
* and bare proxy strings without a scheme (e.g. "user:pass@host:port" -> treated as http).
|
* and bare proxy strings without a scheme (e.g. "user:pass@host:port" -> treated as http).
|
||||||
*/
|
*/
|
||||||
|
/** Proxy dict shape accepted by Playwright/Puppeteer wrappers. */
|
||||||
|
export type ProxyDict = { server: string; bypass?: string; username?: string; password?: string };
|
||||||
|
|
||||||
|
/** Result of resolveProxyConfig — either Playwright dict OR Chrome arg, never both. */
|
||||||
|
export interface ProxyConfig {
|
||||||
|
/** Playwright proxy option (for HTTP proxies). */
|
||||||
|
proxyOption?: ParsedProxy;
|
||||||
|
/** Chrome CLI args (for SOCKS5 proxies, e.g. ["--proxy-server=socks5://..."]). */
|
||||||
|
proxyArgs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a proxy uses the SOCKS5 protocol.
|
||||||
|
*/
|
||||||
|
export function isSocksProxy(proxy: string | ProxyDict | undefined | null): boolean {
|
||||||
|
if (!proxy) return false;
|
||||||
|
const url = typeof proxy === "string" ? proxy : proxy.server;
|
||||||
|
return /^socks5h?:\/\//i.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstruct a SOCKS5 URL with inline credentials from a proxy dict.
|
||||||
|
*/
|
||||||
|
export function reconstructSocksUrl(proxy: ProxyDict): string {
|
||||||
|
const url = new URL(proxy.server);
|
||||||
|
if (proxy.username) {
|
||||||
|
url.username = encodeURIComponent(proxy.username);
|
||||||
|
if (proxy.password) url.password = encodeURIComponent(proxy.password);
|
||||||
|
}
|
||||||
|
return url.href.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve proxy into Playwright option and/or Chrome args.
|
||||||
|
*
|
||||||
|
* Playwright rejects SOCKS5 proxies with credentials in its proxy dict,
|
||||||
|
* so SOCKS5 is passed via --proxy-server Chrome arg instead.
|
||||||
|
*/
|
||||||
|
export function resolveProxyConfig(proxy: string | ProxyDict | undefined): ProxyConfig {
|
||||||
|
if (!proxy) return { proxyArgs: [] };
|
||||||
|
|
||||||
|
if (isSocksProxy(proxy)) {
|
||||||
|
// SOCKS5: bypass Playwright, pass directly to Chrome via --proxy-server.
|
||||||
|
if (typeof proxy === "string") {
|
||||||
|
return { proxyArgs: [`--proxy-server=${proxy}`] };
|
||||||
|
}
|
||||||
|
const socksUrl = reconstructSocksUrl(proxy);
|
||||||
|
const args = [`--proxy-server=${socksUrl}`];
|
||||||
|
if (proxy.bypass) args.push(`--proxy-bypass-list=${proxy.bypass}`);
|
||||||
|
return { proxyArgs: args };
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP/HTTPS: use Playwright's proxy dict
|
||||||
|
if (typeof proxy === "string") {
|
||||||
|
return { proxyOption: parseProxyUrl(proxy), proxyArgs: [] };
|
||||||
|
}
|
||||||
|
return { proxyOption: proxy as ParsedProxy, proxyArgs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
export function parseProxyUrl(proxy: string): ParsedProxy {
|
export function parseProxyUrl(proxy: string): ParsedProxy {
|
||||||
let url: URL;
|
let url: URL;
|
||||||
// Bare format: "user:pass@host:port" — new URL() throws without a scheme.
|
// Bare format: "user:pass@host:port" — new URL() throws without a scheme.
|
||||||
|
|||||||
+9
-7
@@ -9,7 +9,7 @@ import type { LaunchOptions } from "./types.js";
|
|||||||
import { IGNORE_DEFAULT_ARGS } from "./config.js";
|
import { IGNORE_DEFAULT_ARGS } from "./config.js";
|
||||||
import { buildArgs } from "./args.js";
|
import { buildArgs } from "./args.js";
|
||||||
import { ensureBinary } from "./download.js";
|
import { ensureBinary } from "./download.js";
|
||||||
import { parseProxyUrl } from "./proxy.js";
|
import { isSocksProxy, parseProxyUrl, resolveProxyConfig } from "./proxy.js";
|
||||||
import { maybeResolveGeoip, resolveWebrtcArgs } from "./geoip.js";
|
import { maybeResolveGeoip, resolveWebrtcArgs } from "./geoip.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,25 +39,27 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
|||||||
const args = buildArgs({ ...options, ...resolved, args: resolvedArgs });
|
const args = buildArgs({ ...options, ...resolved, args: resolvedArgs });
|
||||||
|
|
||||||
// Puppeteer handles proxy via CLI args, not a separate option.
|
// Puppeteer handles proxy via CLI args, not a separate option.
|
||||||
// Chromium's --proxy-server does NOT support inline credentials,
|
// SOCKS5: Chrome supports inline credentials natively (RFC 1929 auth).
|
||||||
// so we strip them and use page.authenticate() instead.
|
// HTTP: Chrome does NOT support inline credentials — strip them and
|
||||||
|
// use page.authenticate() for Proxy-Authorization headers instead.
|
||||||
let proxyAuth: { username: string; password: string } | undefined;
|
let proxyAuth: { username: string; password: string } | undefined;
|
||||||
if (options.proxy) {
|
if (options.proxy) {
|
||||||
if (typeof options.proxy === "string") {
|
if (isSocksProxy(options.proxy)) {
|
||||||
|
// SOCKS5: pass full URL with credentials to Chrome directly
|
||||||
|
const { proxyArgs } = resolveProxyConfig(options.proxy);
|
||||||
|
args.push(...proxyArgs);
|
||||||
|
} else if (typeof options.proxy === "string") {
|
||||||
const { server, username, password } = parseProxyUrl(options.proxy);
|
const { server, username, password } = parseProxyUrl(options.proxy);
|
||||||
args.push(`--proxy-server=${server}`);
|
args.push(`--proxy-server=${server}`);
|
||||||
if (username) {
|
if (username) {
|
||||||
proxyAuth = { username, password: password ?? "" };
|
proxyAuth = { username, password: password ?? "" };
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
const parsed = parseProxyUrl(options.proxy.server);
|
||||||
args.push(`--proxy-server=${parsed.server}`);
|
args.push(`--proxy-server=${parsed.server}`);
|
||||||
if (options.proxy.bypass) {
|
if (options.proxy.bypass) {
|
||||||
args.push(`--proxy-bypass-list=${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 username = options.proxy.username ?? parsed.username;
|
||||||
const password = options.proxy.password ?? parsed.password;
|
const password = options.proxy.password ?? parsed.password;
|
||||||
if (username) {
|
if (username) {
|
||||||
|
|||||||
+90
-1
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { parseProxyUrl } from "../src/proxy.js";
|
import { parseProxyUrl, isSocksProxy, resolveProxyConfig } from "../src/proxy.js";
|
||||||
import type { LaunchOptions } from "../src/types.js";
|
import type { LaunchOptions } from "../src/types.js";
|
||||||
|
|
||||||
describe("parseProxyUrl", () => {
|
describe("parseProxyUrl", () => {
|
||||||
@@ -115,3 +115,92 @@ describe("bare proxy format (user:pass@host:port)", () => {
|
|||||||
expect(parseProxyUrl("proxy:8080")).toEqual({ server: "proxy:8080" });
|
expect(parseProxyUrl("proxy:8080")).toEqual({ server: "proxy:8080" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isSocksProxy", () => {
|
||||||
|
it("detects socks5 string", () => {
|
||||||
|
expect(isSocksProxy("socks5://user:pass@host:1080")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects socks5h string", () => {
|
||||||
|
expect(isSocksProxy("socks5h://host:1080")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("case insensitive", () => {
|
||||||
|
expect(isSocksProxy("SOCKS5://host:1080")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects http", () => {
|
||||||
|
expect(isSocksProxy("http://host:8080")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects socks5 dict", () => {
|
||||||
|
expect(isSocksProxy({ server: "socks5://host:1080" })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects http dict", () => {
|
||||||
|
expect(isSocksProxy({ server: "http://host:8080" })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for undefined", () => {
|
||||||
|
expect(isSocksProxy(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveProxyConfig", () => {
|
||||||
|
it("returns empty for undefined", () => {
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig(undefined);
|
||||||
|
expect(proxyOption).toBeUndefined();
|
||||||
|
expect(proxyArgs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns playwright dict for http string", () => {
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig("http://user:pass@proxy:8080");
|
||||||
|
expect(proxyOption).toEqual({ server: "http://proxy:8080", username: "user", password: "pass" });
|
||||||
|
expect(proxyArgs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns playwright dict for http dict", () => {
|
||||||
|
const proxy = { server: "http://proxy:8080", bypass: ".example.com" };
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig(proxy);
|
||||||
|
expect(proxyOption).toEqual(proxy);
|
||||||
|
expect(proxyArgs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns chrome arg for socks5 string", () => {
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig("socks5://user:pass@host:1080");
|
||||||
|
expect(proxyOption).toBeUndefined();
|
||||||
|
expect(proxyArgs).toEqual(["--proxy-server=socks5://user:pass@host:1080"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns chrome arg for socks5 no auth", () => {
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig("socks5://host:1080");
|
||||||
|
expect(proxyOption).toBeUndefined();
|
||||||
|
expect(proxyArgs).toEqual(["--proxy-server=socks5://host:1080"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns chrome arg for socks5h string", () => {
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig("socks5h://user:pass@host:1080");
|
||||||
|
expect(proxyOption).toBeUndefined();
|
||||||
|
expect(proxyArgs).toEqual(["--proxy-server=socks5h://user:pass@host:1080"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reconstructs URL from socks5 dict with auth", () => {
|
||||||
|
const { proxyOption, proxyArgs } = resolveProxyConfig({
|
||||||
|
server: "socks5://host:1080",
|
||||||
|
username: "user",
|
||||||
|
password: "p@ss",
|
||||||
|
});
|
||||||
|
expect(proxyOption).toBeUndefined();
|
||||||
|
expect(proxyArgs.length).toBe(1);
|
||||||
|
expect(proxyArgs[0]).toContain("--proxy-server=socks5://user:p%40ss@host:1080");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes bypass for socks5 dict", () => {
|
||||||
|
const { proxyArgs } = resolveProxyConfig({
|
||||||
|
server: "socks5://host:1080",
|
||||||
|
bypass: ".example.com",
|
||||||
|
});
|
||||||
|
expect(proxyArgs).toContain("--proxy-server=socks5://host:1080");
|
||||||
|
expect(proxyArgs).toContain("--proxy-bypass-list=.example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -113,4 +113,29 @@ describe("puppeteer launch", () => {
|
|||||||
expect(callArgs.args).toContain("--disable-gpu");
|
expect(callArgs.args).toContain("--disable-gpu");
|
||||||
expect(callArgs.args).toContain("--no-first-run");
|
expect(callArgs.args).toContain("--no-first-run");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps SOCKS5 credentials in --proxy-server URL", async () => {
|
||||||
|
const { launch } = await import("../src/puppeteer.js");
|
||||||
|
const browser = await launch({ proxy: "socks5://user:pass@proxy:1080" });
|
||||||
|
|
||||||
|
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
|
||||||
|
expect(callArgs.args).toContain("--proxy-server=socks5://user:pass@proxy:1080");
|
||||||
|
|
||||||
|
// Should NOT set up page.authenticate for SOCKS5
|
||||||
|
const page = await browser.newPage();
|
||||||
|
expect(page.authenticate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reconstructs SOCKS5 dict with auth into --proxy-server URL", async () => {
|
||||||
|
const { launch } = await import("../src/puppeteer.js");
|
||||||
|
const browser = await launch({
|
||||||
|
proxy: { server: "socks5://proxy:1080", username: "user", password: "p@ss" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
|
||||||
|
expect(callArgs.args).toContain("--proxy-server=socks5://user:p%40ss@proxy:1080");
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
expect(page.authenticate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
geoip = ["geoip2>=4.0"]
|
geoip = ["geoip2>=4.0", "socksio>=1.0"] # socksio: SOCKS5 transport for httpx
|
||||||
patchright = ["patchright>=1.40"]
|
patchright = ["patchright>=1.40"]
|
||||||
serve = ["aiohttp>=3.9", "websockets>=12.0"]
|
serve = ["aiohttp>=3.9", "websockets>=12.0"]
|
||||||
dev = ["pytest>=7.0", "pytest-asyncio>=0.23"]
|
dev = ["pytest>=7.0", "pytest-asyncio>=0.23"]
|
||||||
|
|||||||
+127
-15
@@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from cloakbrowser.browser import _build_proxy_kwargs, maybe_resolve_geoip, _parse_proxy_url
|
from cloakbrowser.browser import (
|
||||||
|
_is_socks_proxy,
|
||||||
|
_parse_proxy_url,
|
||||||
|
_resolve_proxy_config,
|
||||||
|
maybe_resolve_geoip,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestParseProxyUrl:
|
class TestParseProxyUrl:
|
||||||
@@ -38,23 +43,30 @@ class TestParseProxyUrl:
|
|||||||
|
|
||||||
|
|
||||||
class TestBuildProxyKwargs:
|
class TestBuildProxyKwargs:
|
||||||
|
"""Tests for _resolve_proxy_config (formerly _build_proxy_kwargs) HTTP path."""
|
||||||
|
|
||||||
def test_none(self):
|
def test_none(self):
|
||||||
assert _build_proxy_kwargs(None) == {}
|
kwargs, args = _resolve_proxy_config(None)
|
||||||
|
assert kwargs == {}
|
||||||
|
assert args == []
|
||||||
|
|
||||||
def test_simple_proxy(self):
|
def test_simple_proxy(self):
|
||||||
result = _build_proxy_kwargs("http://proxy:8080")
|
kwargs, args = _resolve_proxy_config("http://proxy:8080")
|
||||||
assert result == {"proxy": {"server": "http://proxy:8080"}}
|
assert kwargs == {"proxy": {"server": "http://proxy:8080"}}
|
||||||
|
assert args == []
|
||||||
|
|
||||||
def test_proxy_with_auth(self):
|
def test_proxy_with_auth(self):
|
||||||
result = _build_proxy_kwargs("http://user:pass@proxy:8080")
|
kwargs, args = _resolve_proxy_config("http://user:pass@proxy:8080")
|
||||||
assert result == {
|
assert kwargs == {
|
||||||
"proxy": {"server": "http://proxy:8080", "username": "user", "password": "pass"}
|
"proxy": {"server": "http://proxy:8080", "username": "user", "password": "pass"}
|
||||||
}
|
}
|
||||||
|
assert args == []
|
||||||
|
|
||||||
def test_proxy_dict_passthrough(self):
|
def test_proxy_dict_passthrough(self):
|
||||||
proxy_dict = {"server": "http://proxy:8080", "bypass": ".google.com,localhost"}
|
proxy_dict = {"server": "http://proxy:8080", "bypass": ".google.com,localhost"}
|
||||||
result = _build_proxy_kwargs(proxy_dict)
|
kwargs, args = _resolve_proxy_config(proxy_dict)
|
||||||
assert result == {"proxy": proxy_dict}
|
assert kwargs == {"proxy": proxy_dict}
|
||||||
|
assert args == []
|
||||||
|
|
||||||
def test_proxy_dict_with_auth(self):
|
def test_proxy_dict_with_auth(self):
|
||||||
proxy_dict = {
|
proxy_dict = {
|
||||||
@@ -63,8 +75,9 @@ class TestBuildProxyKwargs:
|
|||||||
"password": "pass",
|
"password": "pass",
|
||||||
"bypass": ".example.com",
|
"bypass": ".example.com",
|
||||||
}
|
}
|
||||||
result = _build_proxy_kwargs(proxy_dict)
|
kwargs, args = _resolve_proxy_config(proxy_dict)
|
||||||
assert result == {"proxy": proxy_dict}
|
assert kwargs == {"proxy": proxy_dict}
|
||||||
|
assert args == []
|
||||||
|
|
||||||
|
|
||||||
class TestMaybeResolveGeoip:
|
class TestMaybeResolveGeoip:
|
||||||
@@ -117,6 +130,27 @@ class TestMaybeResolveGeoip:
|
|||||||
mock_geo.assert_called_once_with("http://proxy:8080")
|
mock_geo.assert_called_once_with("http://proxy:8080")
|
||||||
assert tz == "America/New_York"
|
assert tz == "America/New_York"
|
||||||
|
|
||||||
|
@patch("cloakbrowser.geoip.resolve_proxy_geo_with_ip", return_value=("Europe/Berlin", "de-DE", "5.6.7.8"))
|
||||||
|
def test_geoip_socks5_dict_reconstructs_credentials(self, mock_geo):
|
||||||
|
proxy_dict = {"server": "socks5://proxy:1080", "username": "user", "password": "pass"}
|
||||||
|
tz, locale, ip = maybe_resolve_geoip(True, proxy_dict, None, None)
|
||||||
|
mock_geo.assert_called_once_with("socks5://user:pass@proxy:1080")
|
||||||
|
assert tz == "Europe/Berlin"
|
||||||
|
assert locale == "de-DE"
|
||||||
|
|
||||||
|
@patch("cloakbrowser.geoip.resolve_proxy_geo_with_ip", return_value=("Europe/Berlin", "de-DE", "5.6.7.8"))
|
||||||
|
def test_geoip_socks5_dict_no_auth_uses_server(self, mock_geo):
|
||||||
|
proxy_dict = {"server": "socks5://proxy:1080"}
|
||||||
|
tz, locale, ip = maybe_resolve_geoip(True, proxy_dict, None, None)
|
||||||
|
mock_geo.assert_called_once_with("socks5://proxy:1080")
|
||||||
|
|
||||||
|
@patch("cloakbrowser.geoip.resolve_proxy_geo_with_ip", return_value=("Europe/London", "en-GB", "1.1.1.1"))
|
||||||
|
def test_geoip_http_dict_does_not_inline_creds(self, mock_geo):
|
||||||
|
# HTTP dict: credentials stay separate, only server URL passed
|
||||||
|
proxy_dict = {"server": "http://proxy:8080", "username": "user", "password": "pass"}
|
||||||
|
tz, locale, ip = maybe_resolve_geoip(True, proxy_dict, None, None)
|
||||||
|
mock_geo.assert_called_once_with("http://proxy:8080")
|
||||||
|
|
||||||
|
|
||||||
class TestBareProxyFormat:
|
class TestBareProxyFormat:
|
||||||
"""_parse_proxy_url must handle bare 'user:pass@host:port' strings (no scheme)."""
|
"""_parse_proxy_url must handle bare 'user:pass@host:port' strings (no scheme)."""
|
||||||
@@ -149,8 +183,86 @@ class TestBareProxyFormat:
|
|||||||
r = _parse_proxy_url("proxy:8080")
|
r = _parse_proxy_url("proxy:8080")
|
||||||
assert r == {"server": "proxy:8080"}
|
assert r == {"server": "proxy:8080"}
|
||||||
|
|
||||||
def test_build_proxy_kwargs_bare(self):
|
def test_resolve_proxy_config_bare(self):
|
||||||
r = _build_proxy_kwargs("user:pass@proxy:8080")
|
kwargs, args = _resolve_proxy_config("user:pass@proxy:8080")
|
||||||
assert r["proxy"]["username"] == "user"
|
assert kwargs["proxy"]["username"] == "user"
|
||||||
assert r["proxy"]["password"] == "pass"
|
assert kwargs["proxy"]["password"] == "pass"
|
||||||
assert "user" not in r["proxy"]["server"]
|
assert "user" not in kwargs["proxy"]["server"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsSocksProxy:
|
||||||
|
def test_socks5_string(self):
|
||||||
|
assert _is_socks_proxy("socks5://user:pass@host:1080") is True
|
||||||
|
|
||||||
|
def test_socks5h_string(self):
|
||||||
|
assert _is_socks_proxy("socks5h://host:1080") is True
|
||||||
|
|
||||||
|
def test_socks5_uppercase(self):
|
||||||
|
assert _is_socks_proxy("SOCKS5://host:1080") is True
|
||||||
|
|
||||||
|
def test_http_string(self):
|
||||||
|
assert _is_socks_proxy("http://host:8080") is False
|
||||||
|
|
||||||
|
def test_dict_socks5(self):
|
||||||
|
assert _is_socks_proxy({"server": "socks5://host:1080"}) is True
|
||||||
|
|
||||||
|
def test_dict_http(self):
|
||||||
|
assert _is_socks_proxy({"server": "http://host:8080"}) is False
|
||||||
|
|
||||||
|
def test_none(self):
|
||||||
|
assert _is_socks_proxy(None) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveProxyConfig:
|
||||||
|
def test_none(self):
|
||||||
|
kwargs, args = _resolve_proxy_config(None)
|
||||||
|
assert kwargs == {}
|
||||||
|
assert args == []
|
||||||
|
|
||||||
|
def test_http_string_returns_playwright_dict(self):
|
||||||
|
kwargs, args = _resolve_proxy_config("http://user:pass@proxy:8080")
|
||||||
|
assert "proxy" in kwargs
|
||||||
|
assert kwargs["proxy"]["server"] == "http://proxy:8080"
|
||||||
|
assert kwargs["proxy"]["username"] == "user"
|
||||||
|
assert args == []
|
||||||
|
|
||||||
|
def test_http_dict_passthrough(self):
|
||||||
|
proxy = {"server": "http://proxy:8080", "bypass": ".example.com"}
|
||||||
|
kwargs, args = _resolve_proxy_config(proxy)
|
||||||
|
assert kwargs == {"proxy": proxy}
|
||||||
|
assert args == []
|
||||||
|
|
||||||
|
def test_socks5_string_returns_chrome_arg(self):
|
||||||
|
kwargs, args = _resolve_proxy_config("socks5://user:pass@host:1080")
|
||||||
|
assert kwargs == {}
|
||||||
|
assert args == ["--proxy-server=socks5://user:pass@host:1080"]
|
||||||
|
|
||||||
|
def test_socks5_no_auth_returns_chrome_arg(self):
|
||||||
|
kwargs, args = _resolve_proxy_config("socks5://host:1080")
|
||||||
|
assert kwargs == {}
|
||||||
|
assert args == ["--proxy-server=socks5://host:1080"]
|
||||||
|
|
||||||
|
def test_socks5h_returns_chrome_arg(self):
|
||||||
|
kwargs, args = _resolve_proxy_config("socks5h://user:pass@host:1080")
|
||||||
|
assert kwargs == {}
|
||||||
|
assert args == ["--proxy-server=socks5h://user:pass@host:1080"]
|
||||||
|
|
||||||
|
def test_socks5_dict_reconstructs_url(self):
|
||||||
|
proxy = {"server": "socks5://host:1080", "username": "user", "password": "p@ss"}
|
||||||
|
kwargs, args = _resolve_proxy_config(proxy)
|
||||||
|
assert kwargs == {}
|
||||||
|
assert len(args) == 1
|
||||||
|
assert args[0].startswith("--proxy-server=socks5://user:p%40ss@host:1080")
|
||||||
|
|
||||||
|
def test_socks5_dict_ipv6_preserves_brackets(self):
|
||||||
|
proxy = {"server": "socks5://[::1]:1080", "username": "user", "password": "pass"}
|
||||||
|
kwargs, args = _resolve_proxy_config(proxy)
|
||||||
|
assert kwargs == {}
|
||||||
|
assert "[::1]" in args[0]
|
||||||
|
|
||||||
|
def test_socks5_dict_with_bypass(self):
|
||||||
|
proxy = {"server": "socks5://host:1080", "bypass": ".example.com"}
|
||||||
|
kwargs, args = _resolve_proxy_config(proxy)
|
||||||
|
assert kwargs == {}
|
||||||
|
assert "--proxy-server=socks5://host:1080" in args
|
||||||
|
assert "--proxy-bypass-list=.example.com" in args
|
||||||
|
|||||||
Reference in New Issue
Block a user