Commit Graph

454 Commits

Author SHA1 Message Date
SGavrl
7440b534f3 fix(cdp): stub setDownloadBehavior, detachFromTarget, activateTarget
These Chrome CDP methods logged "Unknown method" warnings. obscura runs
one page per session and does not download to disk, so there is nothing
to do, but clients that call them should get a success ack instead of a
warning. Added as no-op stubs.

Refs #340.
2026-07-01 06:55:37 +00:00
SGavrl
630f9894f6 fix(cdp): expose main-resource response body and real mime on navigation
Navigating straight to an image, PDF, or other resource could not read the
main response body over CDP, and Page.frameNavigated always reported
text/html.

Two problems: the main resource's body was stored under its internal
request id, but the client sees it as the navigation loaderId (Chrome's
requestId === loaderId convention), so Network.getResponseBody(loaderId)
missed. And binary bodies were stored as UTF-8-lossy text, corrupting the
bytes. Alias the stored body to the loaderId, store non-text-like content
types (images, PDF, octet-stream) base64, and report the response's real
Content-Type in frameNavigated. HTML pages are unchanged.

Fixes #340.
2026-07-01 06:55:37 +00:00
MrBeanDev
ed75730e2d feat(js): implement WebCrypto SubtleCrypto secret-key algorithms
crypto.subtle only implemented digest. sign returned a fixed 32-byte
buffer, verify always returned true, encrypt and decrypt threw, and
generateKey, importKey and deriveBits handed back placeholder data.
Pages that use WebCrypto (anti-bot probes, PKCE and other auth flows,
client-side crypto) silently got wrong results.

Implement the secret-key half of WebCrypto on RustCrypto, following the
existing op_subtle_digest pattern:

- HMAC sign and verify (SHA-1/256/384/512)
- AES-GCM, AES-CBC and AES-CTR encrypt and decrypt
- PBKDF2 and HKDF deriveBits and deriveKey
- raw and symmetric JWK (oct) importKey and exportKey, generateKey,
  wrapKey and unwrapKey
- getRandomValues and randomUUID now draw from the OS CSPRNG instead of
  Math.random, so they are uniform across typed-array widths and no
  longer a fingerprinting tell

Unsupported algorithms (RSA, ECDSA, ECDH) and key formats (pkcs8, spki)
throw NotSupportedError instead of returning fake data. The crypto runs
in stateless Rust ops; the bootstrap.js shim only marshals bytes and
normalizes algorithm parameters, so the native fast paths are untouched
and there is no cost unless a page calls crypto.

Verified against RFC 4231 (HMAC), the NIST GCM vectors, NIST SP800-38A
(AES-CTR), RFC 6070 (PBKDF2) and RFC 5869 (HKDF), plus roundtrip and
tag/padding tamper-rejection checks.
2026-07-01 12:11:55 +05:30
SGavrl
d5d9b7788f fix(js): DOM conformance for checkbox value and NodeList
Two standard-conformance fixes surfaced by #335 (the issue itself is a commercial anti-bot challenge and out of scope; these are the legitimate browser-compatibility gaps from it).

• checkbox/radio value: an input with no value attribute returns "on" in a real browser; obscura returned "". An explicit value attribute still wins.
• NodeList: childNodes returned a plain Array, and NodeList extended Array, so Array.isArray(list) was true and Object.prototype.toString reported "[object Array]". A real browser reports "[object NodeList]" with Array.isArray false. NodeList is now its own type with the array-like surface scripts use (indexed access, length, item, forEach, entries/keys/values, iteration), and childNodes goes through it.

Verified: checkbox/radio default "on" (explicit value preserved), childNodes and querySelectorAll are real NodeLists (constructor "NodeList", "[object NodeList]", Array.isArray false), and spread / for..of / Array.from still work. Obstacle course 33/33, obscura-dom 33/33.

Refs #335.
2026-06-30 15:27:05 +00:00
SGavrl
25ce541ec8 fix(js): make NodeList a real type and childNodes a NodeList
childNodes returned a plain Array (constructor "Array"), and NodeList
extended Array, so Array.isArray(list) was true and
Object.prototype.toString reported "[object Array]". A real browser
reports "[object NodeList]" and Array.isArray is false, which
feature-detection and fingerprinting scripts check.

NodeList is now its own type with the array-like surface scripts use
(indexed access, length, item, forEach, entries/keys/values, iteration),
and childNodes goes through it. Verified spread, for..of, and Array.from
still work.

Refs #335.
2026-06-30 15:18:21 +00:00
SGavrl
a16e8d4fe3 fix(js): default checkbox and radio value to "on"
A checkbox or radio input with no value attribute returns "on" in a real
browser; obscura returned the empty string. Page scripts that read
input.value to build form submissions (and browser-environment checks)
saw the wrong default. An explicit value attribute still wins.

Refs #335.
2026-06-30 15:18:21 +00:00
SGavrl
1826570927 fix(cli): bind multi-worker load balancer to --host
The single-worker serve path honored --host, but run_multi_worker_serve hardcoded the load balancer to 127.0.0.1. With --host 0.0.0.0 and --workers >1 (the common Docker setup) the balancer bound loopback only, so the mapped port was refused from outside the container. Bind the balancer to the requested host. Workers stay on loopback and are reached only by the balancer.

Closes #336.
2026-06-30 08:10:54 +00:00
SGavrl
9837262af8 fix(cli): bind multi-worker load balancer to --host
The single-worker serve path honored --host, but run_multi_worker_serve
hardcoded the load balancer to 127.0.0.1. With --host 0.0.0.0 and
--workers >1 (the common Docker setup) the balancer bound loopback only,
so the mapped port was refused from outside the container. Bind the
balancer to the requested host. Workers stay on loopback and are reached
only by the balancer.

Fixes #336.
2026-06-30 08:05:48 +00:00
SGavrl
50ef1eecbd fix: SVGAnimatedString, :has() selectors, and cookie persistence on serve 2026-06-29 11:00:22 +00:00
SGavrl
b63125faed cdp: flush cookies on SIGTERM in serve (#333)
serve --storage-dir saved cookies only on Ctrl-C (SIGINT). docker stop
and kill send SIGTERM, which was not caught, so the save_cookies() call
at shutdown never ran and cookies were lost on restart. Watch SIGTERM as
well as Ctrl-C on Unix.
2026-06-29 10:48:20 +00:00
SGavrl
2840b08b56 dom: support :has() selectors (#331)
querySelectorAll / --selector silently returned nothing for :has().

Two fixes were needed:
- Enable :has() parsing: the selectors crate gates it on the parser's
  parse_has() (default false), so a:has(p) failed to parse and the error
  was swallowed into an empty match set.
- Fix OpaqueElement stability: opaque() was keyed off the transient
  DomElement (which is Copy and gets a fresh stack address each traversal
  step), so the same node produced a different id every call. :has anchor
  matching compares the anchor's opaque against the element reached by
  walking up the tree, so it never matched. Key opaque() off the node's
  stable slot in the tree's Vec instead.
2026-06-29 10:48:20 +00:00
SGavrl
117415848e js: implement SVGAnimatedString for SVG className and href (#330)
SVG className and href reflected as plain strings, so el.className.animVal
and el.href.animVal were undefined, which made new URL(svgEl.href.animVal)
throw and broke any SVG-aware script. Add an SVGAnimatedString class
(baseVal/animVal both read the live attribute; baseVal writable) and make
the className and href getters return it for SVG-namespace elements. href
falls back to the legacy xlink:href. HTML elements are unchanged.
2026-06-29 10:48:20 +00:00
Rezo
260c4c01fa Add fetch body serialization support for Blob, ArrayBuffer, and TypedArray (#329)
* Add fetch body serialization support for Blob, ArrayBuffer, and TypedArray

* js: fix Blob fetch body serialization

The Blob branch passed the Blob to _bodyToUint8Array, which did not know
about obscura's Blob (its data lives in _bytes) and fell through to
encoding String(blob), so a Blob body was sent as "[object Blob]".
Teach _bodyToUint8Array about _bytes, and build the binary string with
the loop-based _bytesToBinaryString instead of String.fromCharCode.apply
so large blobs do not overflow the argument limit.

---------

Co-authored-by: SGavrl <277843151+SGavrl@users.noreply.github.com>
2026-06-26 20:43:19 +00:00
h4ckf0r0day
505738450a Update README.md 2026-06-26 17:18:24 +02:00
h4ckf0r0day
a1ac12f28d Add banner picture 2026-06-26 17:05:58 +02:00
Gabriel
f196b35b39 net: validate Set-Cookie Domain against the origin
CookieJar::set_cookie and set_cookie_from_js started from the request host but
blindly overwrote it with any Domain attribute, so a response from
attacker.test could plant a cookie scoped to victim.test, which was then sent
on later requests to victim.test. The same sink backs document.cookie, HTTP
Set-Cookie, and CookieStore::set.

Add resolve_cookie_domain: with no Domain the cookie is host-only (exact host
only); a Domain attribute is honored only when it domain-matches the origin
(equal or a parent domain) and is not an obvious public suffix, otherwise the
attribute is ignored and the cookie is stored host-only on the origin. Track
host_only on CookieEntry so host-only cookies are not sent to subdomains.

A full public suffix list is not bundled, so multi-label public suffixes
(co.uk, github.io) are not rejected; the domain-match check still blocks the
reported cross-domain attack, and single-label suffixes (com, local) are.

Tests cover the cross-domain rejection for both HTTP and document.cookie,
public-suffix rejection, host-only subdomain scoping, and that legitimate
parent-domain cookies still work.

Co-authored-by: DavidCarliez <DavidCarliez@users.noreply.github.com>
2026-06-25 22:05:06 +02:00
Gabriel
3a5d977940 Docs/security and contributing
Docs/security and contributing
2026-06-24 23:29:18 +00:00
SGavrl
5db0dc0e01 docs: document the interception API and refresh the skill
The request interception API (#306) and the global --stealth flag are
merged, so:

- Use-as-a-Rust-library.md: add an "Intercept requests" section covering
  on_request/on_response, enable_interception (Continue/Fulfill/Fail),
  and add_preload_script, with examples.
- Intercept-and-modify-requests.md: point embedders at the native API.
- AGENTS.md: --stealth is now global (all subcommands incl. scrape, via
  OBSCURA_STEALTH for workers); note the interception API on the obscura
  crate and that op_fetch_url rewrites must stay behind validate_fetch_url.
- skills/obscura/SKILL.md: fix the build note (stealth is not a default
  feature), correct "randomized" to a consistent fingerprint, add the
  global --stealth flag, --dump assets/cookies, and an interception note.
2026-06-24 23:27:19 +00:00
SGavrl
4e500a2aad docs: update guides for global --stealth, --dump cookies, fetch assets
- --stealth is now a global flag: document it on every subcommand
  including scrape, and that it works before or after the subcommand.
- Correct the stealth description: it presents a consistent Chrome
  fingerprint (TLS, headers, JS surfaces), not a randomized one, and
  scripted fetch()/XHR share that fingerprint.
- Add --dump cookies to the CLI reference and the extract-data guide.
- Note --dump assets now also lists fetch()/XHR request URLs.
2026-06-24 23:22:16 +00:00
SGavrl
fa6bc6c134 docs: add issue templates (bug report, feature request)
GitHub issue forms that enforce the fields needed to triage: obscura
version, OS and architecture, how it is being used, stealth mode, a
reproduction, expected vs actual, and whether headless Chrome behaves
the same. config.yml routes questions and API proposals to Discussions
and security reports to the private advisory flow.
2026-06-24 23:22:16 +00:00
SGavrl
ae3fca3366 docs: correct --stealth flag scope in AGENTS.md
--stealth is a per-subcommand flag (on fetch, serve, and mcp; not
scrape), not a global one. Only --proxy and --allow-private-network
are global.
2026-06-24 23:22:16 +00:00
SGavrl
ff39fb6fef docs: add SECURITY.md and CONTRIBUTING.md
SECURITY.md: private reporting via GitHub advisories with an email
fallback, a what-to-include checklist, response and disclosure policy, a
scope section (what is and is not a vulnerability, including stealth and
opt-in flags), third-party dependency handling, and the in-process
security model (V8 watchdog, CLI deadline, panic safety, default SSRF
egress block) with the note that it complements rather than replaces
OS-level isolation. Modeled on the Electron, Node, and Puppeteer policies.

CONTRIBUTING.md: code of conduct, an issue-first submission policy, build
and test workflow (nextest not cargo test, the 33/33 obstacle course
gate), the pre-PR checklist with performance as a hard constraint,
commit and PR conventions, and bug-report expectations. Points at
AGENTS.md for the deeper engine details instead of duplicating them.
2026-06-24 23:22:16 +00:00
Gabriel
a2d3e5b136 Feat/page interception api
• add_preload_script(&str) runs a script before any of the page's own <script> tags (the CDP Page.addScriptToEvaluateOnNewDocument contract). Call it before navigation.
• enable_interception() -> UnboundedReceiver<InterceptedRequest> yields every JS fetch()/XHR request. Resolve each via its resolver with InterceptResolution::{Continue, Fulfill, Fail} to pass, mock, or block it. Continue can rewrite url/method/headers/body.
• on_request(cb) / on_response(cb) register passive callbacks fired for every request and response (navigation plus JS fetch()/XHR), the response callback including the body. This is the main path for capturing SPA API payloads.
• op_fetch_url (the op backing JS fetch()/XHR) now invokes the on_request/on_response callbacks and honors Continue overrides. Before this they fired only for navigation-level requests. The stealth fetch path is wired too.
• Interception types (InterceptedRequest, InterceptResolution, RequestInfo, RequestCallback, ResponseCallback, ResourceType, Response) are re-exported up through obscura-browser to the public obscura crate.
• A Continue URL rewrite is re-validated against the SSRF/private-network gate (validate_fetch_url), the same as a redirect, so a rewrite can't reach an internal address the original request was blocked from.
• The callbacks are gated behind an emptiness check, so there is no per-request cost when no interceptor is registere
2026-06-24 23:11:43 +00:00
SGavrl
bd39512ffb api: re-validate intercept URL rewrites against the SSRF gate
A Continue resolution that rewrites the request URL applied the new URL
after validate_fetch_url had already run on the original, so a rewrite
could reach a private or internal address the original request was
blocked from. Run the overridden URL back through validate_fetch_url,
matching how redirects are re-validated, and return a blocked response
if it fails.

Also fix a doc comment that ended up attached to the new fetch_response
helper instead of stealth_fetch_all.
2026-06-24 23:06:54 +00:00
SGavrl
c19a21c142 api: honor interception Continue overrides and fix pre-navigation enable
op_fetch_url ignored the url/method/headers/body fields on
InterceptResolution::Continue, so the interception channel could pass, mock, or
block a request but not rewrite it. Apply them: shadow url/method/body and
derive custom_headers from the override after the channel resolves Continue.

Also fix enable_interception() called before the first navigation: init_js
propagated intercept_tx to the new runtime but not intercept_enabled, so the
runtime started with interception disabled and op_fetch_url never intercepted.
Propagate intercept_enabled in init_js as well.

Add a test that rewrites a fetch's url via Continue and asserts the rewritten
response is captured.
2026-06-24 21:51:21 +00:00
SGavrl
1f5066fc30 api: add request/response interception API to Page (#306)
Expose the interception infrastructure that already existed internally but was
private, so embedders can intercept, observe, mock, or block JS fetch()/XHR
without forking or unsafe casts.

On obscura::Page:
- add_preload_script: inject JS before any page <script> (CDP
  Page.addScriptToEvaluateOnNewDocument). The field already ran the scripts;
  this only adds the method.
- enable_interception: returns a receiver for every JS fetch()/XHR request;
  resolve each through its resolver with InterceptResolution::{Continue,
  Fulfill, Fail} to pass, mock, or block (CDP Fetch.enable equivalent).
- on_request / on_response: passive callbacks. on_response is the main path for
  capturing API response payloads from SPAs.

on_request/on_response previously fired only for navigation; op_fetch_url now
invokes them for JS fetch()/XHR too (both reqwest and stealth paths), gated on
a non-empty callback vector so the common no-callback case pays nothing.

Re-export InterceptedRequest, InterceptResolution, RequestInfo, ResourceType,
RequestCallback, ResponseCallback, Response from the obscura crate.

Integration test tests/interception.rs exercises all three against a local echo
server.
2026-06-24 21:36:01 +00:00
Gabriel
12b927f476 Merge pull request #325 from h4ckf0r0day/feat/global-stealth-flag
cli: make --stealth a global flag, available on every subcommand
2026-06-24 13:41:30 +00:00
SGavrl
8c14c4393d cli: make --stealth a global flag, available on every subcommand
--stealth was a per-subcommand flag defined separately on fetch, serve,
and mcp, and missing from scrape entirely. You could not enable stealth
for parallel scraping, and `obscura --stealth fetch` (flag before the
subcommand) errored.

Promote --stealth to a single global flag like --proxy, so it is valid
before or after any subcommand and applies uniformly to fetch, serve,
scrape, and mcp. scrape now forwards it to its workers via OBSCURA_STEALTH
(mirroring OBSCURA_PROXY), and the bare `obscura` default CDP server
honors it too.

Verified: navigating directly to a blocklisted tracker renders 55503
chars without stealth and 39 (blocked) with it, identically on fetch and
scrape. Obstacle course 33/33.
2026-06-24 09:46:10 +00:00
Gabriel
d2206fea84 stealth: fingerprint consistency, scripted-request TLS, FormData
**Fingerprint consistency (stealth)** The stealth TLS, navigator, and WebGL surfaces previously disagreed. The wreq client reported Windows Chrome but navigator reported Linux, and WebGL returned Direct3D11 renderers for a Linux UA — a combination no real browser ever produces. Everything now presents one consistent Windows Chrome 145 identity: the HTTP User-Agent header, sec-ch-ua-platform, navigator.userAgent, navigator.platform, userAgentData, and WebGL renderer all agree. userAgentData brands are derived from the Chrome major version via Chromium's GREASE algorithm instead of a single hardcoded token.

**sec-ch-ua client hints on the default (non-stealth) path** The plain navigation client hardcoded `sec-ch-ua-platform: "Linux"` and Chrome 149 brands regardless of which profile was selected. Both headers are now derived from the actual User-Agent using the same GREASE algorithm, so non-stealth requests are also internally consistent.

**Route scripted fetch()/XHR through the stealth TLS client** In stealth mode, JS `fetch()` and `XMLHttpRequest` were still going through the plain reqwest/rustls stack, so subresource requests (sensors, WAF challenge verify calls) arrived with a non-browser TLS fingerprint. They now share the navigation's Chrome TLS fingerprint and sec-ch-ua client hints. The non-stealth path is unchanged.

**FormData serialization in fetch() — the booking.com fix** Passing a `FormData` as a `fetch()` or `XHR.send()` body sent the literal string `"[object Object]"` instead of a multipart payload. booking.com's AWS WAF challenge POSTs its proof-of-work solution to `/mp_verify` as FormData, so the verify always failed and no `aws-waf-token` was issued. `FormData` is now serialized to a proper `multipart/form-data` body with a generated boundary; `URLSearchParams` gets `application/x-www-form-urlencoded`. XHR routes through `fetch()` so it is covered too.

**navigator.permissions.query defaults** `permissions.query` returned `"granted"` for every permission. Real Chrome defaults camera, microphone, geolocation, and midi to `"prompt"`. Fixed.

**SHA-512/224 and SHA-512/256 in crypto.subtle.digest** Added FIPS 180-4 truncated SHA-512 variants to the Rust op and the JS allowlist. WAF challenge scripts that use these algorithms now get real output instead of a NotSupportedError.

**location updates synchronously on navigation, Blob-URL Worker race fixed**
2026-06-23 18:14:44 +00:00
SGavrl
8582fcceca Merge stealth-overhaul into main
Resolved conflicts:
- page.rs: keep both set_blocked_urls (#315) and stealth client wiring
- ops.rs: keep SHA-512/224 + SHA-512/256 from stealth branch; keep
  the unreachable catch-all (vec![]) from #314 instead of silent
  SHA-256 fallback; updated comment accordingly
- bootstrap.js: extend the digest algorithm allowlist to include
  SHA-512/224 and SHA-512/256 to match the Rust additions
2026-06-23 18:05:55 +00:00
SGavrl
768cbc056b net: send non-stealth navigation headers in Chrome's order
The default HTTP path emitted headers with user-agent first and sec-fetch-dest
before sec-fetch-site, an order no real browser produces and a known
fingerprint signal. Reorder the explicit headers to Chrome's top-level
navigation order (sec-ch-ua family, upgrade-insecure-requests, user-agent,
accept, sec-fetch-site/mode/user/dest, accept-language). reqwest still appends
accept-encoding and host after these, so accept-encoding lands after
accept-language instead of before it; the rest matches Chrome exactly.
2026-06-23 17:40:16 +00:00
SGavrl
db24d7a18c js: prompt for sensitive permissions in navigator.permissions.query
permissions.query returned "granted" for every permission except notifications.
Real Chrome defaults camera, microphone, geolocation, and midi to "prompt", so
answering "granted" for them is a bot tell. Return "prompt" for those (keeping
notifications in sync with Notification.permission) and "granted" for the rest.
2026-06-23 17:35:37 +00:00
SGavrl
f7b3ff5d75 net: derive sec-ch-ua client hints from the UA on the default HTTP path
The non-stealth navigation path hardcoded sec-ch-ua-platform "Linux" and a
fixed sec-ch-ua using Chrome 149's GREASE token, regardless of the selected
profile. With the default Windows Chrome 143 profile that meant a Windows UA
and navigator.userAgentData traveling with a Linux sec-ch-ua-platform header
and mismatched brands, a cross-layer inconsistency a site can check at a
glance. Derive both headers from the profile User-Agent using Chromium's
per-major GREASE algorithm, so the HTTP client hints agree with the JS surfaces.
2026-06-23 17:35:37 +00:00
Gabriel
0913be86dc cdp: make Backspace and typing honor text selection (#316) (#321)
Backspace always trimmed the last character and typing always appended,
ignoring any selection. So "select-all, then Backspace to clear" left
all but one character, and typing over a selection accumulated text
instead of replacing it.

Backspace now deletes the selected range when one is set, and typed
text replaces it, matching the browser. selectionStart is null during
ordinary typing, so the previous append/trim behavior is preserved when
no selection is tracked.

Pairs with the triple-click select-all in #320 to make the common
"triple-click, Backspace, retype" clear pattern work end to end. Also
fixes the same pattern when a page sets the selection via select() or
setSelectionRange().
2026-06-23 13:28:35 +02:00
zhangyang-crazy-one
5d01b5eb6a fix(cdp): implement triple-click select-all for text inputs (#316 follow-up) (#320)
Passing detail:clickCount to MouseEvent is necessary but not sufficient
for triple-click to select all text. The select-all-on-triple-click is a
browser native behavior that synthetic MouseEvent dispatch doesn't trigger.

Add manual selection after click dispatch: when clickCount >= 3 and the
target is an INPUT or TEXTAREA, set selectionStart=0 and
selectionEnd=value.length.
2026-06-23 13:26:18 +02:00
SGavrl
3eb28da5c6 js: serialize FormData bodies to multipart/form-data in fetch
fetch() and XHR did String(body), so passing a FormData as the request body
sent the literal "[object Object]" and lost the multipart payload along with
its boundary. Servers expecting multipart/form-data replied 400 "Invalid
boundary for multipart/form-data", which is what blocked booking.com: the AWS
WAF challenge POSTs its proof-of-work solution to /mp_verify as FormData, so
the verify always failed and no aws-waf-token was issued. Serialize FormData to
a multipart/form-data body with a generated boundary and set the matching
Content-Type, the way a real browser does; URLSearchParams bodies get
application/x-www-form-urlencoded. XHR.send routes through fetch() so it is
covered too. With this, obscura --stealth renders the real booking.com landing
page the same as headless Chrome.
2026-06-23 10:52:57 +00:00
SGavrl
402de261aa js: fix Blob-URL Worker race and update location synchronously on navigation
URL.createObjectURL stored blob content asynchronously (blob.text().then) but
the Worker constructor read __blobStore synchronously, so new Worker(blobURL)
in the same tick raced the store, fell through to fetch(blobURL), and failed
with net::ERR_FAILED. This broke Blob-URL workers including the AWS WAF
challenge proof-of-work worker. Store synchronously from Blob._bytes, which the
obscura Blob materializes in its constructor.

location.reload was a no-op, so challenges that reload after setting a token
cookie never re-fetched. It now navigates like assign/replace.

location.href/assign/replace/reload cleared __virtualUrl immediately, so a
script that set location.href and re-read it saw the old URL until the async
navigation completed; real browsers update location.href synchronously. Preview
the resolved target in __virtualUrl and clear it in __obscura_init once the
navigation completes, so immediate reads match and redirects still win.
2026-06-23 10:28:42 +00:00
SGavrl
2e0981ef3f stealth: route scripted fetch()/XHR through the wreq client
op_fetch_url backed JS fetch() and XHR with a plain reqwest/rustls client, so
in stealth mode scripted requests carried a non-browser TLS fingerprint and
none of the Chrome client hints the main navigation sends. Bot managers that
score subresource requests (the AWS WAF challenge verify call, Akamai sensors)
read that as a script and reject it. Add StealthHttpClient::send_single and a
stealth_fetch_all path that op_fetch_url uses when a stealth client is
installed, preserving the redirect, SSRF, and CORS handling so JS-level
requests share the navigation's Chrome TLS fingerprint and sec-ch-ua client
hints. The reqwest path is unchanged for non-stealth. Adds a stealth feature to
obscura-js gated on obscura-net/stealth.
2026-06-23 10:28:33 +00:00
SGavrl
4309935abd stealth: keep the Chrome 145 identity consistent across TLS, JS, and WebGL
Stealth mode forced a Linux navigator UA and platform but reported Windows
in userAgentData.getHighEntropyValues and served Windows Direct3D11 WebGL
renderers, a combination no real Linux Chrome ever produces and that
cross-layer fingerprint checks flag. Present one consistent Windows Chrome
145 identity instead: the wreq emulation uses Platform::Windows, and the
navigator surfaces read the UA and platform from shared constants so the
HTTP User-Agent header, sec-ch-ua-platform, navigator.userAgent, and
userAgentData all agree. userAgentData brands are now derived from the
Chrome major version via Chromium's GREASE algorithm rather than a single
hardcoded token, and a Linux WebGL renderer pool is added so a Linux UA can
never report a Direct3D11 renderer.
2026-06-22 22:19:16 +00:00
SGavrl
dc780d77e4 js: handle SHA-512/224 and SHA-512/256 in crypto.subtle.digest
The digest op fell through to SHA-256 for any unrecognized algorithm name,
so SHA-512/224 and SHA-512/256 silently returned a SHA-256 hash. Add the
two FIPS 180-4 truncated variants, which the AWS WAF challenge proof-of-work
(used by booking.com and other CloudFront/WAF sites) requires.

Fixes #314.
2026-06-22 22:19:02 +00:00
Gabriel
edde67ddd4 crypto.subtle.digest: reject unsupported algorithms (#314) (#319)
Unknown algorithm names (SHA-512/224, MD5, etc.) were silently falling
back to SHA-256, producing wrong bytes with no error. Now throws
DOMException('NotSupportedError') to match Chrome behavior.

Supported: SHA-1, SHA-256, SHA-384, SHA-512.
2026-06-22 13:02:05 +00:00
Gabriel
a8358cc82d add Plugin, MimeType, MimeTypeArray globals (#305) (#318)
These three interfaces were missing from the global scope entirely.
Site bundles that reference Plugin as a constructor threw
"ReferenceError: Plugin is not defined".

Added as plain function declarations (same pattern as PluginArray) so
they survive the V8 snapshot. navigator.plugins now returns real Plugin
instances and navigator.mimeTypes returns a real MimeTypeArray with
MimeType instances, so instanceof checks work correctly.
2026-06-22 13:01:29 +00:00
RohitMadhu
3bf0da2fae Add Network.setBlockedURLs support (#315)
Added Network.setBlockedURLs dispatch handling.
Stores blocked URL patterns on each page.
Applies blocking to page subresources and JS fetch / XHR requests.
Supports wildcard CDP-style patterns like *://*.gstatic.com/*.woff2.
2026-06-22 11:02:49 +00:00
johnnogueira
7689eda037 cli: add --dump cookies to export browser jar as JSON (#313)
Adds DumpFormat::Cookies that serializes the full browser cookie jar as
pretty-printed JSON after page load. Unlike document.cookie, this captures
HttpOnly cookies inaccessible from JS — useful for extracting session
tokens set by anti-bot challenges.
2026-06-22 11:02:11 +00:00
zhangyang-crazy-one
cd0fbc02c2 fix(cdp): pass clickCount to MouseEvent.detail + fix char handler event loop (#316) (#317)
Bug 1: clickCount parameter was read with underscore prefix and silently
discarded. MouseEvent constructor never received the detail field, so
triple-click (clickCount:3) was always treated as a single click by the
DOM — inputs never selected their full text.

Fix: rename _click_count to click_count, pass it as detail in both
mousedown and click MouseEvent constructors.

Bug 2: page.type() intermittently dropped the last character because
the char handler's quote-escaping order was wrong (single-quote before
backslash), and the V8 event loop wasnt pumped after dispatching the
input event.

Fix: fix escaping order (backslash first, then single-quote), and add
page.settle(50) after the char handler's evaluate to pump the event
loop.
2026-06-22 11:01:40 +00:00
Gabriel
cb6ed24d98 docs: add AGENTS.md
Repo guidance for AI coding agents and contributors, covering the things you
can't infer from the code: build (V8 from source, cmake for stealth, per-crate
scoping), why tests run under cargo nextest and not cargo test (one V8 isolate
per process), the obstacle course as the behavioral gate, the DOM/runtime
gotchas, the hang/panic-safety invariants, and a pre-finish validation
checklist.
2026-06-18 19:30:46 +00:00
SGavrl
3f2f507a7f docs: add AGENTS.md 2026-06-18 19:27:27 +00:00
Gabriel
28f1a7fd48 Update README.md 2026-06-18 21:18:25 +02:00
Gabriel
a919eb2234 Added NodeMaven sponsor logo 2026-06-18 19:17:13 +00:00
Gabriel
d9835e029b js: add document.evaluate and XPathResult (XPath subset)
Pages and scrapers that call document.evaluate() hit "TypeError: document.evaluate is not a function", which can break script execution and hydration. Add document.evaluate() plus the XPathResult global with the standard result-type constants so those paths work (needed for Maps scraping).

The evaluator handles the common relative forms: //tag and .//tag with attribute predicates (@attr, @attr='v', contains(@attr,'v'), starts-with(@attr,'v')), backed by querySelectorAll and attribute filtering. Absolute paths, positional predicates ([n]), text() and axes are not supported and return an empty result.
2026-06-18 08:34:49 +00:00