a2fd4d17356a58d6a4df2cb56b7ef7333f8c7754
118
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
5d53dfef03 | Add XPath compatibility for Maps scraping | ||
|
|
d526a4da7d |
js: make event.isTrusted false for page-constructed events
isTrusted was hardcoded to true for every Event, so new Event('x').isTrusted returned true. Real browsers return false for script-constructed events and true only for user-agent dispatched ones, so this was a trivial bot-detection tell (Closes #303).
Track trusted events in a closure-private WeakSet that page JS cannot read or forge; default isTrusted to false. The CDP input pipeline marks its synthesized mouse/key events trusted via a non-enumerable __obscura_markTrusted helper, so automation clicks stay trusted like real Chrome over CDP while page-built events report false.
|
||
|
|
6264e6bfad |
js: make event.isTrusted false for page-constructed events
isTrusted was hardcoded to true for every Event, so new Event('x').isTrusted
returned true. Real browsers return false for script-constructed events and
true only for user-agent dispatched ones, so this was a trivial bot-detection
tell (issue #303).
Track trusted events in a closure-private WeakSet that page JS cannot read or
forge; default isTrusted to false. The CDP input pipeline marks its synthesized
mouse/key events trusted via a non-enumerable __obscura_markTrusted helper, so
automation clicks stay trusted like real Chrome over CDP while page-built
events report false.
|
||
|
|
1a3888c056 |
js: mark Event constructor as native for stealth compatibility
Mark the Event constructor as native so that Event.toString() returns "[native code]" instead of leaking the JavaScript implementation. This closes a stealth fingerprinting vector. |
||
|
|
37b33c1109 | js: mark Event constructor as native for stealth compatibility | ||
|
|
570ca1544c | add Network.getResponseBody support | ||
|
|
09fe30ec39 |
js: implement Element.getAttributeNames()
The method was missing, so calling it threw TypeError: ... is not a function, which broke Vue 3/Nuxt client hydration (issue #297). Back it with the existing attribute_names op, the same source as the attributes getter, so the order matches attributes and outerHTML. |
||
|
|
cd889d5659 |
Fix/stealth consistency
hasIframeProxy fix contentWindow now returns null when parentNode === null (iframe never inserted into any tree). The previous attempt used isConnected, which broke getBehemothIframe by returning null for iframes inside the phantom iframe's detached document. That caused PHANTOM_DARKNESS to fall back to self (main window), making all DOM measurement ops run on the main document instead of the isolated iframe body, which corrupted the fingerprint element lookup. Worker auto-execution Worker constructor now resolves relative URLs against location.href, fetches the script, and auto-runs it. The worker scope exposes WorkerGlobalScope so IS_WORKER_SCOPE resolves correctly in creepjs. All three worker types now complete within the 3s timeout instead of silently timing out. AudioBuffer + AudioContext AudioBuffer class added to globalThis with getChannelData, copyFromChannel, copyToChannel. AudioContext.createBuffer returns a real instance. AudioContext.addEventListener was a no-op; startRendering now dispatches to stored listeners so the complete event fires. Crypto Crypto class added to globalThis with randomUUID and getRandomValues on its prototype. Fixes ReferenceError in getPlatformEstimate. NetworkInformation + ContentIndex NetworkInformation class with downlinkMax on the prototype clears noDownlinkMax. ContentIndex stub on globalThis clears noContentIndex. navigator.connection is now a real NetworkInformation instance. getElementById / querySelector stale index fix id_index only registers nodes at creation time. When replaceChild(fragment, el) swapped in new nodes their IDs hit .or_insert() and were skipped, leaving the index pointing at the detached original. getElementById now verifies the indexed node is in the live document and falls back to a full attribute-selector scan. query_selector_from falls through to the full scan on both index miss and stale entry instead of returning None early. matchMedia Handles common queries instead of always returning false. prefers-color-scheme: dark returns true (headless Chrome defaults to light mode, so returning true for light is a bot signal). any-pointer: fine, hover: hover, prefers-reduced-motion: no-preference, min/max-width, and (color) return sensible values. Fingerprint consistency jsHeapSizeLimit raised to 4294705152 (4 GB, matching 64-bit Chrome). getHighEntropyValues platform corrected from "Linux" to "Windows", platformVersion from "6.1.0" to "10.0.0" to match the Windows NT 10.0 UA string. Canvas: real PNG encoder, arc fill, multiply blend mode. Screen class, performance.now, chrome runtime errors fixed. Build note deviceMemory and hardwareConcurrency use the high-value pools ([4, 8] GB and [4, 6, 8, 12, 16] cores) only when built with --features stealth. |
||
|
|
4f722dcfd3 |
stealth: fix fingerprint consistency, matchMedia, jsHeapSizeLimit
jsHeapSizeLimit raised to 4294705152 (~4 GB) to match 64-bit Chrome. Was 2172649472 (~2 GB). getHighEntropyValues platform corrected to "Windows" and platformVersion to "10.0.0" to match the Windows NT 10.0 user agent string. Was "Linux" / "6.1.0", which mismatched the UA and could be detected. matchMedia now handles common queries rather than always returning false: prefers-color-scheme:dark returns true (avoiding prefersLightColor headless flag — headless Chrome defaults to light so true is a bot signal), any-pointer:fine, hover:hover, prefers-reduced-motion:no-preference, min/max-width, and (color) return sensible values. Result: 0% like-headless, 0% headless, 0% stealth, 33/33 obstacle course. Note: build must include --features stealth for deviceMemory and hardwareConcurrency to use the high-value pools ([4,8] and [4,6,8,12,16]). |
||
|
|
4e8960e323 |
stealth: add NetworkInformation and ContentIndex shims
NetworkInformation class with downlinkMax on its prototype clears
noDownlinkMax from the like-headless flag list. creepjs checks
'downlinkMax' in (window.NetworkInformation?.prototype || {}).
ContentIndex stub on globalThis clears noContentIndex. creepjs checks
'ContentIndex' in window.
navigator.connection is now a real NetworkInformation instance instead
of a plain object literal.
Result: 0% like-headless, 0% headless, 0% stealth on creepjs. 33/33
obstacle course unchanged.
|
||
|
|
93b2fba6a1 |
fix: creepjs stealth, AudioBuffer, Crypto, Worker, hasIframeProxy
Worker auto-execution: Worker constructor resolves relative URLs against
location.href, fetches the script, and auto-runs it. WorkerGlobalScope is
exposed so IS_WORKER_SCOPE resolves correctly. Worker data (navigator,
locale, timezone) now arrives via postMessage within the 3s timeout.
AudioBuffer class added to globalThis with getChannelData, copyFromChannel,
copyToChannel. AudioContext.createBuffer returns a real AudioBuffer. Fixes
ReferenceError that blocked the audio fingerprint section.
Crypto class added to globalThis with randomUUID and getRandomValues on its
prototype. Fixes ReferenceError in getPlatformEstimate.
AudioContext.addEventListener was a no-op; startRendering only fired
oncomplete. Add _listeners map and dispatch to stored listeners.
getElementById and querySelector('#id') returned null for elements inserted
via replaceChild(documentFragment, el). id_index only registers nodes at
creation time; .or_insert skipped IDs on cloned fragments; None caused
query_selector_from to short-circuit the full scan.
Fix querySelector: fall through to full scan on index miss or stale entry.
Fix getElementById: verify node is in live document; fall back to attribute
selector scan if not.
contentWindow getter: return null when parentNode is null (iframe was never
inserted into any tree). Previously returning null for all disconnected
iframes broke getBehemothIframe, which caused PHANTOM_DARKNESS to fall back
to self (main window) instead of the phantom iframe. That made DOC point to
the main document, so rect and font measurements wrote fragment nodes into
the main body, and the subsequent full-DOM scan in getElementById could not
find fingerprint-data through nested fragments in time.
Limiting the null return to parentNode===null (never inserted) means
getBehemothIframe gets a valid nested contentWindow as expected. creepjs
now reports hasIframeProxy: false and 0% stealth detection.
DocumentFragment constructor: pass nid through when provided.
Range.getBoundingClientRect / getClientRects: return commonAncestor rect.
|
||
|
|
f58bb45956 |
fix(stealth): synchronize TLS and JS fingerprints to Chrome 145
Upgraded wreq to 6.0.0-rc.29 and wreq-util to 3.0.0-rc.12 to apply the latest Chrome 145 TLS fingerprint (16 cipher suites, proper extension order, ALPS, GREASE). Introduced __obscura_stealth global to isolate stealth-specific JS behaviors from the standard execution path. Overrode navigator.userAgentData and getHighEntropyValues() in stealth mode to perfectly match the sec-ch-ua network headers. Restricted hardwareConcurrency and deviceMemory pools in stealth mode to desktop-realistic values (e.g., minimum 4 cores, 4GB RAM) to eliminate mobile-tier leaks. Fixed usedJSHeapSize occasionally exceeding totalJSHeapSize. |
||
|
|
56ab54e615 |
stealth: gate JS behaviours, upgrade wreq, fix fp consistency
wreq upgraded to 6.0.0-rc.29 and wreq-util to 3.0.0-rc.12 to apply the latest Chrome 145 TLS fingerprint (16 cipher suites, proper extension order, ALPS, GREASE). __obscura_stealth global variable introduced and passed from Rust to JS, allowing stealth-specific JS behaviors to run only when the stealth flag is enabled. Tightened hardwareConcurrency and deviceMemory pools in stealth mode to desktop-realistic values (e.g. min 4GB RAM, 4 cores), eliminating mobile-tier values that leak the spoof. navigator.userAgentData and getHighEntropyValues() are now overridden in stealth mode to perfectly match the sec-ch-ua header sent by wreq_util::Profile::Chrome145. Fixed usedJSHeapSize sometimes exceeding totalJSHeapSize. |
||
|
|
dc2c585511 |
stealth: fix audio fingerprint, Screen class, chrome errors, performance.now
OfflineAudioContext.startRendering() now fires oncomplete asynchronously
(microtask) so callers that set ctx.oncomplete after calling startRendering
(fpCollect pattern) get the callback. The rendered buffer contains a
triangle wave scaled to produce the Chrome Linux audio fingerprint sum
(~124.04). createBuffer() now uses persistent Float32Array channels so
written sample values are read back correctly.
screen is now an instance of a Screen class with native getters on the
prototype. Object.getOwnPropertyDescriptor(Object.getPrototypeOf(screen),
"width").get.toString() now returns the expected native-code format instead
of throwing.
chrome.runtime.connect() and sendMessage() now throw the Chrome error
("Could not establish connection. Receiving end does not exist.") instead
of returning silently, matching what fpCollect records for detailChrome.
performance.now() now advances by 0.1ms per call within the same clock
tick, simulating ~100us timer resolution. Previously it aliased Date.now()
which returns the same integer for all calls in one tick, making
redPill2 = Infinity (serialized as null).
WebSocket constructor now throws SyntaxError for non-ws/wss URLs, matching
Chrome's behavior and populating errorsGenerated correctly.
|
||
|
|
b29f0c0eb4 |
canvas: real PNG encoder, arc fill, multiply blend mode
toDataURL() now encodes the real pixel buffer as a valid PNG using
stored-block DEFLATE, CRC32, and Adler32. The 106-byte stub it replaced
was a static value that any canvas fingerprinter could trivially flag.
fill() rasterizes arc paths from this._path using a distance-check
circle fill, so arc-based canvas tests produce real pixel data.
_setPixel() handles globalCompositeOperation='multiply' so fpCollect's
colored-circle blend test composites correctly.
Canvas dimensions now read from getAttribute('width'/'height') so
explicitly sized canvases (e.g. 400x200) use their declared dimensions
instead of always defaulting to 300x150.
|
||
|
|
a7a7b0f4d8 |
stealth: add HTMLMediaElement with canPlayType, Navigator constructor
canPlayType was undefined on <audio> and <video> elements. Adds HTMLMediaElement extending Element with canPlayType returning values that match Chrome 145 on Linux (probably/maybe/empty per MIME type). HTMLAudioElement and HTMLVideoElement extend HTMLMediaElement. _elementClassFor routes AUDIO and VIDEO tags to the correct class. Navigator: adds a global Navigator constructor so typeof Navigator !== 'undefined'. Links it into the navigator prototype chain (_wdProto = Object.create(Navigator.prototype)) so navigator instanceof Navigator returns true, matching Chrome. These are two of the signals in server-side fingerprint classifiers (fpCollect et al). |
||
|
|
a59e756da4 |
stealth: add PluginArray class, move webdriver to navigator proto
PluginArray: real browsers expose a native PluginArray type. navigator.plugins was returning a plain Array, so `navigator.plugins instanceof PluginArray` and `navigator.plugins[0].toString()` (which should return [object Plugin]) were failing sannysoft-style checks. Adds a PluginArray constructor whose prototype inherits from Array.prototype, with Symbol.toStringTag='PluginArray'. Each plugin entry now carries Symbol.toStringTag='Plugin' so Object.prototype.toString returns [object Plugin]. webdriver: moved the webdriver getter off the navigator instance and onto a thin prototype so Object.getOwnPropertyDescriptor(navigator, 'webdriver') returns undefined, matching Chrome where the property lives on Navigator.prototype. Getter is marked native. sannysoft "WebDriver (New)" now reports "missing (passed)". |
||
|
|
ac75cf52a6 |
stealth: fix platform/UA mismatch and hide internal globals from Object.keys
navigator.platform returned "Win32" while navigator.userAgent said Linux when
using the stealth client. page.rs now calls set_platform("Linux x86_64", "Linux", "")
alongside set_user_agent when the stealth client is active.
bootstrap.js: pre-declare 71 internal _* globals as enumerable:false before
var runs, so they don't show up in Object.keys(window) on real pages.
|
||
|
|
3c92c36862 |
fix(js): console.log(Error) must not trigger Error.prepareStackTrace
deviceandbrowserinfo.com detects CDP by setting Error.prepareStackTrace as a spy, then calling console.log(new Error()). _consoleFn accessed a.stack, which fires prepareStackTrace if set. Fix: save/clear/restore around the .stack access. |
||
|
|
534de77882 |
Add EventTarget methods to navigator.connection and serviceWorker
NetworkInformation (navigator.connection) and ServiceWorkerContainer (navigator.serviceWorker) are EventTargets in real browsers. React 18 and mobile SPAs call addEventListener on them; the stubs lacked it, so the app bundle crashed with 'addEventListener is not a function' (e.g. goofish.com). |
||
|
|
fa4928faa6 | Trim redundant comments | ||
|
|
38fa85946c |
fix(js): add insertAdjacentText/insertAdjacentElement polyfills (#285) (#286)
DDoS-Guard's view.js challenge calls Element.prototype.insertAdjacentText and dies with 'TypeError: t.insertAdjacentText is not a function', blocking any DDoS-Guard-protected site (e.g. pikabu.ru). insertAdjacentElement was missing too. insertAdjacentHTML was already implemented, so this just lifts the same insertion pattern for Text-node and Element insertion semantics. Both methods mirror the spec: case-insensitive position keywords, four insertion points (beforebegin/afterbegin/beforeend/afterend), and insertAdjacentElement returns the inserted element (or null when beforebegin/afterend has no parent). Registered with _markNative so Function.prototype.toString keeps reporting [native code] — consistent with the existing Element.prototype.insertAdjacentHTML stealth registration. |
||
|
|
5225f6d985 |
fix(js): serialize dynamic module imports + re-land base href for SPA lazy chunks (#246) (#275)
* fix(js): serialize dynamic module imports + re-land base href for SPA lazy chunks (#246) Add a serialized import queue (__dynScriptQueue + __processDynScriptQueue) in bootstrap.js so that dynamically inserted <script module> tags execute one at a time rather than concurrently. This prevents re-entrant RefCell panics in deno_core's futures_unordered_driver when SPAs (Angular, etc.) append multiple lazy chunks at once. Also re-land dynamic import <base href> resolution (previously reverted as 88af239). The original panic was caused by concurrency, not the URL resolution logic — now safe because imports are serialized. Inline module scripts also route through the same queue for consistency. Fixes #246 Closes #242 (dynamic import base href) * Fix base-href resolution, queue wedge, and double-fired load event - Resolve <base href> to an absolute URL before resolving the script src, and guard the new URL() call. A path-only base like <base href="/"> (the standard Angular form) was passed raw to new URL(src, base), which threw "Invalid URL" out of appendChild and broke the SPAs this change targets. - Wrap the dynamic-script queue drain in try/finally so __dynScriptBusy is always cleared. An unexpected throw used to wedge the queue and silently block every later dynamic script on the page. - Fire load/error through dispatchEvent only. dispatchEvent already invokes the element's onload property handler, so the extra explicit onload() call double-fired it. This also drops the stale enqueue-time callback snapshot and reads handlers live off the element. --------- Co-authored-by: SGavrl <[email protected]> |
||
|
|
179d9f3369 |
fix(js): implement CSS.supports() feature detection (#209)
* fix(js): implement CSS.supports() feature detection
CSS.supports() was a stub that always returned false. Sites that
feature-detect CSS support (e.g. CSS.supports('display','flex'))
treated the engine as lacking flexbox and redirected to their
"unsupported browser" page, so the SPA never rendered.
Implement both call signatures — supports(property, value) and
supports(conditionText) — and return true for well-formed standard
CSS declarations, matching modern browser behaviour. Malformed input
(empty property/value, or a condition string with no colon) returns
false.
* Revert CSS.escape change, keep PR scoped to CSS.supports
The escape stub was switched to String(s), which is unrelated to the
feature-detection fix and still not a spec CSS.escape. Restore the original
stub so this PR only touches CSS.supports.
---------
Co-authored-by: SGavrl <[email protected]>
|
||
|
|
82b7a0baa8 |
Make navigator.geolocation coordinates configurable (#272)
The geolocation shim returned a fixed Frankfurt position. With the timezone now configurable, a deployment in another region would report a location that does not match its exit IP or timezone, the mismatch issue #228 describes. Set OBSCURA_GEOLOCATION="lat,lon" to override the reported coordinates; the value is range-checked and falls back to the Frankfurt default when unset or malformed. The existing positional jitter is preserved. |
||
|
|
a23dad7cd9 |
Stealth profile consistency (#269)
* feat(stealth): randomize browser profile per session Add a pool of 8 realistic browser profiles (Win10/Win11/macOS, Chrome 143-146). One is picked randomly per BrowserContext instead of always advertising Linux, which is a strong bot signal. Each profile keeps navigator.platform, userAgentData.platform, and userAgentData.platformVersion internally consistent with the UA string. - profiles.rs: static PROFILES pool + random_profile() picker - context.rs: pick profile on construction, store platform fields - page.rs: call rt.set_platform() alongside rt.set_user_agent() - runtime.rs: add set_platform() to inject __obscura_platform globals - bootstrap.js: read platform/userAgentData from globals (default Win32) Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * feat(stealth): match GPU renderer pool to OS profile Windows profiles get ANGLE Direct3D11 renderers; macOS profiles get ANGLE Metal renderers. Vendor strings updated to match (Google Inc. (Apple) for Apple Silicon/Intel Mac, Google Inc. (NVIDIA/AMD/Intel) for Windows). Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(stealth): navigator.webdriver returns false instead of undefined Browsers set navigator.webdriver to false when automation is not active. Returning undefined is a detectable bot signal — fingerprinting scripts check typeof navigator.webdriver === 'boolean' as part of headless detection. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * Make stealth profile stable by default, rotation opt-in Picking a random profile per BrowserContext meant one address would cycle through different browser identities, which is itself a bot signal, and the rotated profile carries no matching TLS or timezone fingerprint. Default to a single stable profile; opt into rotation with OBSCURA_ROTATE_PROFILE, or pin one with OBSCURA_PROFILE=<index>. Drive timezone from the process TZ so Intl.DateTimeFormat and Date report one consistent zone. Default Europe/Berlin; override with OBSCURA_TIMEZONE. Update navigator.webdriver test to expect false, matching real Chrome. --------- Co-authored-by: SGavrl <[email protected]> |
||
|
|
6754c1caac |
Make AbortSignal a real constructor (prototype, throwIfAborted, events) (#267)
The AbortController/AbortSignal globals were minimal stubs: AbortSignal had no prototype and no throwIfAborted, and AbortController.abort() only set a flag without firing listeners. Pages that read AbortSignal.prototype or call throwIfAborted threw non-fatal TypeErrors. AbortSignal is now a class with a prototype, aborted/reason, throwIfAborted(), working addEventListener/onabort firing, and static abort()/timeout()/any(). AbortController.abort(reason) fires the abort event. Bootstrap-only, no perf impact. |
||
|
|
fb2841b7ab |
fix: CJK browser_search panic and multi-tab tab_close crash (#257, #258)
* Fix UTF-8 char boundary panic in browser_search The search snippet window sliced body[..start] / body[end..] at byte offsets derived from char counts and needle.len(), which can land inside a multi-byte (CJK) character and panic, killing the MCP stdio transport. Snap start/end to char boundaries before slicing and advance past whitespace by the actual char width. Closes #257. * Add setLineDash and friends to the canvas 2D mock Charting libraries (Highcharts, ECharts) call setLineDash/getLineDash/ ellipse every animation frame. The missing setLineDash threw 'is not a function' from a timer each tick, spamming the console. Adds the line-dash methods plus ellipse/roundRect/createConicGradient/ getContextAttributes stubs. Addresses the error spam in #258; the multi-tab isolate-teardown crash is a separate deeper issue. * Fix multi-tab tab_close V8 isolate abort rusty_v8 enters each isolate on creation and requires them dropped in reverse order of creation. The MCP gave each tab its own isolate and dropped them in arbitrary (user-chosen) order on tab_close, so closing a non-newest tab aborted the process (Check failed: heap->isolate() == Isolate::TryGetCurrent()). Pages with active timers (Highcharts/ECharts) made it easy to hit. Enforce the CDP server's single-live-isolate discipline in the MCP: at most one tab's isolate is ever live. page_mut calls a new activate() that suspends every other tab (drops its isolate, keeps its DOM) and resumes the active one. close_tab drops the closed tab's isolate via suspend_js while it is still the only entered isolate, and tool_close suspends all tabs before clearing. Confined to obscura-mcp; the engine, CDP server and obstacle course are untouched, and peak memory drops to one isolate instead of N. Closes #258. |
||
|
|
c1e0b0fb5a |
fix: resolve element .src to an absolute URL (#255) (#260)
HTMLScriptElement/HTMLImageElement .src reflected the literal attribute instead of the resolved absolute URL. Loaders that derive their base with new URL(document.currentScript.src).origin threw "Invalid URL" on a relative src and aborted, so the page bundle never loaded and the DOM stayed empty (issue #255). getAttribute("src") still returns the literal. |
||
|
|
5135e3cb4d |
perf: DOM read fast paths (constant nodeType, borrowed op reads, base64 table) (#259)
* DOM read fast paths: constant nodeType, fewer ops and allocations - Element.nodeType is now a constant getter instead of inheriting Node's dynamic op-backed getter. nodeType is invariant (1) for element wrappers (wrappers are only built for element nodes and node ids are never freed and reused), so this drops one JS/Rust op per read on a pervasive path. Micro-bench: ~600k nodeType reads go from ~380ms to ~5ms. - attributes getter reads each attribute once instead of twice. - dataset Proxy is cached per element instead of reallocated on every access, which also makes el.dataset === el.dataset (spec-correct). - stylesheet <link> scan borrows the node (with_node) instead of deep cloning it and compares rel via eq_ignore_ascii_case, dropping a clone and a lowercased String alloc per link per navigation. No regression: obstacle course 33/33, WPT dom/nodes Node- subset identical to baseline (3551/4163 subtests), targeted DOM probe identical. * DOM op reads: borrow the node instead of deep-cloning it The scalar read arms in op_dom (node_type, node_name, tag_name, get_attribute, attribute_names, parent/first/last/next/prev lookups, has_child_nodes, pi_target, doctype_name, doctype_public_id) and the node_root / node_ancestors / node_child_index walks called get_node, which clones the whole Node (for elements a deep clone of the attribute Vec and its Strings) just to read one field. Switch them to with_node, which borrows under the RefCell. element_children and the document_element / document_doctype child scans stay on get_node on purpose (they iterate, so the borrow story is left untouched). Output is identical. Traversal/attribute micro-bench ~25-30% faster with fewer allocations per op. No regression: obstacle course 33/33, WPT dom/nodes identical to baseline (7057/10620 across 314 files). * base64 decode: reverse-lookup table instead of indexOf scan _base64ToUint8Array decodes every fetch/XHR response body. It ran an O(64) alphabet.indexOf plus a one-char substring alloc per character. Build a charCode->6-bit reverse table once at module load and index it by charCodeAt, folding out-of-range (NaN or code >= 128) to -1 so the bitmath stays byte-identical to the old indexOf path on valid and malformed input alike. ~20-25% faster decoding a 2MB body; output byte-identical (differential test across valid, unpadded, '='-position, >127, non-alphabet and truncated inputs matches the old code exactly). No regression: obstacle course 33/33. |
||
|
|
24cf7cad56 |
Give relList and sandbox their supported tokens (#256)
DOMTokenList.supports() threw "DOMTokenList has no supported tokens"
for relList and sandbox because they were created without a supported
set. Vite's modulepreload polyfill runs link.relList.supports('modulepreload')
at the top of every bundle, so the throw aborted the whole ES module and
React/Vite SPAs rendered blank. relList and sandbox now carry their token
sets; classList still has none and correctly throws.
|
||
|
|
98b62e53e8 |
fix(api/js): Browser cookie jar + base href + Node.baseURI + XHR EventTarget + Page::settle (closes #241,#242,#244,#245,#247) (#243)
* fix(api): Browser::cookies() now shares context's CookieJar Previously Browser::build() created its own empty CookieJar, while BrowserContext internally creates a separate jar that actually receives Set-Cookie headers from HTTP responses. This caused browser.cookies().get_all() to always return empty after navigation — the populated jar was unreachable. Fix: use context.cookie_jar instead of creating a separate one. Also removes redundant cookie-loading logic in Browser::build() since BrowserContext::_new_inner already handles loading from storage_dir. Closes #241 * fix(browser): add <base href> support for script URL resolution (closes #242) Previously resolve_base_url() returned the document URL directly, causing relative <script src> URLs to resolve against the page URL rather than the <base href> target. On SPAs served from sub-paths (Angular/Vite/Next.js), this caused all bundle scripts to 404. Add resolve_base_url() that queries DOM for <base href> and uses it as the base for relative URL resolution per HTML spec. Applied to both regular scripts and ES modules in execute_scripts(). * fix(js): implement Node.baseURI getter per WHATWG DOM spec (closes #244) Node.baseURI was missing from the bootstrap.js Node class, causing document.baseURI to return undefined. This broke Angular's PlatformLocation.getBaseHref which calls new URL(href, document.baseURI) -- with baseURI empty, the URL constructor throws Invalid URL. Per https://dom.spec.whatwg.org/#dom-node-baseuri, Node.baseURI must return the href of the first <base href> resolved against the document URL, falling back to the document URL when no <base> is present. * fix(js): add XMLHttpRequestEventTarget + XHR dispatchEvent (closes #245) XHR shim lacked dispatchEvent and XMLHttpRequestEventTarget, causing zone.js (Angular/zone.js) to crash with 'Cannot read properties of undefined (reading call)' when Angular's HttpClient makes XHR calls. Add spec-compliant XMLHttpRequestEventTarget base class with addEventListener/removeEventListener/dispatchEvent. Make XHR extend it so zone.js can walk the proto chain correctly. * fix(js): fix XHR super() call order in ES6 class extends * fix(js): use base href for dynamic script/module URL resolution Angular lazy-loaded chunks were resolving against document.location.href instead of the <base href>. This caused all dynamic import() chunks on the Deutsche Boerse SPA to 404 (/bond/7591.xxx.js instead of /7591.xxx.js). Fix: resolve dynamic script src against <base href> when present, falling back to location.href per HTML spec. * Revert "fix(js): use base href for dynamic script/module URL resolution" This reverts commit 88af239be38f2aa11c3c978a4db2a7482f2ed6c0. * feat(api): expose Page::settle() to pump JS event loop (closes #247) obscura_browser::Page already had settle(max_ms) but it was unreachable from the public API. Exposing it allows callers to resolve Promises, fetch callbacks, and microtasks before reading results with evaluate(). --------- Co-authored-by: zhangyang-crazy-one <[email protected]> |
||
|
|
a703b7ac30 |
WPT conformance + perf: O(1) querySelector('#id'), HTML attribute folding, compareDocumentPosition, URLSearchParams, DOMParser (#238)
* dom: complete detached documents; add ProgressEvent and parseHTMLUnsafe
createHTMLDocument returned a document whose body was null: parsing the
skeleton string as innerHTML of <html> collapsed through the fragment parser
and kept only <title>. Build head>title and body explicitly instead. The
DOMParser doc node also gains appendChild/removeChild/insertBefore and a
doctype getter, and createDocument carries its doctype argument.
This lets dom/common.js setupRangeTests() finish (it builds fixtures with
new Document(), createHTMLDocument(), createDocument()) instead of aborting at
harness setup, so the dom/ranges files run. Range-collapse 186/186,
Range-attributes 1/1, Range-cloneRange 55/62, StaticRange-constructor 14/17.
The large O(ranges x points) matrix files (Range-set, comparePoint,
compareBoundaryPoints) still time out: the boundary-point comparison walks the
tree through per-step DOM ops, which is too slow for those matrices. Speeding
that up (a Rust node-position op) is a separate change.
Also add globalThis.ProgressEvent (Event subclass with lengthComputable/
loaded/total) and the static Document.parseHTMLUnsafe(html).
* perf(dom): compute Range tree-order in Rust ops
The Range boundary-point algorithms compared nodes by walking ancestor chains
and sibling lists from JS, one DOM op per step. The dom/ranges matrices
(testRanges x testPoints, thousands of comparisons) made that time out.
Add node_index, compare_order, and node_root ops that do the walk in Rust over
the DOM tree (slab lookups, no per-step FFI), and use them in _rngNodeIndex,
_rngOrder, and _rngRoot. Range-comparePoint 3534/5580, Range-isPointInRange
5197/5733, Range-compareBoundaryPoints 6441/9313, all completing in under 10s
where they previously did not finish. Not on any engine hot path (querySelector,
mutation, navigation, fetch do not use these).
* fileapi: real byte-backed Blob/File, FileReader state machine, Request.blob
Blob was a one-line stub that string-joined its parts (wrong size for binary
or multibyte input) and exposed only text(). Replace it with a Uint8Array-backed
Blob: normalizes string/ArrayBuffer/typed-array/Blob parts, correct size,
slice(), text()/arrayBuffer()/bytes(), type normalization (lowercased, cleared
when non-ASCII), the endings option (native CRLF normalization), and
Symbol.toStringTag. File adds name/lastModified, the 2-argument requirement, and
its own toStringTag.
FileReader becomes a real state machine: EMPTY/LOADING/DONE, throws
InvalidStateError when a read starts while already LOADING, and fires
loadstart/progress/load/loadend (and abort) to both on* handlers and
addEventListener. readAsText/DataURL/ArrayBuffer/BinaryString produce correct
results. Request gains blob().
FileReader-multiple-reads 6/6, File-constructor-endings 10/11, Blob-slice
141/150.
* encoding: TextDecoder supports legacy charsets via encoding_rs
TextDecoder was a UTF-8-only JS shim that ignored its label, so
new TextDecoder('shift_jis'|'euc-jp'|'big5'|'gbk'|'windows-1252'|'iso-8859-1')
.decode() returned garbage. That matters for scraping: pages and scripts decode
legacy-encoded bytes (XHR/fetch responses, File/Blob text) all the time.
Back it with encoding_rs (already used by the net layer for response bodies):
obscura-net gains label_name() and decode_with_label(), exposed to JS as
op_encoding_for_label and op_text_decode. The constructor validates the label
(RangeError) and sets the canonical .encoding/.fatal/.ignoreBOM; decode()
supports fatal mode (TypeError on invalid input) and BOM handling.
Plain UTF-8 non-fatal decoding keeps a pure-JS fast path (no op, no JSON), so
Response.text()/Blob.text() on large bodies are unchanged; the op only runs for
legacy encodings or fatal mode.
textdecoder-labels 222/222, textdecoder-fatal 36/36, byte-order-marks 3/3,
utf16-surrogates 10/10, single-byte-decoder 168/504.
* selection: real Selection over Range (single instance per document)
getSelection() returned a fresh plain object each call, so it was not a Selection
instance, had no identity (window.getSelection() !== document.getSelection()),
and was missing removeRange/empty/collapseToStart/collapseToEnd/setPosition/
setBaseAndExtent/modify while collapse/extend/getRangeAt were broken stubs. That
was ~4862 failing 'selection[method] is not a function' subtests.
Replace it with a real Selection class built on the live Range: one cached
instance per document (window.getSelection() === document.getSelection(), null
for documents with no defaultView), an internal range + direction, derived
anchor/focus/isCollapsed/type/rangeCount, and the full method set
(getRangeAt with IndexSizeError, addRange, removeRange, removeAllRanges/empty,
collapse/setPosition, collapseToStart/End, extend, setBaseAndExtent,
selectAllChildren, containsNode, toString) with the spec DOMException names.
modify() stays a no-op (needs visual layout). No ops.rs changes; not on any hot
path.
setBaseAndExtent 120/120, removeAllRanges 104/104, collapseToStartEnd 51/51,
type 26/26, extend-exception 1/1.
* forms: HTMLInputElement valueAsNumber/valueAsDate/stepUp/stepDown
These were absent. Implement the number/date conversion API for the applicable
types (number, range, date, month, week, time, datetime-local): parsers and
formatters per type (Date.UTC for date/datetime-local, ISO-week Monday math for
week, months-since-epoch for month), valueAsNumber get/set, valueAsDate get/set,
and stepUp/stepDown(n) with the step scale/default per type, step-base snapping,
and min/max clamping. Non-applicable types and step="any" throw InvalidStateError;
non-finite valueAsNumber throws TypeError; range with empty value reads as the
midpoint (50 for 0..100). Plus min/max/step attribute reflection.
All on Element accessors gated by input type, only evaluated on explicit access;
not on any hot path.
input-stepup 51/53, input-valueasnumber 60/64, input-valueasdate 27/30.
* perf(encoding): skip label op for no-arg TextDecoder (Response/Blob text path)
new TextDecoder() with no argument (what Response.text()/Blob.text() and most
pages use) is UTF-8; resolve that without the op_encoding_for_label round trip.
Explicit labels still validate via the op.
* core: HTMLDialogElement, Node.normalize, Range.createContextualFragment
HTMLDialogElement was a bare alias of Element, so ~40 Core files crashed at the
first dialog.show()/showModal(). Add the dialog members on Element.prototype
(matching the popover/input convention): open reflection, returnValue (internal
slot, String coercion), oncancel/onclose defaulting to null, show()/showModal()
/close()/requestClose() with the open-state transitions, InvalidStateError
throwing (show-on-modal, showModal-on-open, showModal-disconnected), the async
non-bubbling close event, the synchronous cancelable cancel event (no re-entrant
cancel), beforetoggle/toggle, and closedBy. matches(':modal') reports a modally
shown dialog (like :popover-open). Modal top-layer/focus/render stays unhandled
(layout).
Node.normalize() now merges adjacent Text nodes and drops empty ones (was a
no-op). Range.createContextualFragment(html) parses a fragment.
createContextualFragment 34/35, dialog-open 3/3, dialog-return-value 1/1,
dialog-showModal 5/10, dialog-requestclose 22/75. Not on hot paths.
* custom-elements: CustomElementRegistry class, getName, ElementInternals, HTMLUnknownElement
customElements was a bare object literal, so 'CustomElementRegistry is not
defined' crashed ~22 Core files at top level (idlharness/feature-detect), and
HTMLUnknownElement/ElementInternals/attachInternals/getName were missing.
Promote the registry to a real CustomElementRegistry class (window.customElements
is now an instance, so 'define in CustomElementRegistry.prototype' works and the
class is exposed). Add spec define() validation/throw order (TypeError for a
non-constructor, SyntaxError for an invalid name, NotSupportedError for a
duplicate name/constructor or reentrant define), getName(), and a SyntaxError
reject in whenDefined for invalid names. The existing define -> upgrade ->
connectedCallback path (used by lit/Polymer on real pages) is preserved.
Add HTMLUnknownElement, ElementInternals (setFormValue/setValidity/checkValidity/
validity/validationMessage/form/states), and Element.attachInternals() with its
NotSupportedError cases.
Does NOT include the [[HTMLConstructor]] (new.target) construction rework or the
reaction wiring into appendChild/setAttribute (hot paths) - deferred to avoid
element-model and perf risk. getName 4/4.
* shadow-dom: ShadowRoot.adoptedStyleSheets (mirror Document)
ShadowRoot lacked adoptedStyleSheets; constructible-stylesheet adoption into a
shadow root threw. Add the getter/setter on ShadowRoot.prototype, matching
Document.adoptedStyleSheets.
* conformance: charset/URL encoding override, IDL reflection, Range and Storage fixes
Encoding: track the detected document character encoding and expose it as
document.characterSet/charset/inputEncoding instead of hardcoding UTF-8. Add the
WHATWG URL encoding override for <a>/<area> hrefs, so a legacy-charset document
percent-encodes the query in its own encoding (EUC-JP U+8108 becomes %CC%AE).
ASCII delimiters stay literal so structured queries are unaffected, and
unmappable code points become %26%23NNN%3B. UTF-8 documents take an early-return
so the href getter hot path is unchanged. fetch()/XHR text() now decode using
the response Content-Type charset instead of always UTF-8. Adds op_url_encode_query
and a document_encoding op; encoding state is plumbed through Page and the runtime.
Reflection: install the reflected IDL attribute layer once on Element.prototype
(no per-element defineProperty, so element creation and query paths are
untouched): global attributes (title, lang, dir, accessKey, slot, autofocus,
hidden, tabIndex) with correct type coercion and enum canonicalization, plus the
ARIAMixin (aria-* reflected as the ariaXxx nullable DOMString properties).
Range: compareBoundaryPoints ToUint16-converts the how argument (NaN/Infinity
no longer false-throw) and raises a real WrongDocumentError DOMException for
ranges that do not share a root.
Storage: reimplement localStorage/sessionStorage as a WHATWG Proxy so property
access (localStorage.foo = x, delete, in, Object.keys) stays in sync with
length, key() and iteration.
* conformance: HTML attribute folding, compareDocumentPosition, URLSearchParams, DOMParser, fetch UA
Next WPT Core batch, all verified live and benchmarked for no hot-path regression.
DOM:
- compareDocumentPosition rewritten to spec: real PRECEDING/FOLLOWING via the
compare_order op, CONTAINS/CONTAINED_BY, and DISCONNECTED|IMPLEMENTATION_SPECIFIC
for different roots. Adds the DOCUMENT_POSITION_* and missing nodeType constants
on Node and its prototype. WPT getPosition() depends on this, so it cascades into
the Range comparePoint/compareBoundaryPoints/isPointInRange expectations.
- HTML elements now ASCII-lowercase attribute names in getAttribute/setAttribute/
removeAttribute/hasAttribute, so el.setAttribute('accessKey') reflects via
getAttribute('accesskey'). The fold is gated behind a cheap uppercase scan and a
namespaceURI HTML guard (SVG/foreign attributes keep their case), and getAttribute
only folds on a miss, so the all-lowercase hot path is unchanged (benchmarked flat).
- Range comparePoint/isPointInRange ToUint32-coerce the offset (negative throws
IndexSizeError); createTreeWalker/createNodeIterator keep an explicit whatToShow of 0.
URL:
- URLSearchParams uses a proper form-urlencoded byte decoder (partial/invalid escapes
stay literal instead of decodeURIComponent throwing on the whole string), 2-arg
has(name,value)/delete(name,value), and a sequence-pair TypeError. Adds URL.parse
and URL.canParse statics.
Other:
- fetch()/XHR send a default Chrome User-Agent (the navigation path set one but
scripted requests went out with none, so UA-gated servers rejected them); honors an
explicit override.
- DOMParser documents expose URL/compatMode/characterSet/readyState/styleSheets/
defaultView and createTreeWalker/createNodeIterator; XMLDocument is defined globally.
* perf(dom): O(1) querySelector('#id') via the id index
querySelector('#id') went through the full selector engine, scanning descendants
in tree order until the first match (O(n), ~68us per call over a 2000-element
document in the micro-bench). A bare ASCII id selector now resolves through the
existing id index in O(1): ~68us -> ~1.25us per call, about 55x faster, with no
change to any other path (create/getAttribute/setAttribute/querySelectorAll/href
all flat). querySelector by id is one of the hottest scraping operations.
The fast path is conservative: only a bare `#name` selector with an ASCII
letter/underscore-led identifier takes it; escapes, combinators, commas,
whitespace, compounds, and non-ASCII fall through to the full engine unchanged.
For a scoped element.querySelector it returns the indexed element only when it is
a strict descendant of the scope, and falls back to the scan otherwise, so
duplicate-id scoped queries stay correct.
Also makes the id index keep the FIRST element in tree order per id (or_insert at
parse time) instead of the last, so getElementById and querySelector('#id') return
the first-in-tree-order element on duplicate ids, per spec (previously last).
|
||
|
|
3b2c6c0deb |
WPT conformance: legacy-charset URL encoding, IDL attribute reflection, FileAPI, Storage, Range (#237)
* dom: complete detached documents; add ProgressEvent and parseHTMLUnsafe
createHTMLDocument returned a document whose body was null: parsing the
skeleton string as innerHTML of <html> collapsed through the fragment parser
and kept only <title>. Build head>title and body explicitly instead. The
DOMParser doc node also gains appendChild/removeChild/insertBefore and a
doctype getter, and createDocument carries its doctype argument.
This lets dom/common.js setupRangeTests() finish (it builds fixtures with
new Document(), createHTMLDocument(), createDocument()) instead of aborting at
harness setup, so the dom/ranges files run. Range-collapse 186/186,
Range-attributes 1/1, Range-cloneRange 55/62, StaticRange-constructor 14/17.
The large O(ranges x points) matrix files (Range-set, comparePoint,
compareBoundaryPoints) still time out: the boundary-point comparison walks the
tree through per-step DOM ops, which is too slow for those matrices. Speeding
that up (a Rust node-position op) is a separate change.
Also add globalThis.ProgressEvent (Event subclass with lengthComputable/
loaded/total) and the static Document.parseHTMLUnsafe(html).
* perf(dom): compute Range tree-order in Rust ops
The Range boundary-point algorithms compared nodes by walking ancestor chains
and sibling lists from JS, one DOM op per step. The dom/ranges matrices
(testRanges x testPoints, thousands of comparisons) made that time out.
Add node_index, compare_order, and node_root ops that do the walk in Rust over
the DOM tree (slab lookups, no per-step FFI), and use them in _rngNodeIndex,
_rngOrder, and _rngRoot. Range-comparePoint 3534/5580, Range-isPointInRange
5197/5733, Range-compareBoundaryPoints 6441/9313, all completing in under 10s
where they previously did not finish. Not on any engine hot path (querySelector,
mutation, navigation, fetch do not use these).
* fileapi: real byte-backed Blob/File, FileReader state machine, Request.blob
Blob was a one-line stub that string-joined its parts (wrong size for binary
or multibyte input) and exposed only text(). Replace it with a Uint8Array-backed
Blob: normalizes string/ArrayBuffer/typed-array/Blob parts, correct size,
slice(), text()/arrayBuffer()/bytes(), type normalization (lowercased, cleared
when non-ASCII), the endings option (native CRLF normalization), and
Symbol.toStringTag. File adds name/lastModified, the 2-argument requirement, and
its own toStringTag.
FileReader becomes a real state machine: EMPTY/LOADING/DONE, throws
InvalidStateError when a read starts while already LOADING, and fires
loadstart/progress/load/loadend (and abort) to both on* handlers and
addEventListener. readAsText/DataURL/ArrayBuffer/BinaryString produce correct
results. Request gains blob().
FileReader-multiple-reads 6/6, File-constructor-endings 10/11, Blob-slice
141/150.
* encoding: TextDecoder supports legacy charsets via encoding_rs
TextDecoder was a UTF-8-only JS shim that ignored its label, so
new TextDecoder('shift_jis'|'euc-jp'|'big5'|'gbk'|'windows-1252'|'iso-8859-1')
.decode() returned garbage. That matters for scraping: pages and scripts decode
legacy-encoded bytes (XHR/fetch responses, File/Blob text) all the time.
Back it with encoding_rs (already used by the net layer for response bodies):
obscura-net gains label_name() and decode_with_label(), exposed to JS as
op_encoding_for_label and op_text_decode. The constructor validates the label
(RangeError) and sets the canonical .encoding/.fatal/.ignoreBOM; decode()
supports fatal mode (TypeError on invalid input) and BOM handling.
Plain UTF-8 non-fatal decoding keeps a pure-JS fast path (no op, no JSON), so
Response.text()/Blob.text() on large bodies are unchanged; the op only runs for
legacy encodings or fatal mode.
textdecoder-labels 222/222, textdecoder-fatal 36/36, byte-order-marks 3/3,
utf16-surrogates 10/10, single-byte-decoder 168/504.
* selection: real Selection over Range (single instance per document)
getSelection() returned a fresh plain object each call, so it was not a Selection
instance, had no identity (window.getSelection() !== document.getSelection()),
and was missing removeRange/empty/collapseToStart/collapseToEnd/setPosition/
setBaseAndExtent/modify while collapse/extend/getRangeAt were broken stubs. That
was ~4862 failing 'selection[method] is not a function' subtests.
Replace it with a real Selection class built on the live Range: one cached
instance per document (window.getSelection() === document.getSelection(), null
for documents with no defaultView), an internal range + direction, derived
anchor/focus/isCollapsed/type/rangeCount, and the full method set
(getRangeAt with IndexSizeError, addRange, removeRange, removeAllRanges/empty,
collapse/setPosition, collapseToStart/End, extend, setBaseAndExtent,
selectAllChildren, containsNode, toString) with the spec DOMException names.
modify() stays a no-op (needs visual layout). No ops.rs changes; not on any hot
path.
setBaseAndExtent 120/120, removeAllRanges 104/104, collapseToStartEnd 51/51,
type 26/26, extend-exception 1/1.
* forms: HTMLInputElement valueAsNumber/valueAsDate/stepUp/stepDown
These were absent. Implement the number/date conversion API for the applicable
types (number, range, date, month, week, time, datetime-local): parsers and
formatters per type (Date.UTC for date/datetime-local, ISO-week Monday math for
week, months-since-epoch for month), valueAsNumber get/set, valueAsDate get/set,
and stepUp/stepDown(n) with the step scale/default per type, step-base snapping,
and min/max clamping. Non-applicable types and step="any" throw InvalidStateError;
non-finite valueAsNumber throws TypeError; range with empty value reads as the
midpoint (50 for 0..100). Plus min/max/step attribute reflection.
All on Element accessors gated by input type, only evaluated on explicit access;
not on any hot path.
input-stepup 51/53, input-valueasnumber 60/64, input-valueasdate 27/30.
* perf(encoding): skip label op for no-arg TextDecoder (Response/Blob text path)
new TextDecoder() with no argument (what Response.text()/Blob.text() and most
pages use) is UTF-8; resolve that without the op_encoding_for_label round trip.
Explicit labels still validate via the op.
* core: HTMLDialogElement, Node.normalize, Range.createContextualFragment
HTMLDialogElement was a bare alias of Element, so ~40 Core files crashed at the
first dialog.show()/showModal(). Add the dialog members on Element.prototype
(matching the popover/input convention): open reflection, returnValue (internal
slot, String coercion), oncancel/onclose defaulting to null, show()/showModal()
/close()/requestClose() with the open-state transitions, InvalidStateError
throwing (show-on-modal, showModal-on-open, showModal-disconnected), the async
non-bubbling close event, the synchronous cancelable cancel event (no re-entrant
cancel), beforetoggle/toggle, and closedBy. matches(':modal') reports a modally
shown dialog (like :popover-open). Modal top-layer/focus/render stays unhandled
(layout).
Node.normalize() now merges adjacent Text nodes and drops empty ones (was a
no-op). Range.createContextualFragment(html) parses a fragment.
createContextualFragment 34/35, dialog-open 3/3, dialog-return-value 1/1,
dialog-showModal 5/10, dialog-requestclose 22/75. Not on hot paths.
* custom-elements: CustomElementRegistry class, getName, ElementInternals, HTMLUnknownElement
customElements was a bare object literal, so 'CustomElementRegistry is not
defined' crashed ~22 Core files at top level (idlharness/feature-detect), and
HTMLUnknownElement/ElementInternals/attachInternals/getName were missing.
Promote the registry to a real CustomElementRegistry class (window.customElements
is now an instance, so 'define in CustomElementRegistry.prototype' works and the
class is exposed). Add spec define() validation/throw order (TypeError for a
non-constructor, SyntaxError for an invalid name, NotSupportedError for a
duplicate name/constructor or reentrant define), getName(), and a SyntaxError
reject in whenDefined for invalid names. The existing define -> upgrade ->
connectedCallback path (used by lit/Polymer on real pages) is preserved.
Add HTMLUnknownElement, ElementInternals (setFormValue/setValidity/checkValidity/
validity/validationMessage/form/states), and Element.attachInternals() with its
NotSupportedError cases.
Does NOT include the [[HTMLConstructor]] (new.target) construction rework or the
reaction wiring into appendChild/setAttribute (hot paths) - deferred to avoid
element-model and perf risk. getName 4/4.
* shadow-dom: ShadowRoot.adoptedStyleSheets (mirror Document)
ShadowRoot lacked adoptedStyleSheets; constructible-stylesheet adoption into a
shadow root threw. Add the getter/setter on ShadowRoot.prototype, matching
Document.adoptedStyleSheets.
* conformance: charset/URL encoding override, IDL reflection, Range and Storage fixes
Encoding: track the detected document character encoding and expose it as
document.characterSet/charset/inputEncoding instead of hardcoding UTF-8. Add the
WHATWG URL encoding override for <a>/<area> hrefs, so a legacy-charset document
percent-encodes the query in its own encoding (EUC-JP U+8108 becomes %CC%AE).
ASCII delimiters stay literal so structured queries are unaffected, and
unmappable code points become %26%23NNN%3B. UTF-8 documents take an early-return
so the href getter hot path is unchanged. fetch()/XHR text() now decode using
the response Content-Type charset instead of always UTF-8. Adds op_url_encode_query
and a document_encoding op; encoding state is plumbed through Page and the runtime.
Reflection: install the reflected IDL attribute layer once on Element.prototype
(no per-element defineProperty, so element creation and query paths are
untouched): global attributes (title, lang, dir, accessKey, slot, autofocus,
hidden, tabIndex) with correct type coercion and enum canonicalization, plus the
ARIAMixin (aria-* reflected as the ariaXxx nullable DOMString properties).
Range: compareBoundaryPoints ToUint16-converts the how argument (NaN/Infinity
no longer false-throw) and raises a real WrongDocumentError DOMException for
ranges that do not share a root.
Storage: reimplement localStorage/sessionStorage as a WHATWG Proxy so property
access (localStorage.foo = x, delete, in, Object.keys) stays in sync with
length, key() and iteration.
|
||
|
|
9c3dd6b21a |
WPT conformance: dom/ranges, FileAPI, detached documents (#236)
* dom: complete detached documents; add ProgressEvent and parseHTMLUnsafe createHTMLDocument returned a document whose body was null: parsing the skeleton string as innerHTML of <html> collapsed through the fragment parser and kept only <title>. Build head>title and body explicitly instead. The DOMParser doc node also gains appendChild/removeChild/insertBefore and a doctype getter, and createDocument carries its doctype argument. This lets dom/common.js setupRangeTests() finish (it builds fixtures with new Document(), createHTMLDocument(), createDocument()) instead of aborting at harness setup, so the dom/ranges files run. Range-collapse 186/186, Range-attributes 1/1, Range-cloneRange 55/62, StaticRange-constructor 14/17. The large O(ranges x points) matrix files (Range-set, comparePoint, compareBoundaryPoints) still time out: the boundary-point comparison walks the tree through per-step DOM ops, which is too slow for those matrices. Speeding that up (a Rust node-position op) is a separate change. Also add globalThis.ProgressEvent (Event subclass with lengthComputable/ loaded/total) and the static Document.parseHTMLUnsafe(html). * perf(dom): compute Range tree-order in Rust ops The Range boundary-point algorithms compared nodes by walking ancestor chains and sibling lists from JS, one DOM op per step. The dom/ranges matrices (testRanges x testPoints, thousands of comparisons) made that time out. Add node_index, compare_order, and node_root ops that do the walk in Rust over the DOM tree (slab lookups, no per-step FFI), and use them in _rngNodeIndex, _rngOrder, and _rngRoot. Range-comparePoint 3534/5580, Range-isPointInRange 5197/5733, Range-compareBoundaryPoints 6441/9313, all completing in under 10s where they previously did not finish. Not on any engine hot path (querySelector, mutation, navigation, fetch do not use these). * fileapi: real byte-backed Blob/File, FileReader state machine, Request.blob Blob was a one-line stub that string-joined its parts (wrong size for binary or multibyte input) and exposed only text(). Replace it with a Uint8Array-backed Blob: normalizes string/ArrayBuffer/typed-array/Blob parts, correct size, slice(), text()/arrayBuffer()/bytes(), type normalization (lowercased, cleared when non-ASCII), the endings option (native CRLF normalization), and Symbol.toStringTag. File adds name/lastModified, the 2-argument requirement, and its own toStringTag. FileReader becomes a real state machine: EMPTY/LOADING/DONE, throws InvalidStateError when a read starts while already LOADING, and fires loadstart/progress/load/loadend (and abort) to both on* handlers and addEventListener. readAsText/DataURL/ArrayBuffer/BinaryString produce correct results. Request gains blob(). FileReader-multiple-reads 6/6, File-constructor-endings 10/11, Blob-slice 141/150. |
||
|
|
07c6e735fa |
WPT conformance: WHATWG URL, DOM Range, and post-navigation event-loop settle (#233)
* fix: restore regressions introduced by #227 #227 deleted build.rs (reverting #224's version-from-tag feature), removed DOM cycle guards in append_child/insert_before that prevent infinite loops on self-append/self-insert, dropped the CharacterData textContent fast-path, removed PI/Doctype Rust ops, and cut the env_allows_private_network() check from validate_fetch_url. Restored: - crates/obscura-cli/build.rs (version from tag/env) - OBSCURA_BUILD_VERSION in version string and banner - allow_private_network env var propagation in main.rs - append_child/insert_before self-cycle guards in tree.rs - textContent CharacterData fast-path in tree.rs - create_processing_instruction, create_doctype, pi_target, doctype_name, doctype_public_id ops in ops.rs - ProcessingInstruction branch in set_text_content op - env_allows_private_network() check in validate_fetch_url * fix(js): Europe/Berlin timezone, plugins iterator recursion Two bugs in the stealth bootstrap code from #227: - 'Europe/Germany' is not a valid IANA timezone; Intl.DateTimeFormat silently fell back to UTC. Changed to 'Europe/Berlin'. - plugins[Symbol.iterator] called this[Symbol.iterator]() recursively, blowing the stack on Array.from(navigator.plugins). Fixed to bind Array.prototype[Symbol.iterator] directly to the array. * url: back URL/URLSearchParams with the url crate, add <a>/<area> decomposition The URL global was a regex shim: it mis-parsed userinfo (hostname came back as the username, port as "pass@host"), had no username/password, and its setters were no-ops. Replace it with two ops (op_url_parse, op_url_set) backed by the WHATWG-compliant url crate, and add the HTMLHyperlinkElementUtils members (protocol/host/hostname/port/pathname/search/hash/origin/username/password and their setters) to <a> and <area>, which had none. This was the single largest WPT failure. a.search returned undefined, so the encoding encode-href tests threw "Cannot read properties of undefined (reading 'substr')" about 225k times. With the fix url-constructor passes 831/888, url-setters 192/279, urlsearchparams-sort 17/17, url/a-element 415/887, and the encode-href files run their assertions instead of crashing. op_url_set runs the url crate inside catch_unwind: a few setters panic on the pathological inputs the url-setters tests use, and a panicking setter must be a no-op (the WHATWG "do nothing on invalid value" rule) rather than abort. URLSearchParams also gained getAll/sort/size/keys/values/entries/iterator and a back-reference so mutations write through to the bound URL. * dom: real Range/StaticRange, createDocumentType, XML Document type Range was a no-op stub (setStart/setEnd discarded their arguments, so startContainer/endContainer read back undefined and every dom/ranges helper that walked range.endContainer crashed with "reading 'nodeType' of undefined"). Replace it with a property-storing Range over the live tree: setStart/End and the *Before/*After/selectNode/selectNodeContents/collapse positioning, plus the boundary-point algorithms comparePoint, compareBoundaryPoints, isPointInRange, intersectsNode, commonAncestorContainer, cloneRange and a real stringifier, all built on a correct preorder tree-order comparison. Add StaticRange. new Document() now reports an XML contentType (it has no backing node id), so createCDATASection no longer throws NotSupportedError on it. Add DOMImplementation.createDocumentType with the lenient name validation browsers use and an owner-document backref so DocumentType.ownerDocument is correct. StaticRange-constructor 14/17. This does not fully unblock dom/ranges yet: dom/common.js also needs createHTMLDocument()/createDocument() to return documents with a working body/createComment/etc., which is a separate fix. * fix(browser,cli): settle the event loop after navigation obscura fetch read the page at the load event and never pumped the event loop afterward on the eval/dump path, so --wait was effectively ignored there. Pages that schedule timers, do async work, or only report through a completion callback that runs after load (testharness add_completion_callback, SPAs) were observed frozen at the load event and never produced their result. Add Page::settle, which drives run_event_loop until the loop goes idle or --wait elapses (returns early when idle, so static pages stay fast), and run it after navigation in the fetch path. |
||
|
|
6b3cc1a686 |
Fix/rescue 227 regressions (#232)
* fix: restore regressions introduced by #227 #227 deleted build.rs (reverting #224's version-from-tag feature), removed DOM cycle guards in append_child/insert_before that prevent infinite loops on self-append/self-insert, dropped the CharacterData textContent fast-path, removed PI/Doctype Rust ops, and cut the env_allows_private_network() check from validate_fetch_url. Restored: - crates/obscura-cli/build.rs (version from tag/env) - OBSCURA_BUILD_VERSION in version string and banner - allow_private_network env var propagation in main.rs - append_child/insert_before self-cycle guards in tree.rs - textContent CharacterData fast-path in tree.rs - create_processing_instruction, create_doctype, pi_target, doctype_name, doctype_public_id ops in ops.rs - ProcessingInstruction branch in set_text_content op - env_allows_private_network() check in validate_fetch_url * fix(js): Europe/Berlin timezone, plugins iterator recursion Two bugs in the stealth bootstrap code from #227: - 'Europe/Germany' is not a valid IANA timezone; Intl.DateTimeFormat silently fell back to UTC. Changed to 'Europe/Berlin'. - plugins[Symbol.iterator] called this[Symbol.iterator]() recursively, blowing the stack on Array.from(navigator.plugins). Fixed to bind Array.prototype[Symbol.iterator] directly to the array. |
||
|
|
990f2a63c8 |
feat(js): popover, legacy events, CDATA/PI, DOMTokenList, namedItem, attachShadow (#231)
Next batch of WPT dom/shadow-dom conformance fixes in bootstrap.js: - Popover API: popover reflection, showPopover/hidePopover/togglePopover, ToggleEvent, :popover-open - Legacy event init: initEvent/initUIEvent/initMouseEvent/initKeyboardEvent/initCompositionEvent, CompositionEvent - Document.createCDATASection / createProcessingInstruction (XML-gated) - classList as a real DOMTokenList (+ relList/sandbox/sizes/htmlFor), spec-correct namespaceURI - getElementsByClassName ASCII-whitespace tokenization - HTMLCollection.namedItem and named access; createRange on document/iframe/DOMParser - attachShadow mode validation and a real ShadowRoot (cloneNode throws, activeElement, styleSheets) querySelectorAll returns a NodeList; named access on live HTMLCollections is resolved lazily via a Proxy so query hot paths stay at native speed. |
||
|
|
dc2a2e3b52 |
feat(js): DOM, namespace, validity, and parsing APIs surfaced by WPT conformance work (#230)
* feat(js): DOMException, ParentNode methods, composedPath, HTMLCollection/NodeList Add the DOMException global (name, message, code, and the legacy code constants on both the constructor and instances). It was referenced by AbortSignal and others but never defined, so any test touching it failed with 'DOMException is not defined'. ParentNode append/prepend/replaceChildren now live on Element, Document, and DocumentFragment and use the spec node conversion: a non-Node argument (including null and undefined) becomes a Text node instead of being dropped or throwing. replaceChildren was missing entirely; append and prepend were Element-only and skipped null/undefined. Add Event.composedPath() and the HTMLCollection and NodeList globals. Verified with fetch --eval; integration suite still 28/28. * feat(js): setHTMLUnsafe/getHTML, namespace lookups, getElementsByTagNameNS, Attr, form validity, Response.blob Element and ShadowRoot setHTMLUnsafe / getHTML as shims over innerHTML. Node lookupNamespaceURI / lookupPrefix / isDefaultNamespace walking the element ancestry and xmlns declarations, with the built-in xml and xmlns namespaces. Element and Document getElementsByTagNameNS with namespace and localName wildcard support. Attr nodes plus Document createAttribute / createAttributeNS and Element getAttributeNode(NS) / setAttributeNode(NS) / removeAttributeNode, bridged to get and set attribute. Form control constraint validation (ValidityState, validity, willValidate, checkValidity, reportValidity, setCustomValidity, validationMessage) and text selection (selectionStart / End / Direction, setSelectionRange, setRangeText, select) as Element.prototype shims. Patch blob / text / json onto the engine Response prototype so fetch(...).blob() works; the real Response prototype lacked blob. Verified with fetch --eval; integration suite still 28/28. |
||
|
|
f740c19d4e | fix(js): implement DocumentFragment getElementById (#222) | ||
|
|
9ae36764da | feat(stealth): many stealth improvements | ||
|
|
5e23903063 |
fix: DocumentFragment flattening in appendChild / insertBefore / replaceChild (closes #217) (#221)
Heavy-JS sites build subtrees off-DOM in a DocumentFragment then
appendChild the fragment to a real parent. The DOM spec says: move
the fragment's children up and leave the fragment empty. Our shim
was passing the fragment's _nid straight to the Rust op, so the
fragment node itself got grafted instead of its children, and
framework code that read the inserted nodes' properties hit
"Cannot read properties of undefined (reading 'length')" the moment
it expected a real Element but found our fragment wrapper.
That is what was killing Google Shopping and Bing Shopping for
@Tarun-004 — Bing's anti-malware shim, Google's result-row renderer,
and a sticky-container helper (n.Setup / n.InitializeStickyContainer
in the stack) all use the off-DOM-fragment pattern.
Same defect existed in insertBefore and replaceChild — all three
now flatten DocumentFragment per spec.
While there, three smaller defenses that the same kind of page
exposes:
- _nid validation. Passing a non-Node truthy value (e.g. undefined,
a plain object) used to silently call op_dom("append_child",
parent, undefined), which the Rust side parses with unwrap_or(0)
and grafts as a child of the document root.
- Self-append guard mirrors the insert_before self-cycle fix:
appendChild(self) is a no-op rather than corrupting the
sibling-pointer linked list.
- Tree::append_child gains a parent_id == child_id early return as
defense in depth, parallel to the insert_before guard.
Sanity test in eval mode: DocumentFragment with two children appended
to a host now produces "<span>A</span><span>B</span>" and the
fragment is empty; insertBefore with a fragment containing C produces
"<span>C</span><span>A</span><span>B</span>"; self-append and
appendChild(null) no longer throw.
Verified against the actual reproducer: `obscura fetch --dump text
https://www.bing.com/shop?q=test` no longer produces the
TypeError / "Inline script error" / "Timer error" pattern from
the issue.
|
||
|
|
ee10e8ddb5 |
feat(engine): DOM + AbortSignal + storage fixes surfaced by WPT conformance work (#219)
Engine-only changes pulled out of the WPT conformance branch. The tests, runner, and harness vendoring that drove these have moved to a separate benchmark repo (obscura-bench). Real bug fixes: - Tree::insert_before created a self-cycle when the inserted node was its own reference (the spec-conformant `child.after(x)` path that the testharness's ChildNode helpers exercise). The fixup wrote node.prev_sibling = node.next_sibling = own id; every subsequent childNodes / querySelectorAll walk then looped forever and obscura burned RAM until OOM. Per spec the op is a no-op; we now early-return. - Tree::text_content extended to return own data for direct CharacterData access (Text / Comment / ProcessingInstruction). - Element.namespaceURI now reflects the namespace passed to createElementNS instead of collapsing every non-svg namespace to xhtml. Without this, code that creates elements in MathML, custom XML namespaces, or the empty namespace gets misclassified as HTML. - AbortController / AbortSignal: the previous shim was a plain object whose addEventListener was a no-op, so listeners for 'abort' silently never fired and any AbortSignal.timeout / AbortSignal.any chains broke. New impl tracks aborted/reason, has a real addEventListener / removeEventListener / dispatchEvent surface, honors `once`, and implements the static abort / timeout / any factories crawlers use for budgeted fetches. - StorageEvent class added. localStorage / sessionStorage mutations now dispatch 'storage' events on the parent window and every iframe window so cross-frame onstorage listeners fire. - ProcessingInstruction (nodeType 7) and DocumentType (nodeType 10) constructors plus document.createProcessingInstruction and document.implementation.createDocumentType. textContent returns null for Document and DocumentType per spec. - insertAdjacentText / insertAdjacentElement on Element. Without these the testharness output renderer threw in a forEach without try/catch, breaking the completion callback chain for every test with subtests. - Window + Node addEventListener filter non-callable listeners, accept the EventListenerObject shape (object with handleEvent), dedupe by (type, callback, capture), honor the `once` option, and snapshot the listener list before dispatch so a `once: true` removal mid-iteration doesn't corrupt the loop. - Iframe HTML loader now extracts head <script> blocks and body on-handler attributes (`<body onstorage=...>`), evals scripts in the parent realm after the iframe window is built, re-attaches handlers as window listeners. Same iframe element reusing its src no longer double-registers in the iframe registry. - ops.rs validate_fetch_url honors the OBSCURA_ALLOW_PRIVATE_NETWORK env var, and the CLI --allow-private-network flag mirrors itself into that env so the js-side fetch gate matches the http_client gate. Without this, iframe loads of 127.0.0.1 resources were blocked even with the flag set. - ops.rs: create_processing_instruction, create_doctype, pi_target, doctype_name, doctype_public_id ops added; set_text_content extended to handle PI nodes. |