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.
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
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
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
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
* 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>
`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.
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