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.
This commit is contained in:
SGavrl authored and GitHub committed 2026-06-11 22:27:44 +00:00
commit cd889d5659
3 files changed
+311 -87

No files matched your search

+5 -5
View File
@@ -565,11 +565,11 @@ impl DomTree {
// querySelector matches strict descendants of root only, so the // querySelector matches strict descendants of root only, so the
// indexed element must have root among its ancestors. // indexed element must have root among its ancestors.
Some(nid) if self.ancestors(nid).contains(&root) => return Ok(Some(nid)), Some(nid) if self.ancestors(nid).contains(&root) => return Ok(Some(nid)),
// No element has this id at all: a bare id selector cannot match. // Index miss or stale entry (detached node) — fall through to full scan.
None => return Ok(None), // The id_index is best-effort: it only registers nodes at creation time
// Indexed (first) element is not under a scoped root; a later // and does not update on reparent, so it can point to a detached clone
// duplicate could still be a descendant, so fall through to scan. // while the live node (with the same id) is elsewhere in the tree.
Some(_) => {} _ => {}
} }
} }
let selector_list = parse_selector(selector)?; let selector_list = parse_selector(selector)?;
+277 -75
View File
@@ -1628,7 +1628,7 @@ class Element extends Node {
el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el); el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el);
el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl); el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl);
} }
_registerIframe(el);
if (typeof el.onload === 'function') { if (typeof el.onload === 'function') {
try { el.onload(); } catch(e) {} try { el.onload(); } catch(e) {}
} else { } else {
@@ -1638,7 +1638,7 @@ class Element extends Node {
}).catch(() => { }).catch(() => {
el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el); el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el);
el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl); el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl);
_registerIframe(el);
if (typeof el.onload === 'function') try { el.onload(); } catch(e) {} if (typeof el.onload === 'function') try { el.onload(); } catch(e) {}
}); });
} }
@@ -1661,7 +1661,8 @@ class Element extends Node {
get contentWindow() { get contentWindow() {
if (this.localName !== 'iframe') return undefined; if (this.localName !== 'iframe') return undefined;
if (!this._iframeWin) { if (!this._iframeWin) {
this.contentDocument; // side effect: creates _iframeDoc + _iframeWin if (this.parentNode === null) return null;
this.contentDocument;
} }
return this._iframeWin; return this._iframeWin;
} }
@@ -1802,7 +1803,7 @@ class Element extends Node {
toJSON() { return this; }, toJSON() { return this; },
}; };
} }
getClientRects() { return [this.getBoundingClientRect()]; } getClientRects() { return new DOMRectList([this.getBoundingClientRect()]); }
// No layout engine: a stub that always returns true unblocks Playwright's // No layout engine: a stub that always returns true unblocks Playwright's
// actionability polling. With a real layout we'd check display, visibility, // actionability polling. With a real layout we'd check display, visibility,
// opacity and rect dimensions per spec. // opacity and rect dimensions per spec.
@@ -2338,6 +2339,9 @@ class Document extends Node {
} }
class DocumentFragment extends Node { class DocumentFragment extends Node {
constructor(nid) {
super(nid !== undefined ? nid : +_dom("create_document_fragment"));
}
get nodeType() { return 11; } get nodeType() { return 11; }
get nodeName() { return "#document-fragment"; } get nodeName() { return "#document-fragment"; }
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; } get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
@@ -2539,17 +2543,30 @@ Object.defineProperty(globalThis.Window, Symbol.hasInstance, {
}); });
const _iframeRegistry = []; // Remove the static _iframeRegistry and replace with dynamic getters.
function _registerIframe(iframeEl) { Object.defineProperty(globalThis, 'length', {
const idx = _iframeRegistry.length; get() {
_iframeRegistry.push(iframeEl); return document.querySelectorAll('iframe').length;
globalThis.length = _iframeRegistry.length; },
Object.defineProperty(globalThis, idx, { configurable: true,
get() { return iframeEl._iframeWin || null; }, enumerable: true
});
// Since we cannot define a Proxy on globalThis easily, we'll define a reasonable number of indexed getters.
for (let i = 0; i < 50; i++) {
Object.defineProperty(globalThis, i, {
get() {
const iframes = document.querySelectorAll('iframe');
if (i < iframes.length) {
return iframes[i].contentWindow;
}
return undefined;
},
configurable: true, configurable: true,
enumerable: false, enumerable: false
}); });
} }
// Navigator constructor so that typeof Navigator !== 'undefined' and // Navigator constructor so that typeof Navigator !== 'undefined' and
// navigatorPrototype checks don't throw a ReferenceError. // navigatorPrototype checks don't throw a ReferenceError.
function Navigator() {} function Navigator() {}
@@ -2577,6 +2594,23 @@ _markNative(PluginArray.prototype.item);
_markNative(PluginArray.prototype.namedItem); _markNative(PluginArray.prototype.namedItem);
_markNative(PluginArray.prototype.refresh); _markNative(PluginArray.prototype.refresh);
class NetworkInformation {
get downlink() { return 10; }
get downlinkMax() { return Infinity; }
get effectiveType() { return '4g'; }
get rtt() { return 50; }
get saveData() { return false; }
get type() { return 'wifi'; }
get onchange() { return null; }
set onchange(v) {}
get ontypechange() { return null; }
set ontypechange(v) {}
}
_markNative(NetworkInformation);
globalThis.NetworkInformation = NetworkInformation;
globalThis.ContentIndex = class ContentIndex {};
globalThis.navigator = { globalThis.navigator = {
get userAgent() { return globalThis.__obscura_ua || "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; }, get userAgent() { return globalThis.__obscura_ua || "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; },
get appVersion() { return this.userAgent.replace('Mozilla/', ''); }, get appVersion() { return this.userAgent.replace('Mozilla/', ''); },
@@ -2586,7 +2620,7 @@ globalThis.navigator = {
vendor: "Google Inc.", product: "Gecko", productSub: "20030107", vendor: "Google Inc.", product: "Gecko", productSub: "20030107",
doNotTrack: null, doNotTrack: null,
deviceMemory: 8, deviceMemory: 8,
connection: { effectiveType: "4g", type: "wifi", rtt: 50, downlink: 10, saveData: false, onchange: null, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} }, connection: new NetworkInformation(),
pdfViewerEnabled: true, pdfViewerEnabled: true,
get plugins() { get plugins() {
const p = new PluginArray([ const p = new PluginArray([
@@ -3458,7 +3492,29 @@ if (typeof TextDecoder === 'undefined') {
}; };
} }
globalThis.matchMedia = _markNative(function matchMedia(q) { return { matches: false, media: q, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} }; }); globalThis.matchMedia = _markNative(function matchMedia(q) {
var s = (q || '').toLowerCase().replace(/\s+/g, '');
var matches = false;
if (s.includes('prefers-color-scheme:light')) matches = false;
else if (s.includes('prefers-color-scheme:dark')) matches = true;
else if (s.includes('prefers-reduced-motion:no-preference')) matches = true;
else if (s.includes('prefers-reduced-motion:reduce')) matches = false;
else if (s.includes('any-pointer:fine')) matches = true;
else if (s.includes('any-pointer:coarse')) matches = false;
else if (s.includes('pointer:fine')) matches = true;
else if (s.includes('hover:hover')) matches = true;
else if (s.includes('any-hover:hover')) matches = true;
else if (s.includes('color)') || s === '(color)') matches = true;
else if (s.includes('min-width')) {
var m = s.match(/min-width:\s*(\d+)px/);
matches = m ? (globalThis.innerWidth || 1440) >= parseInt(m[1]) : false;
}
else if (s.includes('max-width')) {
var m2 = s.match(/max-width:\s*(\d+)px/);
matches = m2 ? (globalThis.innerWidth || 1440) <= parseInt(m2[1]) : false;
}
return { matches: matches, media: q, onchange: null, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} };
});
globalThis.getComputedStyle = (el) => { globalThis.getComputedStyle = (el) => {
if (!el) el = document.body || {}; if (!el) el = document.body || {};
const style = el?.style || el?._style || new CSSStyleDeclaration(); const style = el?.style || el?._style || new CSSStyleDeclaration();
@@ -4439,7 +4495,7 @@ globalThis.performance = globalThis.performance || {
timing: { navigationStart: 0, domContentLoadedEventEnd: 0, loadEventEnd: 0 }, timing: { navigationStart: 0, domContentLoadedEventEnd: 0, loadEventEnd: 0 },
navigation: { type: 0, redirectCount: 0 }, navigation: { type: 0, redirectCount: 0 },
memory: { memory: {
jsHeapSizeLimit: 2172649472, jsHeapSizeLimit: 4294705152,
totalJSHeapSize: 19321856, totalJSHeapSize: 19321856,
usedJSHeapSize: 16781520, usedJSHeapSize: 16781520,
}, },
@@ -4492,7 +4548,11 @@ Object.defineProperty(Document.prototype, 'fonts', {
}, },
configurable: true, configurable: true,
}); });
globalThis.crypto = globalThis.crypto || { getRandomValues(arr) { for(let i=0;i<arr.length;i++) arr[i]=Math.floor(Math.random()*256); return arr; }, randomUUID(){ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,c=>{const r=Math.random()*16|0;return(c==="x"?r:(r&3|8)).toString(16);}); } }; globalThis.Crypto = class Crypto {
getRandomValues(arr) { for(let i=0;i<arr.length;i++) arr[i]=Math.floor(Math.random()*256); return arr; }
randomUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,c=>{const r=Math.random()*16|0;return(c==="x"?r:(r&3|8)).toString(16);}); }
};
globalThis.crypto = globalThis.crypto || new globalThis.Crypto();
globalThis.structuredClone = globalThis.structuredClone || ((v) => JSON.parse(JSON.stringify(v))); globalThis.structuredClone = globalThis.structuredClone || ((v) => JSON.parse(JSON.stringify(v)));
globalThis.reportError = globalThis.reportError || ((e) => console.error(e)); globalThis.reportError = globalThis.reportError || ((e) => console.error(e));
@@ -4926,8 +4986,20 @@ globalThis.Range = class Range {
insertNode(node) { if (node && this._sc && this._sc.insertBefore) { const kids = this._sc.childNodes; this._sc.insertBefore(node, kids[this._so] || null); } } insertNode(node) { if (node && this._sc && this._sc.insertBefore) { const kids = this._sc.childNodes; this._sc.insertBefore(node, kids[this._so] || null); } }
surroundContents(node) { this.insertNode(node); } surroundContents(node) { this.insertNode(node); }
detach() {} detach() {}
getBoundingClientRect() { return new DOMRect(); } getBoundingClientRect() {
getClientRects() { return []; } if (this.collapsed) return new DOMRect();
let cac = this.commonAncestorContainer;
while (cac && cac.nodeType !== 1 && cac.nodeType !== 9) cac = cac.parentNode;
if (cac && cac.getBoundingClientRect) {
const r = cac.getBoundingClientRect();
return new DOMRect(r.x, r.y, r.width, r.height);
}
return new DOMRect();
}
getClientRects() {
if (this.collapsed) return new DOMRectList([]);
return new DOMRectList([this.getBoundingClientRect()]);
}
static get START_TO_START() { return 0; } static get START_TO_START() { return 0; }
static get START_TO_END() { return 1; } static get START_TO_END() { return 1; }
static get END_TO_END() { return 2; } static get END_TO_END() { return 2; }
@@ -5017,6 +5089,7 @@ _markNative(globalThis.Selection);
Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith, Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith,
HTMLFormElement.prototype.reset, HTMLFormElement.prototype.reset,
Element.prototype.getContext, Element.prototype.toDataURL, Element.prototype.toBlob, Element.prototype.getContext, Element.prototype.toDataURL, Element.prototype.toBlob,
Element.prototype.getBBox,
Node.prototype.appendChild, Node.prototype.removeChild, Node.prototype.appendChild, Node.prototype.removeChild,
Node.prototype.replaceChild, Node.prototype.insertBefore, Node.prototype.replaceChild, Node.prototype.insertBefore,
Node.prototype.contains, Node.prototype.hasChildNodes, Node.prototype.cloneNode, Node.prototype.contains, Node.prototype.hasChildNodes, Node.prototype.cloneNode,
@@ -5303,7 +5376,7 @@ class _Canvas2D {
this._stateStack = []; this._stateStack = [];
} }
_parseColor(css) { _parseColor(css) {
if (!css || css === 'none') return [0,0,0,0]; if (!css || typeof css !== 'string' || css === 'none') return [0,0,0,0];
if (css.startsWith('#')) { if (css.startsWith('#')) {
const hex = css.slice(1); const hex = css.slice(1);
if (hex.length === 3) return [parseInt(hex[0]+hex[0],16),parseInt(hex[1]+hex[1],16),parseInt(hex[2]+hex[2],16),255]; if (hex.length === 3) return [parseInt(hex[0]+hex[0],16),parseInt(hex[1]+hex[1],16),parseInt(hex[2]+hex[2],16),255];
@@ -5499,6 +5572,13 @@ Element.prototype.getContext = function getContext(type) {
if (type === 'webgl' || type === 'experimental-webgl' || type === 'webgl2') { if (type === 'webgl' || type === 'experimental-webgl' || type === 'webgl2') {
return { return {
canvas: this, canvas: this,
MAX_VIEWPORT_DIMS: 0x0D33,
MAX_TEXTURE_SIZE: 0x0D33,
MAX_RENDERBUFFER_SIZE: 0x84E8,
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84EA,
MAX_DRAW_BUFFERS_WEBGL: 0x8824,
getContextAttributes() { return { alpha: true, antialias: true, depth: true, failIfMajorPerformanceCaveat: false, powerPreference: "default", premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: true, desynchronized: false }; },
uniform2f() {},
getExtension(name) { getExtension(name) {
if (name === 'WEBGL_debug_renderer_info') return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 }; if (name === 'WEBGL_debug_renderer_info') return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 };
return null; return null;
@@ -5510,6 +5590,10 @@ Element.prototype.getContext = function getContext(type) {
if (pname === 0x1F00) return 'WebKit'; // GL_VENDOR if (pname === 0x1F00) return 'WebKit'; // GL_VENDOR
if (pname === 0x1F02) return 'OpenGL ES 3.0 (ANGLE)'; // GL_VERSION if (pname === 0x1F02) return 'OpenGL ES 3.0 (ANGLE)'; // GL_VERSION
if (pname === 0x8B8C) return 'WebGL GLSL ES 3.00 (ANGLE)'; // GL_SHADING_LANGUAGE_VERSION if (pname === 0x8B8C) return 'WebGL GLSL ES 3.00 (ANGLE)'; // GL_SHADING_LANGUAGE_VERSION
if (pname === undefined) return [0, 0];
// Some properties like MAX_VIEWPORT_DIMS return arrays
if (pname === 0x0D33) return [8192, 8192];
if (pname === 0x8A2A) return [8192, 8192];
return 0; return 0;
}, },
getSupportedExtensions() { return ['WEBGL_debug_renderer_info','EXT_texture_filter_anisotropic','WEBGL_compressed_texture_s3tc','WEBGL_lose_context']; }, getSupportedExtensions() { return ['WEBGL_debug_renderer_info','EXT_texture_filter_anisotropic','WEBGL_compressed_texture_s3tc','WEBGL_lose_context']; },
@@ -5542,6 +5626,10 @@ Element.prototype.toDataURL = function(type) {
return _fp('canvasFingerprint'); return _fp('canvasFingerprint');
}; };
Element.prototype.toBlob = function(cb, type, q) { cb(new Blob([''])); }; Element.prototype.toBlob = function(cb, type, q) { cb(new Blob([''])); };
Element.prototype.getBBox = function() { return { x: 0, y: 0, width: 0, height: 0 }; };
Element.prototype.getComputedTextLength = function() { return 0; };
Element.prototype.getExtentOfChar = function(ch) { return { x: 0, y: 0, width: 0, height: 0 }; };
Element.prototype.getSubStringLength = function(ch, len) { return 0; };
_markNative(Element.prototype.getContext); _markNative(Element.prototype.getContext);
_markNative(Element.prototype.toDataURL); _markNative(Element.prototype.toDataURL);
@@ -5679,24 +5767,37 @@ if (typeof Document !== 'undefined' && typeof Document.parseHTMLUnsafe !== 'func
_markNative(Document.parseHTMLUnsafe); _markNative(Document.parseHTMLUnsafe);
} }
globalThis.AudioBuffer = class AudioBuffer {
constructor(opts) {
var o = (typeof opts === 'object' && opts !== null) ? opts : {};
this.numberOfChannels = o.numberOfChannels || 1;
this.length = o.length || 0;
this.sampleRate = o.sampleRate || 44100;
this.duration = this.length / (this.sampleRate || 44100);
this._chs = [];
for (var c = 0; c < this.numberOfChannels; c++) this._chs.push(new Float32Array(this.length));
}
getChannelData(c) { return this._chs[c] || this._chs[0] || new Float32Array(0); }
copyFromChannel(dst, ch, start) { var s=this._chs[ch]||this._chs[0]; start=start||0; for(var i=0;i<dst.length;i++) dst[i]=(s&&s[start+i])||0; }
copyToChannel(src, ch, start) { var d=this._chs[ch]||this._chs[0]; start=start||0; if(d) for(var i=0;i<src.length;i++) d[start+i]=src[i]; }
};
globalThis.AudioContext = class AudioContext { globalThis.AudioContext = class AudioContext {
constructor() { this.sampleRate=_fp('audioSampleRate'); this.state='running'; this.currentTime=0; this.baseLatency=_fp('audioBaseLatency'); this.destination={maxChannelCount:2,numberOfInputs:1,numberOfOutputs:0,channelCount:2}; } constructor() { this.sampleRate=_fp('audioSampleRate'); this.state='running'; this.currentTime=0; this.baseLatency=_fp('audioBaseLatency'); this.destination={maxChannelCount:2,numberOfInputs:1,numberOfOutputs:0,channelCount:2}; this._listeners={}; }
createOscillator() { return {type:'sine',frequency:{value:440,setValueAtTime(){}},connect(){},start(){},stop(){},disconnect(){},addEventListener(){}}; } addEventListener(type, fn) { if (!this._listeners[type]) this._listeners[type]=[]; this._listeners[type].push(fn); }
createDynamicsCompressor() { return {threshold:{value:_fp('compThreshold')},knee:{value:_fp('compKnee')},ratio:{value:_fp('compRatio')},attack:{value:0.003},release:{value:0.25},reduction:0,connect(){},disconnect(){}}; } removeEventListener(type, fn) { if (this._listeners[type]) this._listeners[type]=this._listeners[type].filter(h=>h!==fn); }
_ap(v, min=-3.4028235e38, max=3.4028235e38) { return { value: v, defaultValue: v, minValue: min, maxValue: max, setValueAtTime(){} }; }
createOscillator() { return {context:this,type:'sine',frequency:this._ap(440, -22050, 22050),detune:this._ap(0, -153600, 153600),connect(){},start(){},stop(){},disconnect(){},addEventListener(){},removeEventListener(){}}; }
createDynamicsCompressor() { return {context:this,threshold:this._ap(_fp('compThreshold'), -100, 0),knee:this._ap(_fp('compKnee'), 0, 40),ratio:this._ap(_fp('compRatio'), 1, 20),attack:this._ap(0.003, 0, 1),release:this._ap(0.25, 0, 1),reduction:0,connect(){},disconnect(){}}; }
createAnalyser() { createAnalyser() {
return {fftSize:2048,frequencyBinCount:1024,connect(){},disconnect(){}, return {context:this,fftSize:2048,frequencyBinCount:1024,channelCount:2,channelCountMode:'max',channelInterpretation:'speakers',maxDecibels:-30,minDecibels:-100,numberOfInputs:1,numberOfOutputs:1,smoothingTimeConstant:0.8,connect(){},disconnect(){},
getByteFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=Math.floor(_fpRand(600+i)*10);}, getByteFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=Math.floor(_fpRand(600+i)*10);},
getFloatFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=-100+_fpRand(700+i)*5;} getFloatFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=-100+_fpRand(700+i)*5;}
}; };
} }
createGain() { return {gain:{value:1,setValueAtTime(){}},connect(){},disconnect(){}}; } createGain() { return {context:this,gain:this._ap(1),connect(){},disconnect(){}}; }
createBiquadFilter() { return {type:'lowpass',frequency:{value:350},Q:{value:1},connect(){},disconnect(){}}; } createBiquadFilter() { return {context:this,type:'lowpass',frequency:this._ap(350, 0, 22050),Q:this._ap(1, 0.0001, 1000),gain:this._ap(0, -40, 40),connect(){},disconnect(){}}; }
createBufferSource() { return {buffer:null,connect(){},start(){},stop(){},disconnect(){},loop:false}; } createBufferSource() { return {context:this,buffer:null,connect(){},start(){},stop(){},disconnect(){},loop:false}; }
createBuffer(ch,len,rate) { createBuffer(ch,len,rate) { return new globalThis.AudioBuffer({numberOfChannels:ch||1,length:len||0,sampleRate:rate||44100}); }
var chs = [];
for (var _c=0; _c<(ch||1); _c++) chs.push(new Float32Array(len||0));
return {length:len,sampleRate:rate,numberOfChannels:ch,getChannelData:function(c){return chs[c]||chs[0];},duration:len/rate};
}
createScriptProcessor() { return {connect(){},disconnect(){},onaudioprocess:null}; } createScriptProcessor() { return {connect(){},disconnect(){},onaudioprocess:null}; }
decodeAudioData(buf) { return Promise.resolve(this.createBuffer(2,44100,44100)); } decodeAudioData(buf) { return Promise.resolve(this.createBuffer(2,44100,44100)); }
resume() { this.state='running'; return Promise.resolve(); } resume() { this.state='running'; return Promise.resolve(); }
@@ -5704,7 +5805,17 @@ globalThis.AudioContext = class AudioContext {
close() { this.state='closed'; return Promise.resolve(); } close() { this.state='closed'; return Promise.resolve(); }
}; };
globalThis.OfflineAudioContext = class OfflineAudioContext extends AudioContext { globalThis.OfflineAudioContext = class OfflineAudioContext extends AudioContext {
constructor(ch,len,rate) { super(); this.length=len||44100; this.oncomplete=null; } constructor(ch,len,rate) {
super();
if (typeof ch === 'object' && ch !== null) {
this.length = ch.length || 44100;
this.sampleRate = ch.sampleRate || 44100;
} else {
this.length = len || 44100;
this.sampleRate = rate || 44100;
}
this.oncomplete = null;
}
startRendering() { startRendering() {
var self = this; var self = this;
var buf = this.createBuffer(1, self.length, 44100); var buf = this.createBuffer(1, self.length, 44100);
@@ -5721,11 +5832,16 @@ globalThis.OfflineAudioContext = class OfflineAudioContext extends AudioContext
for (var i = 4500; i < 5000; i++) s += Math.abs(data[i]); for (var i = 4500; i < 5000; i++) s += Math.abs(data[i]);
var scale = s > 0 ? target / s : 0; var scale = s > 0 ? target / s : 0;
for (var i = 0; i < self.length; i++) data[i] *= scale; for (var i = 0; i < self.length; i++) data[i] *= scale;
// Fire oncomplete on next microtask so callers can set ctx.oncomplete // Fire oncomplete + 'complete' listeners on next microtask so callers
// synchronously after calling startRendering() (fpCollect pattern). // can register handlers synchronously after startRendering().
var p = Promise.resolve().then(function() { var p = Promise.resolve().then(function() {
var evt = {renderedBuffer: buf, target: self, type: 'complete'};
if (typeof self.oncomplete === 'function') { if (typeof self.oncomplete === 'function') {
try { self.oncomplete({renderedBuffer: buf, target: self, type: 'complete'}); } catch(e) {} try { self.oncomplete(evt); } catch(e) {}
}
var listeners = (self._listeners && self._listeners['complete']) || [];
for (var i = 0; i < listeners.length; i++) {
try { listeners[i](evt); } catch(e) {}
} }
return buf; return buf;
}); });
@@ -5945,18 +6061,75 @@ globalThis.Worker = class Worker {
this._listeners = {}; this._listeners = {};
const worker = this; const worker = this;
if (typeof url === 'string' && (url.startsWith('blob:') || url.startsWith('http'))) { let resolvedUrl = url;
const blobContent = globalThis.__blobStore?.[url]; if (typeof url === 'string') {
if (blobContent) { const blob = globalThis.__blobStore?.[url];
this._code = blobContent; if (blob) {
} else { worker._code = blob;
(async () => { // Auto-start on next tick so caller can set onmessage first.
try { setTimeout(() => worker._autoRun(), 0);
const resp = await fetch(url); return;
worker._code = await resp.text();
} catch(e) { if (worker.onerror) worker.onerror(e); }
})();
} }
// Resolve relative URLs against the current page.
if (!url.startsWith('http') && !url.startsWith('blob:') && !url.startsWith('data:')) {
try { resolvedUrl = new URL(url, globalThis.location?.href || '').href; } catch(e) {}
}
(async () => {
try {
const resp = await fetch(resolvedUrl);
worker._code = await resp.text();
if (!worker._terminated) worker._autoRun();
} catch(e) { if (worker.onerror) worker.onerror(e); }
})();
}
}
_makeScope() {
const worker = this;
// WorkerGlobalScope defined + no document property → IS_WORKER_SCOPE = true in creepjs
const scope = {
WorkerGlobalScope: function WorkerGlobalScope() {},
DedicatedWorkerGlobalScope: function DedicatedWorkerGlobalScope() {},
postMessage: (msg) => {
if (worker._terminated) return;
const evt = { data: msg };
if (worker.onmessage) worker.onmessage(evt);
const ls = worker._listeners['message'] || [];
for (const h of ls) h(evt);
},
addEventListener: (type, fn) => {
if (!scope._ev) scope._ev = {};
if (!scope._ev[type]) scope._ev[type] = [];
scope._ev[type].push(fn);
},
close: () => { worker._terminated = true; },
crypto: globalThis.crypto,
Crypto: globalThis.Crypto,
TextEncoder: globalThis.TextEncoder,
TextDecoder: globalThis.TextDecoder,
atob: globalThis.atob,
btoa: globalThis.btoa,
setTimeout: globalThis.setTimeout,
setInterval: globalThis.setInterval,
clearTimeout: globalThis.clearTimeout,
clearInterval: globalThis.clearInterval,
fetch: globalThis.fetch,
console: globalThis.console,
performance: globalThis.performance,
location: globalThis.location,
};
scope.self = scope;
return scope;
}
_autoRun() {
if (this._terminated || !this._code) return;
const worker = this;
const scope = worker._makeScope();
try {
const fn = new Function('self', 'postMessage', 'addEventListener', 'close', worker._code);
fn(scope, scope.postMessage, scope.addEventListener, scope.close);
} catch(e) {
console.error('Worker error:', e.message);
if (worker.onerror) worker.onerror(e);
} }
} }
postMessage(data) { postMessage(data) {
@@ -5964,32 +6137,13 @@ globalThis.Worker = class Worker {
const worker = this; const worker = this;
setTimeout(() => { setTimeout(() => {
if (worker._terminated || !worker._code) return; if (worker._terminated || !worker._code) return;
const scope = worker._makeScope();
try { try {
const workerSelf = {
onmessage: null,
postMessage: (msg) => {
const evt = { data: msg };
if (worker.onmessage) worker.onmessage(evt);
const handlers = worker._listeners['message'] || [];
for (const h of handlers) h(evt);
},
addEventListener: (type, fn) => { workerSelf['on' + type] = fn; },
close: () => { worker._terminated = true; },
crypto: globalThis.crypto,
TextEncoder: globalThis.TextEncoder,
TextDecoder: globalThis.TextDecoder,
atob: globalThis.atob,
btoa: globalThis.btoa,
setTimeout: globalThis.setTimeout,
setInterval: globalThis.setInterval,
clearTimeout: globalThis.clearTimeout,
clearInterval: globalThis.clearInterval,
fetch: globalThis.fetch,
console: globalThis.console,
};
const fn = new Function('self', 'postMessage', 'addEventListener', 'close', worker._code); const fn = new Function('self', 'postMessage', 'addEventListener', 'close', worker._code);
fn(workerSelf, workerSelf.postMessage, workerSelf.addEventListener, workerSelf.close); fn(scope, scope.postMessage, scope.addEventListener, scope.close);
if (workerSelf.onmessage) workerSelf.onmessage({ data }); const evs = (scope._ev && scope._ev['message']) || [];
if (evs.length) { for (const h of evs) h({ data }); }
else if (scope.onmessage) scope.onmessage({ data });
} catch(e) { } catch(e) {
console.error('Worker error:', e.message); console.error('Worker error:', e.message);
if (worker.onerror) worker.onerror(e); if (worker.onerror) worker.onerror(e);
@@ -6134,6 +6288,20 @@ if (typeof DOMRect === 'undefined') {
static fromRect(r={}) { return new DOMRect(r.x,r.y,r.width,r.height); } static fromRect(r={}) { return new DOMRect(r.x,r.y,r.width,r.height); }
}; };
} }
if (typeof DOMRectList === 'undefined') {
globalThis.DOMRectList = class DOMRectList {
constructor(arr=[]) {
this.length = arr.length;
for (let i = 0; i < arr.length; i++) this[i] = arr[i];
}
item(i) { return this[i] || null; }
[Symbol.iterator]() {
let i = 0, self = this;
return { next() { const done = i >= self.length; return { value: done ? undefined : self[i++], done }; } };
}
};
}
if (typeof DOMPoint === 'undefined') { if (typeof DOMPoint === 'undefined') {
globalThis.DOMPoint = class DOMPoint { globalThis.DOMPoint = class DOMPoint {
constructor(x=0,y=0,z=0,w=1) { this.x=x;this.y=y;this.z=z;this.w=w; } constructor(x=0,y=0,z=0,w=1) { this.x=x;this.y=y;this.z=z;this.w=w; }
@@ -6397,10 +6565,44 @@ if (typeof Range === 'undefined') {
setStart(n,o){this.startContainer=n;this.startOffset=o;} setEnd(n,o){this.endContainer=n;this.endOffset=o;} setStart(n,o){this.startContainer=n;this.startOffset=o;} setEnd(n,o){this.endContainer=n;this.endOffset=o;}
collapse(){} selectNode(){} selectNodeContents(){} cloneContents(){return document?.createDocumentFragment();} collapse(){} selectNode(){} selectNodeContents(){} cloneContents(){return document?.createDocumentFragment();}
deleteContents(){} insertNode(){} getBoundingClientRect(){return new DOMRect();} deleteContents(){} insertNode(){} getBoundingClientRect(){return new DOMRect();}
getClientRects(){return [];} cloneRange(){return new Range();} toString(){return '';} getClientRects(){return new DOMRectList([]);} cloneRange(){return new Range();} toString(){return '';}
}; };
} }
if (typeof FontFace === 'undefined') {
globalThis.FontFace = class FontFace {
constructor(family, source, descriptors={}) {
this.family = family;
this.style = descriptors.style || 'normal';
this.weight = descriptors.weight || 'normal';
this.stretch = descriptors.stretch || 'normal';
this.unicodeRange = descriptors.unicodeRange || 'U+0-10FFFF';
this.variant = descriptors.variant || 'normal';
this.featureSettings = descriptors.featureSettings || 'normal';
this.status = 'unloaded';
}
load() { this.status = 'loaded'; return Promise.resolve(this); }
};
globalThis.FontFaceSet = class FontFaceSet extends EventTarget {
constructor() { super(); this.status = 'loaded'; this.ready = Promise.resolve(this); }
add() { return this; }
check() { return true; }
clear() {}
delete() { return false; }
load() { return Promise.resolve([]); }
forEach() {}
has() { return false; }
[Symbol.iterator]() { return [][Symbol.iterator](); }
};
Object.defineProperty(Document.prototype, 'fonts', {
get() {
if (!this._fonts) this._fonts = new FontFaceSet();
return this._fonts;
},
configurable: true
});
}
if (typeof SharedWorker === 'undefined') { if (typeof SharedWorker === 'undefined') {
globalThis.SharedWorker = class SharedWorker { globalThis.SharedWorker = class SharedWorker {
constructor() { this.port = { postMessage(){}, onmessage:null, start(){}, close(){}, addEventListener(){}, removeEventListener(){} }; this.onerror = null; } constructor() { this.port = { postMessage(){}, onmessage:null, start(){}, close(){}, addEventListener(){}, removeEventListener(){} }; this.onerror = null; }
@@ -6484,7 +6686,7 @@ globalThis.__obscura_init = function() {
globalThis.visualViewport = { width:sw, height:sh-80, offsetLeft:0, offsetTop:0, scale:1, addEventListener(){}, removeEventListener(){} }; globalThis.visualViewport = { width:sw, height:sh-80, offsetLeft:0, offsetTop:0, scale:1, addEventListener(){}, removeEventListener(){} };
globalThis.devicePixelRatio = sw >= 2560 ? 2 : 1; globalThis.devicePixelRatio = sw >= 2560 ? 2 : 1;
globalThis.innerWidth = sw; globalThis.innerHeight = sh - 80; globalThis.innerWidth = sw; globalThis.innerHeight = sh - 80;
globalThis.outerWidth = sw; globalThis.outerHeight = sh; globalThis.outerWidth = sw; globalThis.outerHeight = sh - 40;
var hwValues = globalThis.__obscura_stealth ? [4, 6, 8, 12, 16] : [2, 4, 6, 8, 12, 16]; var hwValues = globalThis.__obscura_stealth ? [4, 6, 8, 12, 16] : [2, 4, 6, 8, 12, 16];
globalThis.navigator.hardwareConcurrency = hwValues[Math.floor(_fpRand(400) * hwValues.length)]; globalThis.navigator.hardwareConcurrency = hwValues[Math.floor(_fpRand(400) * hwValues.length)];
@@ -6496,7 +6698,7 @@ globalThis.__obscura_init = function() {
globalThis.performance.timing = { navigationStart: t0, domContentLoadedEventEnd: t0, loadEventEnd: t0 }; globalThis.performance.timing = { navigationStart: t0, domContentLoadedEventEnd: t0, loadEventEnd: t0 };
var _totalHeap = 15000000 + Math.floor(_fpRand(620) * 85000000); var _totalHeap = 15000000 + Math.floor(_fpRand(620) * 85000000);
globalThis.performance.memory = { globalThis.performance.memory = {
jsHeapSizeLimit: 2172649472, jsHeapSizeLimit: 4294705152,
totalJSHeapSize: _totalHeap, totalJSHeapSize: _totalHeap,
usedJSHeapSize: Math.floor(_totalHeap * (0.3 + _fpRand(621) * 0.5)), usedJSHeapSize: Math.floor(_totalHeap * (0.3 + _fpRand(621) * 0.5)),
}; };
@@ -6519,8 +6721,8 @@ globalThis.__obscura_init = function() {
{brand:"Chromium",version:"145.0.0.0"}, {brand:"Chromium",version:"145.0.0.0"},
], ],
mobile: false, model: "", mobile: false, model: "",
platform: "Linux", platform: "Windows",
platformVersion: "6.1.0", platformVersion: "10.0.0",
uaFullVersion: "145.0.0.0", uaFullVersion: "145.0.0.0",
}); });
}; };
+29 -7
View File
@@ -137,7 +137,21 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
"null".into() "null".into()
} }
"get_element_by_id" => { "get_element_by_id" => {
dom.get_element_by_id(&arg1).map(|id| id.index().to_string()).unwrap_or("-1".into()) // Verify the indexed node is in the live document. The id_index is best-effort:
// it only registers nodes at creation time and doesn't update on reparent, so
// it can point to a detached clone while the live node is elsewhere in the tree.
let doc = dom.document();
let nid = dom.get_element_by_id(&arg1);
let live = nid.filter(|&n| dom.ancestors(n).contains(&doc));
match live {
Some(n) => n.index().to_string(),
None => {
// Fall back to full scan for the live document.
let sel = format!("[id=\"{}\"]", arg1.replace('\\', "\\\\").replace('"', "\\\""));
dom.query_selector(&sel).ok().flatten()
.map(|id| id.index().to_string()).unwrap_or("-1".into())
}
}
} }
"query_selector" => { "query_selector" => {
dom.query_selector(&arg1).ok().flatten().map(|id| id.index().to_string()).unwrap_or("-1".into()) dom.query_selector(&arg1).ok().flatten().map(|id| id.index().to_string()).unwrap_or("-1".into())
@@ -235,19 +249,22 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
serde_json::to_string(&dom.outer_html(NodeId::new(nid))).unwrap_or("\"\"".into()) serde_json::to_string(&dom.outer_html(NodeId::new(nid))).unwrap_or("\"\"".into())
} }
"append_child" => { "append_child" => {
let parent = arg1.parse::<u32>().unwrap_or(0); // Reject if either nid failed to parse (was "undefined"/empty) — those
let child = arg2.parse::<u32>().unwrap_or(0); // default to 0 which is the document root, and silently operating on it
// corrupts the tree. Require both args to be valid positive integers.
let parent = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
let child = match arg2.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
dom.append_child(NodeId::new(parent), NodeId::new(child)); dom.append_child(NodeId::new(parent), NodeId::new(child));
"true".into() "true".into()
} }
"remove_child" => { "remove_child" => {
let child = arg1.parse::<u32>().unwrap_or(0); let child = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
dom.remove_child(NodeId::new(child)); dom.remove_child(NodeId::new(child));
"true".into() "true".into()
} }
"insert_before" => { "insert_before" => {
let new_node = arg1.parse::<u32>().unwrap_or(0); let new_node = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
let ref_node = arg2.parse::<u32>().unwrap_or(0); let ref_node = match arg2.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
dom.insert_before(NodeId::new(ref_node), NodeId::new(new_node)); dom.insert_before(NodeId::new(ref_node), NodeId::new(new_node));
"true".into() "true".into()
} }
@@ -261,7 +278,12 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
"true".into() "true".into()
} }
"set_inner_html" => { "set_inner_html" => {
let nid = arg1.parse::<u32>().unwrap_or(0); let nid = match arg1.parse::<u32>() {
Ok(n) if n > 0 => n,
// nid=0 is the document root; never allow innerHTML to clear it.
// nid parse failure (e.g. "undefined") also falls here.
_ => return "false".into(),
};
let target = NodeId::new(nid); let target = NodeId::new(nid);
let children = dom.children(target); let children = dom.children(target);
for child in children { for child in children {