Commit Graph

444 Commits

Author SHA1 Message Date
SGavrl
215861b30f Merge pull request #554 from mnaza/fix/stealth-empty-ssl-cert-env-551
fix(net): treat empty SSL_CERT_FILE/SSL_CERT_DIR as unset in the stealth client
2026-08-07 16:50:04 +00:00
SGavrl
8cd9a001f1 Merge pull request #555 from mnaza/fix/event-constructor-conformance-552
fix(js): make Event/CustomEvent constructors match WebIDL
2026-08-07 16:49:41 +00:00
SGavrl
750d54ce56 Merge pull request #559 from mnaza/fix/documentelement-innerhtml-head-556
fix(dom): keep <head> content when setting documentElement.innerHTML
2026-08-07 16:47:55 +00:00
SGavrl
8cd131fbae Merge pull request #561 from mnaza/fix/string-timer-global-scope-558
fix(js): run string timer handlers as global-scope scripts at fire time
2026-08-07 16:46:52 +00:00
SGavrl
f234ff9e0b Merge pull request #567 from alectimison-maker/fix/meta-charset-sniffing
fix(net): reject unrelated meta charset hints
2026-08-07 16:45:51 +00:00
SGavrl
0d48afe1e0 Merge pull request #564 from ibukanov/fix/stealth-content-encoding-563
fix(net): decode Content-Encoding on the stealth client
2026-08-07 16:45:24 +00:00
SGavrl
70062e0870 Update README.md 2026-08-05 16:57:21 +02:00
alectimison-maker
a4e8ee574f fix(net): reject unrelated meta charset hints 2026-08-05 15:53:21 +08:00
Igor Bukanov
30a51b9008 fix(net): decode Content-Encoding on the stealth client
The wreq dependency was declared without the gzip, brotli, deflate and zstd
features, so the stealth client returned bodies still compressed while the
reqwest path decoded them. The Chrome145 emulation advertises those encodings,
so origins do compress, and the raw gzip bytes reached the HTML parser as
document text.

Add a regression test that serves one gzip response and asserts the stealth
client decodes it.

Fixes #563.
2026-08-04 16:50:47 +00:00
Andrey Mnatsakanov
d0d8617ca6 fix(dom): keep <head> content when setting documentElement.innerHTML
Setting innerHTML on the <html> element parses the fragment in its own context
(added in a2fd4d1), which runs html5ever's "before head" insertion mode and
synthesizes both <head> and <body> under the fragment root. The importer's
helper walked into the root <html> and, if it found a <body> child, returned
that body -- so import_children_from imported only the body's children and any
assigned <head> content (<title>, <meta>, <link>) was silently dropped.

A <body> child of the fragment root only appears in this <html>-context case;
every other element context leaves parsed content directly under the root and
already fell through to returning it. Rename find_body_or_root -> fragment_root
and return the root <html> element so both head and body children import.
Behavior for all non-<html> contexts is unchanged.

Fixes #556
2026-08-04 17:41:57 +02:00
Andrey Mnatsakanov
452cc8554f fix(js): run string timer handlers as global-scope scripts at fire time
A string setTimeout/setInterval handler was compiled with new Function(fn) at
scheduling time. That diverged from the HTML spec in two ways: top-level
var/function declarations in the body stayed local to the compiled function
instead of becoming globals, and a syntax error was caught eagerly at
scheduling and turned into a silent no-op (that still returned a timer id)
rather than surfacing when the timer fires.

Defer string handlers to fire-time indirect eval ((0, eval)(src)), which runs
in the true global scope like a classic script, so declarations become global
and a SyntaxError is thrown when the timer elapses. Matches the dynamic-script
path, which already uses indirect eval for the same reason. Function handlers
are unchanged.

Fixes #558
2026-08-04 17:40:19 +02:00
Andrey Mnatsakanov
86e6d5ce15 fix(net): treat empty SSL_CERT_FILE/SSL_CERT_DIR as unset in the stealth client
The stealth (wreq) client gated its custom-trust-store build on
`var_os(...).is_some()`, which is true for a set-but-empty variable. Supplying
a store to wreq's `tls_cert_store` REPLACES the bundled webpki roots, so an
empty `SSL_CERT_FILE` (a common shell accident, e.g. `export SSL_CERT_FILE=$UNSET`)
built a near-empty default-paths store and made every HTTPS request fail with
CERTIFICATE_VERIFY_FAILED -- while the reqwest client, which already filters
empty values in configured_root_paths(), kept working. Same input, opposite
behavior.

Extract a shared `custom_cert_store_requested` predicate in client.rs that
applies the same `!is_empty()` filter, and call it from the stealth client's
guard. The predicate takes the values as arguments so it is unit-tested under
default features (the stealth feature pulls in boringssl and is not built in
the normal test job).

Fixes #551
2026-08-04 11:08:28 +02:00
Andrey Mnatsakanov
af1e15f6c8 fix(js): make Event/CustomEvent constructors match WebIDL
The Event and CustomEvent constructors diverged from Chrome in three
observable ways:

- new Event() / new CustomEvent() with no argument did not throw, though the
  type argument is required (Chrome: "1 argument required, but only 0
  present."). initEvent/initMouseEvent already had this check; the
  constructors did not.
- the type argument was not coerced to a string, so new Event(123).type was
  the number 123 instead of "123".
- CustomEvent.detail defaulted to undefined; WebIDL CustomEventInit specifies
  `any detail = null`.

Add the required-argument guard to both constructors, coerce type via
String(t) (inherited by every subclass through super), and default detail to
null. createEvent() builds events with an explicit '' type, so the arity
guard leaves the type-"" uninitialized-event path intact.

Fixes #552
2026-08-04 11:00:01 +02:00
SGavrl
f0541f40a4 fix(js): isolate bootstrap declarations (#544) 2026-08-03 16:36:19 +00:00
SGavrl
6a55b63fa6 fix(js): isolate bootstrap declarations 2026-08-03 16:31:30 +00:00
Tamish Mhatre
23a8a3111b fix: add missing #[ignore] gate and local fixture for concurrent fetch test (#450)
* fix: add missing #[ignore] gate and local fixture for concurrent fetch test

* fix(test): make fetch fixture nonblocking

---------

Co-authored-by: SGavrl <277843151+SGavrl@users.noreply.github.com>
2026-08-03 16:11:48 +00:00
SGavrl
5912c37c57 Merge pull request #514 from mnaza/fix/attribute-ns-503
fix(js): make setAttributeNS/getAttributeNS/removeAttributeNS namespace-aware
2026-08-03 16:10:58 +00:00
SGavrl
73fe8b91ae test namespace style synchronization 2026-08-03 15:36:52 +00:00
SGavrl
1518ca2a70 Merge remote-tracking branch 'origin/main' into maintainer/pr-514-review
# Conflicts:
#	crates/obscura-js/js/bootstrap.js
2026-08-03 15:35:42 +00:00
SGavrl
42f7fad709 Merge pull request #507 from mnaza/fix/insert-adjacent-html-495
fix(js): case-insensitive insertAdjacentHTML that keeps table/select fragments
2026-08-03 15:33:34 +00:00
SGavrl
62ada5ebb9 Merge pull request #494 from mnaza/fix/clonenode-structural-490
fix(js): clone nodes structurally instead of via div.innerHTML
2026-08-03 15:32:18 +00:00
SGavrl
624b98c7ad Merge pull request #542 from lisa0314/fix/531-scroll-into-view-if-needed
feat(cdp): support DOM.scrollIntoViewIfNeeded
2026-08-03 15:31:59 +00:00
SGavrl
becb8816e8 Merge pull request #539 from dgcavalcante-png/fix/stealth-honors-ssl-cert-file
fix(net): honor SSL_CERT_FILE / SSL_CERT_DIR in the stealth client (#525 follow-up)
2026-08-03 15:31:19 +00:00
SGavrl
57cedbf069 Merge pull request #540 from dgcavalcante-png/fix/webidl-globals-non-enumerable
fix(js): expose WebIDL interfaces as non-enumerable, like a real browser
2026-08-03 15:30:45 +00:00
lisa0314
84513d5bcc feat(cdp): support DOM.scrollIntoViewIfNeeded 2026-08-03 14:42:03 +08:00
Diego Cavalcante
c7e7c703b7 fix(js): expose WebIDL interfaces as non-enumerable, like a real browser
`bootstrap.js` publishes its DOM interfaces with `globalThis.Node = Node`,
which defaults to `enumerable: true`. A real browser exposes every WebIDL
interface on the global as `enumerable: false`, so the difference is a
one-liner for any script that cares:

    Object.getOwnPropertyDescriptor(window, 'Node').enumerable
    // real Chrome: false     obscura: true

It also means `Object.keys(window)` and `for...in` over the global enumerate
22 names a browser would never list.

The file already has the mechanism for this -- `_preHideInternals()`
pre-declares internal globals with `enumerable: false`, and its own comment
notes that later `globalThis.x = value` assignments only update the value and
leave the descriptor intact. This just adds the WebIDL names to that list, so
no assignment site changes.

Before: 9 of 10 sampled interfaces enumerable. After: 0 of 10, matching Chrome.

Regression: interfaces still callable (`typeof Node === 'function'`),
`instanceof Node` / `instanceof Element` hold, `querySelector` works, Hacker
News still yields 30 results under `--stealth`, snapshot builds clean.

Composes with #537 -- verified with both applied: `var Node` pages run, the
JSF/RichFaces case loads (jQuery/A4J/RichFaces all live), and enumerability
stays at 0/10.
2026-08-02 01:26:42 -03:00
Diego Cavalcante
fb42229200 fix(net): honor SSL_CERT_FILE / SSL_CERT_DIR in the stealth client
The #525 fix landed on the reqwest path (`client.rs`: `configured_root_paths()`
-> `add_root_certificate`) but did not cover `wreq_client.rs`. The stealth
client builds its `wreq::Client` without a certificate store, so those
variables are silently ignored whenever `--stealth` is on.

The consequence is inverted: the transport with the best TLS fingerprint is the
only one that cannot reach a host behind a private or incomplete CA chain --
precisely the environments #525 described.

Same binary on current main, same private-CA server, only the flag differs:

  SSL_CERT_FILE=ca.pem obscura           fetch https://127.0.0.1:8443/  -> ok
  SSL_CERT_FILE=ca.pem obscura --stealth fetch https://127.0.0.1:8443/  -> fails

`wreq` already exposes the hook -- `ClientBuilder::tls_cert_store(CertStore)`,
and `CertStore::builder().set_default_paths()` reads both variables. It is
simply never called.

Two constraints, both of which fail silently if got wrong:

* `tls_cert_store`, not `tls_options`. `emulation()` is documented as
  "This will overwrite the existing configuration" and internally calls
  `self.tls_options(emulation.tls_options)`; setting TLS options here would
  discard the Chrome fingerprint, which is the whole point of this client.
  `tls_cert_store` is a separate config field and composes with emulation.
* Opt-in only. Supplying a store REPLACES the webpki roots (see
  `set_cert_store` in `tls/conn/ext.rs`), it does not add to them. Applying it
  unconditionally would break ordinary sites whenever the bundle is
  incomplete. With neither variable set, behaviour is byte-for-byte unchanged.

Verified: with the variable, a private-CA host resolves under --stealth;
without it, the same host still fails; with a deliberately EMPTY bundle,
example.com fails -- which is what proves the store is actually applied rather
than the bundle merely happening to be right.

Refs #538, #525
2026-08-02 01:10:02 -03:00
SGavrl
eab2a5eea1 Merge pull request #535 from h4ckf0r0day/fix/492-style-sync
Fix/492 style sync
2026-08-01 19:58:18 +00:00
SGavrl
108e777418 Merge remote-tracking branch 'origin/main' into maintainer/pr-492-fix 2026-08-01 19:51:52 +00:00
SGavrl
4b41d7c746 Merge pull request #534 from h4ckf0r0day/fix/517-string-timers
Fix/517 string timers
2026-08-01 19:51:23 +00:00
SGavrl
037e321a79 Merge remote-tracking branch 'origin/main' into maintainer/pr-517-tests 2026-08-01 19:47:51 +00:00
SGavrl
92576571ef Merge pull request #533 from h4ckf0r0day/fix/509-performance-clock
Fix/509 performance clock
2026-08-01 19:46:49 +00:00
SGavrl
2275d8fea0 Merge remote-tracking branch 'origin/main' into maintainer/pr-509-fix
# Conflicts:
#	crates/obscura-js/src/runtime.rs
2026-08-01 19:39:24 +00:00
SGavrl
bbc4980eba Merge pull request #510 from mnaza/fix/childnode-coerce-498
fix(js): coerce non-string primitives in before/after/replaceWith
2026-08-01 17:18:11 +00:00
SGavrl
389de72d1b Merge remote-tracking branch 'origin/main' into maintainer/pr-510-review 2026-08-01 17:14:05 +00:00
SGavrl
31ac297a48 Merge pull request #508 from mnaza/fix/history-preserve-url-496
fix(js): keep the current URL when pushState/replaceState omit url
2026-08-01 17:12:59 +00:00
SGavrl
e9f783b3cf Merge pull request #511 from mnaza/fix/quirks-mode-selectors-499
fix(dom): honor quirks mode for class/id selector case-sensitivity
2026-08-01 17:12:44 +00:00
SGavrl
ec7334d91b Merge pull request #512 from mnaza/fix/remove-double-free-500
fix(dom): guard DomTree::remove() against OOB and double-free
2026-08-01 17:12:28 +00:00
SGavrl
0428099b0f Merge remote-tracking branch 'origin/main' into maintainer/pr-509-fix 2026-08-01 16:04:37 +00:00
SGavrl
76e5280c32 fix(dom): preserve namespaced attribute qualified names 2026-08-01 16:00:52 +00:00
SGavrl
1e305135e3 Merge remote-tracking branch 'origin/main' into maintainer/pr-514-review 2026-08-01 15:58:03 +00:00
SGavrl
a2fd4d1735 fix(js): parse fragments in their insertion context 2026-08-01 15:54:51 +00:00
SGavrl
35fa61032b Merge remote-tracking branch 'origin/main' into maintainer/pr-507-review 2026-08-01 15:50:23 +00:00
SGavrl
907e15d85d perf(js): avoid per-element style sync objects 2026-08-01 15:46:25 +00:00
SGavrl
82aa320084 Merge remote-tracking branch 'origin/main' into maintainer/pr-492-fix 2026-08-01 15:44:40 +00:00
SGavrl
5d7a02041a fix(js): preserve inline styles when cloning 2026-08-01 15:41:45 +00:00
SGavrl
c327354911 Merge remote-tracking branch 'origin/main' into maintainer/pr-494-review 2026-08-01 15:40:11 +00:00
SGavrl
16b6a09546 Merge remote-tracking branch 'origin/main' into maintainer/pr-517-tests 2026-08-01 15:37:17 +00:00
SGavrl
dbc87e86f9 test(js): cover string timer handlers 2026-08-01 15:37:17 +00:00
SGavrl
b5c2f9cc4e Merge branch 'maintainer/pr-508-review' into maintainer/pr-510-review
# Conflicts:
#	crates/obscura-js/src/runtime.rs
2026-08-01 15:30:32 +00:00