Commit Graph
15 Commits
Author SHA1 Message Date
Sander Spilleman 9ae36764da feat(stealth): many stealth improvements 2026-06-03 08:21:14 +00:00
SGavrl fd2421940b Perf/cdp fetch cache and cookie alloc (#216)
* perf: cache HTTP clients + selector parsing + exponential wait backoff

Three low-risk wins discovered while reading the hot paths.

1. op_fetch_url cached its reqwest::Client. Previously every JS-side
   fetch(), XHR, and dynamic script load built a fresh client, which
   re-loaded TLS roots and started with an empty connection pool. On
   an asset-heavy page with 30+ subresources this added ~150ms of
   wasted setup with zero connection reuse. The cache is a process-
   wide map keyed by proxy URL (empty string for no-proxy), so the
   first fetch on each proxy pays the build cost once and every
   subsequent fetch reuses the same pool.
   Also bumped pool_idle_timeout to 5 minutes and tcp_keepalive to
   60s so connections stay warm for SPA-heavy workloads that hit
   the same origin dozens of times across a navigation.

2. DomTree::parse_selector now caches parsed selectors in a thread-
   local HashMap (cap 256). querySelector / querySelectorAll re-
   parsed the selector string every call before; batch DOM access
   patterns (an agent scraping a table row-by-row, a framework
   polling for elements) paid the cssparser cost N times.

3. browser_wait_for / browser_wait_for_text now exponentially back
   off (5 -> 10 -> 20 -> ... -> 200 ms). The old fixed 200ms tick
   meant a selector that appeared 30ms after the call still took
   the next full 200ms tick to be detected. Worst case is still
   200ms per check, but the median wait drops sharply for fast
   appearances.

28/28 integration tests pass. Asset-heavy bench (rust-lang/rust,
docs.stripe.com, etc) doesn't regress.

* perf: module loader client cache, fetch concurrency cap, snapshot hide list, cookie alloc

Round two of perf wins after a deep code audit.

1. ES module loader (module_loader.rs) was building a fresh
   reqwest::Client on every dynamic import. Modern SPAs (Next, Vite)
   load 20-50 chunks via dynamic import; each one used to spin up an
   empty connection pool with fresh TLS init. Now reuses the
   process-wide FETCH_CLIENT_CACHE that op_fetch_url already uses
   (the cached_request_client helper). Saves ~30-100ms on chunk-
   heavy first navigations.

2. Script and CSS fetches in execute_scripts used futures::join_all
   with unbounded concurrency. A 100-script page would open 100 TCP
   sockets at once, exhausting the connection pool and the OS
   ephemeral port range. Capped at 16 in-flight (well above the per-
   host pool ceiling real Chrome uses).

3. bootstrap.js __obscura_init() ran Object.keys(globalThis).filter()
   on every page setup to find internals to hide. The set of keys
   is static at snapshot-build time, so we now precompute it once
   (top-level statement just below the function) and __obscura_init
   just iterates the precomputed array. Was 5-40ms per page on heavy
   SPAs (1000+ globals); now ~0.2ms per page.

4. CookieJar::domain_matches allocated 2 lowercase Strings and a
   format! per call. The lookup runs per fetch per cookie domain;
   on a heavily-cookied site with 100 cookies across 30 domains
   that was thousands of allocations per page. Now zero-alloc with
   eq_ignore_ascii_case and char-boundary checks.
2026-06-01 00:00:22 +02:00
SGavrl e7bb0c676d feat(cli): --allow-private-network for localhost development (#33) (#203)
Picks up @Timtech4u's PR #33, which has been waiting since April. The
  design (CLI flag + threading through BrowserContext + http client) is
  theirs and is correct; this is a clean rewrite on top of current main
  because repeated rebase rounds had been reverting unrelated work that
  landed in the meantime.

  Issue #4 blocked all loopback / RFC1918 / link-local fetches to prevent
  SSRF. The only escape was OBSCURA_ALLOW_PRIVATE_NETWORK=1, which is
  process-wide and clunky in pipelines. Multiple users (@Timtech4u in
  the original PR, then @duartemvix and @necrogami in the comment
  thread) reported they cannot use obscura for development against
  http://localhost:N without it.

  Adds --allow-private-network as a global CLI flag, works on fetch,
  serve, and scrape. Threads through BrowserContext::with_storage_and_network
  and the http client's validate_url. CdpContext gets a new_full
  builder and obscura_cdp::start_with_full_serve_options entry point so
  'obscura serve --allow-private-network' lets Puppeteer/Playwright
  navigate to localhost too. Default remains blocked.

  Backward compat: the env var still works (OR'd with the flag inside
  validate_url). All existing constructors and entry points unchanged.

  Verified end-to-end against a local http.server on 127.0.0.1: fetch
  and CDP Page.navigate both blocked by default, both succeed with the
  flag. Public URLs unaffected. Integration suite 28/28, full workspace
  cargo test all green.
2026-05-29 17:53:23 +00:00
906dcfcde1 feat: cookie jar persistence + --storage-dir flag + fix HTTP cookie header filtering (#114)
* feat: cookie jar persistence + storage-dir + filtering fix (clean v4)

* fix: resolve build errors, add storage_dir param throughout server chain

* fix: storage_dir end-to-end, plus two test-literal field additions

  Five issues found while testing PR 114 end-to-end.

  Build fixes (two test literals out of sync with new struct fields):
  - crates/obscura-net/src/cookies.rs: CookieInfo literal in
    test_domain_matches_subdomain_without_leading_dot was missing the
    same_site and expires fields added in #170.
  - crates/obscura-cli/src/main.rs: Command::Fetch literal in
    matcher_still_uses_fetch_variant was missing the new storage_dir
    field. Without these cargo test --workspace would not compile.

  run_fetch was branching between BrowserContext::with_storage (which
  hardcodes proxy=None, stealth=false, user_agent=None) and
  with_options (which has no storage_dir slot), so --storage-dir +
  --proxy + --stealth silently dropped the proxy and stealth flags.
  Same bug SGavrl flagged on dispatch.rs in the previous round, just on
  the other code path. Switched to with_storage_full which carries all
  four. Verified the fetch now errors out against an invalid proxy.

  run_fetch --eval short-circuited the function before
  context.save_cookies() ran, so cookies set during an eval session
  were never persisted to {storage_dir}/cookies.json. Moved the save
  above the return.

  obscura serve loaded cookies on startup but never saved them
  anywhere, so a session that set cookies via Network.setCookie or
  navigation dropped them on shutdown. Subscribed to Ctrl-C once in
  put a single save_cookies() after the loop so both the Ctrl-C path
  and the channel-closed path persist cookies. Verified end-to-end.

---------

Co-authored-by: zhangyang-crazy-one <[email protected]>
Co-authored-by: SGavrl <[email protected]>
2026-05-26 11:09:10 +02:00
SGavrl 8a8c767338 fix(net): use bundled webpki roots in stealth client (#184) (#185)
set_default_paths only resolves OpenSSL's compile-time CA dirs, which
  exist on Linux but are empty on Windows. The stealth client's cert store
  ended up empty there and every TLS handshake failed with
  CERTIFICATE_VERIFY_FAILED inside BoringSSL handshake.cc.

  CertStore::default() uses wreq's bundled webpki-root-certs (Mozilla's
  trust list), which behaves the same on every platform and removes the
  dependency on a system CA store.
2026-05-25 19:24:56 +02:00
Ben Younes 1da6032639 feat(cdp): implement Network.setCookie + deleteCookies, full field fidelity (#170)
* feat(cdp): implement Network.setCookie + deleteCookies, full field fidelity

Closes #168.

Adds the missing Network.setCookie (singular) and Network.deleteCookies
methods and upgrades the existing Network.setCookies/getCookies handlers
so sameSite, expires, httpOnly, secure, and the url-fallback parameter
all round-trip correctly.

Network and Storage cookie handlers share a single parse_cdp_cookie /
parse_delete_cookies_params helper to avoid drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(cdp): fall back to default cookie jar, add Network.getAllCookies

Addresses @SGavrl review on PR #170:

- Network.{set,get,delete}Cookie(s)/clearBrowserCookies now resolve the
  cookie jar via a shared helper that prefers the session page's jar and
  falls back to ctx.default_context.cookie_jar when no target has been
  attached. Mirrors how Storage.setCookies already routes when there is
  no session, so Puppeteer/Playwright flows that issue cookie methods
  before Target.attachToTarget no longer hit "No page".
- Add Network.getAllCookies (separate CDP method per spec); aliases the
  existing getCookies handler so both arms return the full jar.

Tests: 6 new #[tokio::test] cases in domains::network::tests cover the
no-session fallback for set/get/setMany/delete/clear and the new
getAllCookies arm. RED on prod-reverted file (6/6 fail with "No page" /
"Unknown Network method: getAllCookies"), GREEN on patched
(22 -> 30 obscura-cdp lib tests, obscura-net 22/22 baseline preserved).
2026-05-25 17:58:48 +02:00
SGavrl 52fcf2a35a fix(net,browser): detect response charset, decode non-UTF-8 pages (#182)
obscura was calling String::from_utf8_lossy on every response body, which
silently corrupts every non-UTF-8 page (GBK, Big5, Shift-JIS, Windows-125x,
EUC-KR, ISO-8859-x). Whole language families came through as replacement
characters.

Add an encoding module (obscura-net::encoding) that picks the decoder
the way real browsers do per HTML5 § 8.2.2.4:

  1. HTTP Content-Type charset=... parameter (authoritative when present)
  2. <meta charset> or <meta http-equiv=Content-Type> sniffed from the
     first 1KB of the body (HTML only)
  3. UTF-8 fallback

Use it in:
  - obscura-net Response::text (signature changed: String, not Result)
  - obscura-browser page.rs HTML body decode (the primary scrape path)
  - obscura-browser page.rs JS body decode
  - obscura-browser page.rs CSS body decode

For JS / CSS we deliberately skip the meta sniff: those bodies are not
HTML and any '<meta charset=...>' substring inside them is not a charset
declaration.

Adds 7 unit tests for the encoding module covering header precedence,
quoted charset values, meta charset sniff, http-equiv form, the 1KB
sniff window, and a round-trip decode of GBK bytes. Adds a Python
feature test exercising file:// + meta charset and a live HTTP server
serving Shift_JIS with the wrong meta tag.

Closes #113.
2026-05-24 20:14:53 +00:00
Alexandr KondakovandSGavrl 6dd1e4e65f Fix CDP click submit navigation parity (#69)
* Fix Runtime.evaluate awaitPromise settling

* Fix DOM event navigation side effects

* Fix CDP click-triggered navigation parity

* Strengthen CDP inline script click parity test

* Handle positions-like submit URLs in CDP parity


Co-authored-by: SGavrl <[email protected]>
2026-05-15 20:38:54 +00:00
Ben Younes f3c783f6f5 fix(net,js): route op_fetch_url + ES module loader through proxy_url (#139) (#151)
Fixes the remaining two paths from #139: JS-level fetch()/XHR
(op_fetch_url) and dynamic ES module imports (ObscuraModuleLoader) both
built their reqwest clients with `Client::builder().build()` — no
proxy — so every JS-initiated network call silently bypassed
BrowserContext.proxy_url. (The third path, StealthHttpClient, was
already fixed in main: page.rs:61 uses with_proxy.)

Changes:

* obscura-net: expose ObscuraHttpClient::proxy_url() so callers outside
  the crate can route their own reqwest requests through the same
  upstream proxy. The field was already there — just lacked an accessor.

* obscura-js/module_loader: add `proxy_url: Option<String>` and
  `with_proxy(base_url, proxy_url)`. ObscuraModuleLoader::new keeps the
  pre-#139 direct-connection behaviour. load() builds a reqwest client
  per import, threading the proxy and bubbling up "Invalid module proxy"
  on bad URLs.

* obscura-js/ops.rs: replace the process-wide
  `static SHARED_HTTP_CLIENT: OnceLock<reqwest::Client>` (which could
  never be reconfigured after first call) with a per-request
  `build_request_client(proxy_url)` that pulls the configured proxy out
  of SharedState.http_client.proxy_url() each call. Per-request build
  cost is ~1ms vs the network round-trip, and we no longer have a
  global singleton that lies about its config.

* obscura-js/runtime: add ObscuraJsRuntime::with_base_url_and_proxy.
  with_base_url delegates to it with None so existing tests / callers
  are unaffected.

* obscura-browser/page.rs: thread context.proxy_url through to
  with_base_url_and_proxy when constructing the JS runtime.
2026-05-14 08:25:55 +00:00
GapesteinandSixSeven-Labs 0054c177c2 fix(stealth): gate wreq's prefix-symbols feature to Linux/Android (#39) (#106)
Co-authored-by: SixSeven-Labs <[email protected]>
2026-05-09 17:54:47 +02:00
Adam Ma a8b1ec0968 chore: silence unused import/variable/dead-code warnings (#101) (#126)
- obscura-net/cookies.rs: #[allow(dead_code)] on CookieEntry.same_site
  (parsed today, kept for future SameSite enforcement).
- obscura-js/runtime.rs: drop vestigial BOOTSTRAP_JS const; bootstrap.js
  is already embedded into the V8 snapshot via build.rs.
- obscura-js/ops.rs: prefix the four unused fields in
  InterceptResolution::Continue with _.
- obscura-cdp/dispatch.rs: drop unused InterceptResolution import.
- obscura-cdp/domains/fetch.rs: drop unused Arc / Mutex / CdpEvent imports.
- obscura-cdp/domains/input.rs: prefix _button / _click_count; CDP
  parses them but the JS click stub is hardcoded for now.
- obscura-cdp/server.rs: prefix _processor_handle and _ctx; drop dead
  initial Ok(())/None for navigate_result/page_back - they are
  unconditionally overwritten in the only loop break arm.

cargo build --workspace: 0 warnings.
cargo test --workspace: 113 passed.
2026-05-09 17:52:32 +02:00
MrBob 9bc2451a00 fix: match leading-dot cookie domains (#100) 2026-05-03 17:10:34 +02:00
F0Rextasy 64c22e8d87 fix(net): remove unused IpAddr import (#97) 2026-05-03 16:43:33 +02:00
ben1009 21136ca819 feat: support file:// URL scheme for local HTML testing
Allow loading local HTML/CSS/JS files via the file:// protocol,
enabling Obscura to be used for testing static sites without
running a local HTTP server.

Changes:
- Allow "file" scheme in validate_url() and validate_fetch_url()
- Add fetch_file_url() to read local files and return synthetic
  HTTP responses with appropriate content-type headers
- Skip private IP / localhost checks for file:// URLs

Closes #16
2026-04-25 00:18:31 +08:00
h4ckf0r0day 8749725b45 Initial Release 2026-04-13 12:30:28 +02:00