From f15de75aefb80620366c177a2d121a5a9ff024ed Mon Sep 17 00:00:00 2001 From: brecht stamper Date: Tue, 13 Aug 2024 15:52:37 +0200 Subject: [PATCH] wip --- .../Document.prototype.cookie.ts | 17 +- .../injected-scripts/JSON.stringify.ts | 8 +- ...MediaDevices.prototype.enumerateDevices.ts | 10 +- .../RTCRtpSender.getCapabilities.ts | 9 +- .../SharedWorker.prototype.ts | 23 +- .../UnhandledErrorsAndRejections.ts | 19 +- ...RenderingContext.prototype.getParameter.ts | 7 +- .../injected-scripts/_descriptorBuilder.ts | 40 +-- .../injected-scripts/_proxyUtils.ts | 254 +++++++++++------- .../injected-scripts/console.ts | 20 +- .../injected-scripts/error.ts | 72 ++++- .../navigator.deviceMemory.ts | 44 +-- .../navigator.hardwareConcurrency.ts | 9 +- .../injected-scripts/navigator.ts | 57 ++-- .../injected-scripts/performance.ts | 1 + .../injected-scripts/polyfill.add.ts | 13 +- .../injected-scripts/polyfill.modify.ts | 20 +- .../injected-scripts/polyfill.remove.ts | 13 +- .../injected-scripts/polyfill.reorder.ts | 13 +- .../speechSynthesis.getVoices.ts | 17 +- .../injected-scripts/tsconfig.json | 4 +- .../injected-scripts/webrtc.ts | 31 ++- .../injected-scripts/window.screen.ts | 23 +- .../interfaces/IBrowserData.ts | 8 +- .../interfaces/IBrowserEmulatorConfig.ts | 61 +++-- .../lib/DomOverridesBuilder.ts | 26 +- .../lib/loadDomOverrides.ts | 201 ++++++-------- .../test/proxyLeak.test.ts | 17 +- 28 files changed, 658 insertions(+), 379 deletions(-) diff --git a/plugins/default-browser-emulator/injected-scripts/Document.prototype.cookie.ts b/plugins/default-browser-emulator/injected-scripts/Document.prototype.cookie.ts index d475c3f6a..399c73d8b 100644 --- a/plugins/default-browser-emulator/injected-scripts/Document.prototype.cookie.ts +++ b/plugins/default-browser-emulator/injected-scripts/Document.prototype.cookie.ts @@ -1,11 +1,18 @@ -const triggerName = args.callbackName; +export type Args = { + callbackName: string; +}; +const typedArgs = args as Args; +const triggerName = typedArgs.callbackName; if (!self[triggerName]) throw new Error('No cookie trigger'); -const cookieTrigger = ((self[triggerName] as unknown) as Function).bind(self); +const cookieTrigger = (self[triggerName] as unknown as Function).bind(self); delete self[triggerName]; -proxySetter(Document.prototype, 'cookie', (target, thisArg, cookie) => { - cookieTrigger(JSON.stringify({ cookie, origin: self.location.origin })); - return ProxyOverride.callOriginal; +proxySetter(Document.prototype, 'cookie', (target, thisArg, argArray) => { + const cookie = argArray.at(0); + if (cookie) { + cookieTrigger(JSON.stringify({ cookie, origin: self.location.origin })); + } + return ReflectCached.apply(target, thisArg, argArray!); }); diff --git a/plugins/default-browser-emulator/injected-scripts/JSON.stringify.ts b/plugins/default-browser-emulator/injected-scripts/JSON.stringify.ts index 7bebdf9da..85df0dfc0 100644 --- a/plugins/default-browser-emulator/injected-scripts/JSON.stringify.ts +++ b/plugins/default-browser-emulator/injected-scripts/JSON.stringify.ts @@ -1,9 +1,7 @@ -proxyFunction(JSON, 'stringify', (target, thisArg, argArray) => { - argArray[1] = null; - argArray[2] = 2; +export type Args = {}; - const result = target.apply(thisArg, argArray); +proxyFunction(JSON, 'stringify', (target, thisArg, argArray) => { + const result = ReflectCached.apply(target, thisArg, [argArray.at(0), null, 2]); console.log(result); - return result; }); diff --git a/plugins/default-browser-emulator/injected-scripts/MediaDevices.prototype.enumerateDevices.ts b/plugins/default-browser-emulator/injected-scripts/MediaDevices.prototype.enumerateDevices.ts index 98d5a03f3..6474d225d 100644 --- a/plugins/default-browser-emulator/injected-scripts/MediaDevices.prototype.enumerateDevices.ts +++ b/plugins/default-browser-emulator/injected-scripts/MediaDevices.prototype.enumerateDevices.ts @@ -1,11 +1,17 @@ +export type Args = { + deviceId?: string; + groupId?: string +}; +const typedArgs = args as Args; + if ( navigator.mediaDevices && navigator.mediaDevices.enumerateDevices && navigator.mediaDevices.enumerateDevices.name !== 'bound reportBlock' ) { const videoDevice = { - deviceId: args.deviceId, - groupId: args.groupId, + deviceId: typedArgs.deviceId, + groupId: typedArgs.groupId, kind: 'videoinput', label: '', }; diff --git a/plugins/default-browser-emulator/injected-scripts/RTCRtpSender.getCapabilities.ts b/plugins/default-browser-emulator/injected-scripts/RTCRtpSender.getCapabilities.ts index 58e10216c..eeef4e158 100644 --- a/plugins/default-browser-emulator/injected-scripts/RTCRtpSender.getCapabilities.ts +++ b/plugins/default-browser-emulator/injected-scripts/RTCRtpSender.getCapabilities.ts @@ -1,5 +1,10 @@ -// @ts-ignore -const { audioCodecs, videoCodecs } = args; +export type Args = { + audioCodecs: any; + videoCodecs: any; +}; +const typedArgs = args as Args; + +const { audioCodecs, videoCodecs } = typedArgs; if ('RTCRtpSender' in self && RTCRtpSender.prototype) { proxyFunction(RTCRtpSender, 'getCapabilities', function (target, thisArg, argArray) { diff --git a/plugins/default-browser-emulator/injected-scripts/SharedWorker.prototype.ts b/plugins/default-browser-emulator/injected-scripts/SharedWorker.prototype.ts index fe034351f..1f0b46bdd 100644 --- a/plugins/default-browser-emulator/injected-scripts/SharedWorker.prototype.ts +++ b/plugins/default-browser-emulator/injected-scripts/SharedWorker.prototype.ts @@ -1,3 +1,5 @@ +export type Args = {}; + if (typeof SharedWorker === 'undefined') { // @ts-ignore return; @@ -34,33 +36,32 @@ proxyConstructor(self, 'SharedWorker', (target, argArray) => { setTimeout(()=> { removeEventListener('connect', storeEvent); original(); - events.forEach(ev => onconnect(ev)); + // new Event('connect', ev) + events.forEach(ev => dispatchEvent(ev)); delete events; }, 0); } - function isInjectedDone() { + // See proxyUtils + function getSharedStorage() { try { - // We can use this to check if injected logic is loaded - return Error['${sourceUrl}']; + return Function.prototype.toString.call('${sourceUrl}'); } catch { - return false; + return undefined; } } - - if (isInjectedDone()) { + if (getSharedStorage()?.ready) { originalAsSync(); return; } // Keep checking until we are ready const interval = setInterval(() => { - if (!isInjectedDone()) { - return + if (getSharedStorage()?.ready) { + clearInterval(interval); + originalAsSync(); } - clearInterval(interval); - originalAsSync(); }, 20); })() `; diff --git a/plugins/default-browser-emulator/injected-scripts/UnhandledErrorsAndRejections.ts b/plugins/default-browser-emulator/injected-scripts/UnhandledErrorsAndRejections.ts index 37deef425..7e23bb6db 100644 --- a/plugins/default-browser-emulator/injected-scripts/UnhandledErrorsAndRejections.ts +++ b/plugins/default-browser-emulator/injected-scripts/UnhandledErrorsAndRejections.ts @@ -1,5 +1,16 @@ -self.addEventListener('error', preventDefault); -self.addEventListener('unhandledrejection', preventDefault); +export type Args = { + preventDefaultUncaughtError: boolean; + preventDefaultUnhandledRejection: boolean; +}; + +const typedArgs = args as Args; + +if (typedArgs.preventDefaultUncaughtError) { + self.addEventListener('error', preventDefault); +} +if (typedArgs.preventDefaultUnhandledRejection) { + self.addEventListener('unhandledrejection', preventDefault); +} function preventDefault(event: ErrorEvent | PromiseRejectionEvent) { event.preventDefault(); @@ -14,7 +25,7 @@ function preventDefault(event: ErrorEvent | PromiseRejectionEvent) { ReflectCached.apply(originalFunction, thisArg, argArray); prevented = true; }, - true, + { overrideOnlyForInstance: true }, ); proxyGetter( event, @@ -23,7 +34,7 @@ function preventDefault(event: ErrorEvent | PromiseRejectionEvent) { ReflectCached.get(target, thisArg); return prevented; }, - true, + { overrideOnlyForInstance: true }, ); if (!('console' in self)) { diff --git a/plugins/default-browser-emulator/injected-scripts/WebGLRenderingContext.prototype.getParameter.ts b/plugins/default-browser-emulator/injected-scripts/WebGLRenderingContext.prototype.getParameter.ts index d09108460..244600868 100644 --- a/plugins/default-browser-emulator/injected-scripts/WebGLRenderingContext.prototype.getParameter.ts +++ b/plugins/default-browser-emulator/injected-scripts/WebGLRenderingContext.prototype.getParameter.ts @@ -1,3 +1,6 @@ +export type Args = Record; +const typedArgs = args as Args; + const activatedDebugInfo = new WeakSet(); for (const context of [ @@ -18,12 +21,12 @@ for (const context of [ const parameter = argArray && argArray.length ? argArray[0] : null; // call api to make sure signature goes through const result = ReflectCached.apply(originalFunction, thisArg, argArray); - if (args[parameter]) { + if (typedArgs[parameter]) { if (!result && !activatedDebugInfo.has(context)) { return result; } - return args[parameter]; + return typedArgs[parameter]; } return result; }); diff --git a/plugins/default-browser-emulator/injected-scripts/_descriptorBuilder.ts b/plugins/default-browser-emulator/injected-scripts/_descriptorBuilder.ts index 5b7c46872..4fdd05d77 100644 --- a/plugins/default-browser-emulator/injected-scripts/_descriptorBuilder.ts +++ b/plugins/default-browser-emulator/injected-scripts/_descriptorBuilder.ts @@ -10,7 +10,7 @@ for (const symbol of ReflectCached.ownKeys(Symbol)) { function createError(message: string, type?: { new (msg: string): any }) { if (!type) { const match = nativeErrorRegex.exec(message); - if (match.length) { + if (match?.length) { message = message.replace(`${match[1]}: `, ''); try { type = self[match[1]]; @@ -47,6 +47,7 @@ function newObjectConstructor( } const props = Object.entries(newProps); const obj = {}; + if (!newProps._$protos) throw new Error('newProps._$protos undefined'); Object.setPrototypeOf( obj, prototypesByPath[newProps._$protos[0]] ?? getObjectAtPath(newProps._$protos[0]), @@ -55,7 +56,7 @@ function newObjectConstructor( if (prop.startsWith('_$')) continue; let propName: string | symbol = prop; if (propName.startsWith('Symbol(')) { - propName = Symbol.for(propName.match(/Symbol\((.+)\)/)[1]); + propName = Symbol.for(propName.match(/Symbol\((.+)\)/)![1]); } Object.defineProperty(obj, propName, buildDescriptor(value, `${path}.${prop}`)); } @@ -73,14 +74,14 @@ function buildDescriptor(entry: IDescriptor, path: string): PropertyDescriptor { if (flags.includes('e')) attrs.enumerable = true; if (entry._$get) { - attrs.get = new Proxy(Function.prototype.call.bind({}), { + attrs.get = new Proxy(function () {}, { apply() { if (entry._$accessException) throw createError(entry._$accessException); if (entry._$value) return entry._$value; if (entry['_$$value()']) return entry['_$$value()'](); }, }); - overriddenFns.set(attrs.get, entry._$get); + overriddenFns.set(attrs.get!, entry._$get); } else if (entry['_$$value()']) { attrs.value = entry['_$$value()'](); } else if (entry._$value !== undefined) { @@ -88,13 +89,13 @@ function buildDescriptor(entry: IDescriptor, path: string): PropertyDescriptor { } if (entry._$set) { - attrs.set = new Proxy(Function.prototype.call.bind({}), { + attrs.set = new Proxy(function () {}, { apply() {}, }); - overriddenFns.set(attrs.set, entry._$set); + overriddenFns.set(attrs.set!, entry._$set); } - let prototypeDescriptor: PropertyDescriptor; + let prototypeDescriptor: PropertyDescriptor | undefined; if (entry.prototype) { prototypeDescriptor = buildDescriptor(entry.prototype, `${path}.prototype`); @@ -116,12 +117,12 @@ function buildDescriptor(entry: IDescriptor, path: string): PropertyDescriptor { Object.keys(entry) .filter((key): key is OtherInvocationKey => key.startsWith('_$otherInvocation')) // Not supported currently - .filter((key)=> !key.includes('new()')) + .filter(key => !key.includes('new()')) .forEach(key => OtherInvocationsTracker.addOtherInvocation(path, key, entry[key])); // use function call just to get a function that doesn't create prototypes on new // bind to an empty object so we don't modify the original - attrs.value = new Proxy(Function.prototype.call.bind({}), { + attrs.value = new Proxy(function () {}, { apply(_target, thisArg) { const invocation = OtherInvocationsTracker.getOtherInvocation(path, thisArg)?.invocation ?? @@ -164,13 +165,13 @@ function buildDescriptor(entry: IDescriptor, path: string): PropertyDescriptor { if (propName.startsWith('Symbol(')) { propName = globalSymbols[propName]; if (!propName) { - const symbolName = (propName as string).match(/Symbol\((.+)\)/)[1]; + const symbolName = (propName as string).match(/Symbol\((.+)\)/)![1]; propName = Symbol.for(symbolName); } } let descriptor: PropertyDescriptor; if (propName === 'prototype') { - descriptor = prototypeDescriptor; + descriptor = prototypeDescriptor!; } else { descriptor = buildDescriptor(value, `${path}.${prop}`); } @@ -197,9 +198,10 @@ function breakdownPath(path: string, propsToLeave) { const parts = path.split(/\.Symbol\(([\w.]+)\)|\.(\w+)/).filter(Boolean); let obj: any = self; while (parts.length > propsToLeave) { - let next: string | symbol = parts.shift(); + let next: string | symbol | undefined = parts.shift(); + if (next === undefined) throw new Error('Reached end of parts without finding obj'); if (next === 'window') continue; - if (next.startsWith('Symbol.')) next = Symbol.for(next); + if (next?.startsWith('Symbol.')) next = Symbol.for(next); obj = obj[next]; if (!obj) { throw new Error(`Property not found -> ${path} at ${String(next)}`); @@ -264,7 +266,9 @@ class PathToInstanceTracker { } private static getInstanceForPath(path: string) { - const { parent, property } = getParentAndProperty(path); + const result = getParentAndProperty(path); + if (!result) throw new Error('no parent and property found'); + const { parent, property } = result; return parent[property]; } } @@ -283,11 +287,7 @@ class OtherInvocationsTracker { { invocation: any; isAsync: boolean } >(); - static addOtherInvocation( - basePath: string, - otherKey: OtherInvocationKey, - otherInvocation: any, - ) { + static addOtherInvocation(basePath: string, otherKey: OtherInvocationKey, otherInvocation: any) { const [invocationKey, ...otherParts] = otherKey.split('.'); // Remove key/property from path const otherPath = otherParts.slice(0, -1).join('.'); @@ -303,7 +303,7 @@ class OtherInvocationsTracker { static getOtherInvocation( basePath: string, otherThis: any, - ): { invocation: any; path: string; isAsync: boolean } { + ): { invocation: any; path: string; isAsync?: boolean } | undefined { const otherPath = PathToInstanceTracker.getPath(otherThis); if (!otherPath) { return; diff --git a/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts b/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts index d188c51aa..1c7cc462f 100644 --- a/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts +++ b/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts @@ -1,3 +1,23 @@ +// Injected by DomOverridesBuilder +declare let sourceUrl: string; +declare let targetType: string | undefined; +declare let args: any; + +type SharedStorage = { ready: boolean }; +function getSharedStorage(): SharedStorage | undefined { + try { + return Function.prototype.toString.call(sourceUrl) as SharedStorage; + } catch { + return undefined; + } +} + +// Make sure we run our logic only once, see toString proxy for how this storage works. +if (getSharedStorage()?.ready) { + // @ts-expect-error + return; +} + /////// MASK TO STRING //////////////////////////////////////////////////////////////////////////////////////////////// // eslint-disable-next-line prefer-const -- must be let: could change for different browser (ie, Safari) @@ -8,7 +28,10 @@ const overriddenFns = new Map(); const proxyToTarget = new WeakMap(); // From puppeteer-stealth: this is to prevent someone snooping at Reflect calls -const ReflectCached = { +const ReflectCached: Pick< + typeof Reflect, + 'construct' | 'get' | 'set' | 'apply' | 'setPrototypeOf' | 'ownKeys' | 'getOwnPropertyDescriptor' +> = { construct: Reflect.construct.bind(Reflect), get: Reflect.get.bind(Reflect), set: Reflect.set.bind(Reflect), @@ -19,8 +42,19 @@ const ReflectCached = { }; const ErrorCached = Error; - -const ObjectCached = { +const ObjectCached: Pick< + ObjectConstructor, + | 'setPrototypeOf' + | 'getPrototypeOf' + | 'defineProperty' + | 'create' + | 'entries' + | 'values' + | 'keys' + | 'getOwnPropertyDescriptors' + | 'getOwnPropertyDescriptor' + | 'hasOwn' +> = { setPrototypeOf: Object.setPrototypeOf.bind(Object), getPrototypeOf: Object.getPrototypeOf.bind(Object), defineProperty: Object.defineProperty.bind(Object), @@ -30,6 +64,7 @@ const ObjectCached = { keys: Object.keys.bind(Object), getOwnPropertyDescriptors: Object.getOwnPropertyDescriptors.bind(Object), getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor.bind(Object), + hasOwn: Object.hasOwn.bind(Object), }; // Store External proxies as undefined so we can treat it as just missing @@ -61,7 +96,12 @@ function runAndInjectProxyInStack(target: any, thisArg: any, argArray: any, prox // by later mapping `Internal-${id}` to the value we stored in our map. const wrapper = { [name]() { - return ReflectCached.apply(target, thisArg, argArray); + // eslint-disable-next-line no-useless-catch + try { + return ReflectCached.apply(target, thisArg, argArray); + } catch (e) { + throw e; + } }, }; @@ -71,6 +111,8 @@ function runAndInjectProxyInStack(target: any, thisArg: any, argArray: any, prox (function trackProxyInstances() { if (typeof self === 'undefined') return; const descriptor = ObjectCached.getOwnPropertyDescriptor(self, 'Proxy'); + if (!descriptor) return; + const toString = descriptor.value.toString(); descriptor.value = new Proxy(descriptor.value, { construct(target: any, argArray: any[], newTarget: Function): object { @@ -83,12 +125,17 @@ function runAndInjectProxyInStack(target: any, thisArg: any, argArray: any, prox ObjectCached.defineProperty(self, 'Proxy', descriptor); })(); +const sharedStorage: SharedStorage = { ready: false }; const fnToStringDescriptor = ObjectCached.getOwnPropertyDescriptor(Function.prototype, 'toString'); const fnToStringProxy = internalCreateFnProxy({ target: Function.prototype.toString, descriptor: fnToStringDescriptor, inner: { - apply: (target, thisArg, args) => { + apply: (target, thisArg, applyArgs) => { + // Can be empty in some tests + const hiddenKey = typeof sourceUrl === 'string' ? sourceUrl : 'testing'; + if (thisArg === hiddenKey) return sharedStorage; + const storedToString = overriddenFns.get(thisArg); if (storedToString) { return storedToString; @@ -101,11 +148,11 @@ const fnToStringProxy = internalCreateFnProxy({ ); if (hasSameProto === false) { // Pass the call on to the local Function.prototype.toString instead - return thisArg.toString(...(args ?? [])); + return thisArg.toString(...(applyArgs ?? [])); } } - return runAndInjectProxyInStack(target, thisArg, args, thisArg); + return runAndInjectProxyInStack(target, thisArg, applyArgs, thisArg); }, }, }); @@ -117,6 +164,8 @@ ObjectCached.defineProperty(Function.prototype, 'toString', { /////// END TOSTRING ////////////////////////////////////////////////////////////////////////////////////////////////// +// TODO remove this, but make sure we clean this up in: +// hero/core/injected-scripts/domOverride_openShadowRoots.ts enum ProxyOverride { callOriginal = '_____invoke_original_____', } @@ -124,35 +173,28 @@ enum ProxyOverride { function proxyConstructor( owner: T, key: K, - overrideFn: ( - target?: T[K], - argArray?: T[K] extends new (...args: infer P) => any ? P : never[], - newTarget?: T[K], - ) => (T[K] extends new () => infer Z ? Z : never) | ProxyOverride, + overrideFn: typeof ReflectCached.construct, ) { const descriptor = ObjectCached.getOwnPropertyDescriptor(owner, key); + if (!descriptor) throw new Error(`Descriptor with key ${String(key)} not found`); + const toString = descriptor.value.toString(); descriptor.value = new Proxy(descriptor.value, { - construct() { - const result = overrideFn(...arguments); - if (result !== ProxyOverride.callOriginal) { - return result as any; - } - - return ReflectCached.construct(...arguments); + construct(target, argArray, newTarget) { + return overrideFn(target, argArray, newTarget); }, }); overriddenFns.set(descriptor.value, toString); ObjectCached.defineProperty(owner, key, descriptor); } -const setProtoTracker = new WeakSet(); +const setProtoTracker = new WeakSet(); -function internalCreateFnProxy(opts: { +function internalCreateFnProxy(opts: { target: T; descriptor?: any; custom?: ProxyHandler; - inner?: ProxyHandler & { disableGetProxyOnFunction?: boolean }; + inner?: ProxyHandler & { fixThisArg?: boolean }; disableStoreToString?: boolean; }) { function apply(target: any, thisArg: any, argArray: any[]) { @@ -176,7 +218,7 @@ function internalCreateFnProxy(opts: { ErrorCached.captureStackTrace(temp); const stack = temp.stack.split('\n'); - const isFromReflect = stack.at(1).includes('Reflect.setPrototypeOf'); + const isFromReflect = stack.at(1)?.includes('Reflect.setPrototypeOf'); try { const caller = isFromReflect ? ReflectCached : ObjectCached; return caller.setPrototypeOf(target, protoTarget); @@ -199,11 +241,20 @@ function internalCreateFnProxy(opts: { ? opts.inner.get(target, p, receiver) : ReflectCached.get(target, p, receiver); - if (typeof value === 'function' && !opts.inner.disableGetProxyOnFunction) { + if (typeof value === 'function') { return internalCreateFnProxy({ target: value, inner: { + fixThisArg: opts.inner?.fixThisArg, apply: (fnTarget, fnThisArg, fnArgArray) => { + // Make sure we use the correct thisArg, but only for things that we didn't mean to replace + // overriddenFns.has(fnTarget); + // const proto = getPrototypeSafe(fnThisArg); + // const shouldHide = + // overriddenFns.has(fnThisArg) || proto ? overriddenFns.has(proto) : false; + if (opts.inner?.fixThisArg && fnThisArg === receiver) { + return runAndInjectProxyInStack(fnTarget, target, fnArgArray, proxy); + } return runAndInjectProxyInStack(fnTarget, fnThisArg, fnArgArray, proxy); }, }, @@ -223,7 +274,7 @@ function internalCreateFnProxy(opts: { const result = opts.inner?.set ? opts.inner.set(target, p, value, receiver) - : ReflectCached.set(...arguments); + : ReflectCached.set(target, p, value, receiver); return result; } @@ -242,15 +293,16 @@ function internalCreateFnProxy(opts: { return proxy as any; } +type OverrideFn = (target: Function, thisArg: any, argArray: any[]) => any; + function proxyFunction( thisObject: T, functionName: K, - overrideFn: ( - target?: T[K], - thisArg?: T, - argArray?: T[K] extends (...args: infer P) => any ? P : never[], - ) => (T[K] extends (...args: any[]) => infer Z ? Z : never) | ProxyOverride, - overrideOnlyForInstance = false, + overrideFn: OverrideFn, + opts?: { + overrideOnlyForInstance?: boolean; + fixThisArg?: boolean; + }, ) { const descriptorInHierarchy = getDescriptorInHierarchy(thisObject, functionName); if (!descriptorInHierarchy) { @@ -263,10 +315,13 @@ function proxyFunction( descriptor, inner: { apply: (target, thisArg, argArray) => { - const shouldOverride = overrideOnlyForInstance === false || thisArg === thisObject; - const overrideFnToUse = shouldOverride ? overrideFn : null; - return defaultProxyApply([target, thisArg, argArray], overrideFnToUse); + const shouldOverride = !opts?.overrideOnlyForInstance || thisArg === thisObject; + if (shouldOverride) { + return overrideFn(target, thisArg, argArray); + } + return ReflectCached.apply(target, thisArg, argArray); }, + fixThisArg: opts?.fixThisArg, }, }); return thisObject[functionName]; @@ -275,8 +330,8 @@ function proxyFunction( function proxyGetter( thisObject: T, propertyName: K, - overrideFn: (target?: T[K], thisArg?: T) => T[K] | ProxyOverride, - overrideOnlyForInstance = false, + overrideFn: OverrideFn, + opts?: { overrideOnlyForInstance?: boolean }, ) { const descriptorInHierarchy = getDescriptorInHierarchy(thisObject, propertyName); if (!descriptorInHierarchy) { @@ -284,15 +339,23 @@ function proxyGetter( } const { descriptorOwner, descriptor } = descriptorInHierarchy; + // TODO we might still want to allow this? + if (!descriptor.get) { + throw new Error('Trying to apply a proxy getter on something that doesnt have a getter'); + } descriptor.get = internalCreateFnProxy({ target: descriptor.get, descriptor, inner: { apply: (target, thisArg, argArray) => { - const shouldOverride = overrideOnlyForInstance === false || thisArg === thisObject; - const overrideFnToUse = shouldOverride ? overrideFn : null; - return defaultProxyApply([target, thisArg, argArray], overrideFnToUse); + const shouldOverride = !opts?.overrideOnlyForInstance || thisArg === thisObject; + if (shouldOverride) { + const result = overrideFn(target, thisArg, argArray); + // TODO remove + if (result !== ProxyOverride.callOriginal) return result; + } + return ReflectCached.apply(target, thisArg, argArray); }, }, }); @@ -303,26 +366,27 @@ function proxyGetter( function proxySetter( thisObject: T, propertyName: K, - overrideFn: ( - target?: T[K], - thisArg?: T, - value?: T[K] extends (value: infer P) => any ? P : never, - ) => void | ProxyOverride, - overrideOnlyForInstance = false, + overrideFn: OverrideFn, + opts?: { overrideOnlyForInstance?: boolean }, ) { const descriptorInHierarchy = getDescriptorInHierarchy(thisObject, propertyName); if (!descriptorInHierarchy) { throw new Error(`Could not find descriptor for setter: ${String(propertyName)}`); } const { descriptorOwner, descriptor } = descriptorInHierarchy; + // TODO we might still want to allow this? + if (!descriptor.set) { + throw new Error('Trying to apply a proxy setter on something that doesnt have a setter'); + } + descriptor.set = internalCreateFnProxy({ target: descriptor.set, descriptor, inner: { apply: (target, thisArg, argArray) => { - if (!overrideOnlyForInstance || thisArg === thisObject) { - const result = overrideFn(target, thisArg, ...argArray); - if (result !== ProxyOverride.callOriginal) return result; + const shouldOverride = !opts?.overrideOnlyForInstance || thisArg === thisObject; + if (shouldOverride) { + return overrideFn(target, thisArg, argArray); } return ReflectCached.apply(target, thisArg, argArray); }, @@ -332,30 +396,15 @@ function proxySetter( return descriptor.set; } -function defaultProxyApply( - args: [target: any, thisArg: T, argArray: any[]], - overrideFn?: (target?: T[K], thisArg?: T, argArray?: any[]) => T[K] | ProxyOverride, -): any { - let result: T[K] | ProxyOverride = ProxyOverride.callOriginal; - if (overrideFn) { - result = overrideFn(...args); - } - - if (result === ProxyOverride.callOriginal) { - result = ReflectCached.apply(...args); - } - - return result; -} - function getDescriptorInHierarchy(obj: T, prop: K) { let proto = obj; do { if (!proto) return null; - if (proto.hasOwnProperty(prop)) { + const descriptor = ObjectCached.getOwnPropertyDescriptor(proto, prop); + if (descriptor) { return { descriptorOwner: proto, - descriptor: ObjectCached.getOwnPropertyDescriptor(proto, prop), + descriptor, }; } proto = ObjectCached.getPrototypeOf(proto); @@ -385,7 +434,7 @@ function addDescriptorAfterProperty( const inHierarchy = getDescriptorInHierarchy(owner, propertyName); if (inHierarchy && descriptor.value) { if (inHierarchy.descriptor.get) { - proxyGetter(owner, propertyName, () => descriptor.value, true); + proxyGetter(owner, propertyName, () => descriptor.value, { overrideOnlyForInstance: true }); } else { throw new Error("Can't override descriptor that doesnt have a getter"); } @@ -417,7 +466,9 @@ const reordersByObject = new WeakMap< >(); proxyFunction(Object, 'getOwnPropertyDescriptors', (target, thisArg, argArray) => { - const descriptors = ReflectCached.apply(target, thisArg, argArray); + const descriptors = ReflectCached.apply(target, thisArg!, argArray!) as ReturnType< + ObjectConstructor['getOwnPropertyDescriptors'] + >; const reorders = reordersByObject.get(thisArg) ?? reordersByObject.get(argArray?.[0]); if (reorders) { const keys = Object.keys(descriptors); @@ -434,7 +485,7 @@ proxyFunction(Object, 'getOwnPropertyDescriptors', (target, thisArg, argArray) = }); proxyFunction(Object, 'getOwnPropertyNames', (target, thisArg, argArray) => { - const keys = ReflectCached.apply(target, thisArg, argArray); + const keys = ReflectCached.apply(target, thisArg!, argArray!); const reorders = reordersByObject.get(thisArg) ?? reordersByObject.get(argArray?.[0]); if (reorders) { @@ -446,7 +497,7 @@ proxyFunction(Object, 'getOwnPropertyNames', (target, thisArg, argArray) => { }); proxyFunction(Object, 'keys', (target, thisArg, argArray) => { - const keys = ReflectCached.apply(target, thisArg, argArray); + const keys = ReflectCached.apply(target, thisArg!, argArray!); const reorders = reordersByObject.get(thisArg) ?? reordersByObject.get(argArray?.[0]); if (reorders) { @@ -458,25 +509,49 @@ proxyFunction(Object, 'keys', (target, thisArg, argArray) => { }); (['call', 'apply'] as const).forEach(key => { - proxyFunction(Function.prototype, key, (target, thisArg, argArray) => { - const originalThis = argArray.at(0); - return runAndInjectProxyInStack(target, thisArg, argArray, originalThis); - }); + proxyFunction( + Function.prototype, + key, + (target, thisArg, argArray) => { + const originalThis = argArray.at(0); + return runAndInjectProxyInStack(target, thisArg, argArray, originalThis); + }, + { fixThisArg: true }, + ); }); -proxyFunction(Function.prototype, 'bind', (target, thisArg, argArray) => { - const result = ReflectCached.apply(target, thisArg, argArray); - const proxy = internalCreateFnProxy({ - target: result, - inner: { - apply(innerTarget, innerThisArg, innerArgArray) { - const originalThis = argArray.at(0); - return runAndInjectProxyInStack(innerTarget, innerThisArg, innerArgArray, originalThis); +const boundStuff = new WeakSet(); +proxyFunction( + Function.prototype, + 'bind', + (target, thisArg, argArray) => { + // const originalThis = argArray.at(0); + // return runAndInjectProxyInStack(target, thisArg, argArray, originalThis); + + const result = ReflectCached.apply(target, thisArg, argArray); + + if (boundStuff.has(thisArg)) return result; + + const proxy = internalCreateFnProxy({ + target: result, + inner: { + apply(innerTarget, innerThisArg, innerArgArray) { + const originalThis = argArray.at(0); + return runAndInjectProxyInStack(innerTarget, innerThisArg, innerArgArray, originalThis); + }, + fixThisArg: true, + // disableGetProxyOnFunction: true, }, - }, - }); - return proxy; -}); + // disableStoreToString: true, + }); + + boundStuff.add(proxy); + // const proxy = result; + // const proxy = new Proxy(result, {}); + return proxy; + }, + { fixThisArg: true }, +); function reorderNonConfigurableDescriptors( objectPath, @@ -486,7 +561,7 @@ function reorderNonConfigurableDescriptors( ) { const objectAtPath = getObjectAtPath(objectPath); if (!reordersByObject.has(objectAtPath)) reordersByObject.set(objectAtPath, []); - const reorders = reordersByObject.get(objectAtPath); + const reorders = reordersByObject.get(objectAtPath)!; reorders.push({ prevProperty, propertyName, throughProperty }); } @@ -529,8 +604,3 @@ if (typeof module === 'object' && typeof module.exports === 'object') { proxyFunction, }; } - -// Injected by DomOverridesBuilder -declare let sourceUrl: string; -declare let targetType: string | undefined; -declare let args: any; diff --git a/plugins/default-browser-emulator/injected-scripts/console.ts b/plugins/default-browser-emulator/injected-scripts/console.ts index ec82a59a4..ce83cb3a9 100644 --- a/plugins/default-browser-emulator/injected-scripts/console.ts +++ b/plugins/default-browser-emulator/injected-scripts/console.ts @@ -5,14 +5,15 @@ export type Args = { const typedArgs = args as Args; ObjectCached.keys(console).forEach(key => { - proxyFunction(console, key, (target, thisArg, args) => { + const typedKey = key as keyof typeof console; + proxyFunction(console, typedKey, (target, thisArg, args) => { if (typedArgs.mode === 'disableConsole') return undefined; args = replaceErrorStackWithOriginal(args); - return ReflectCached.apply(target, thisArg, args); + return ReflectCached.apply(target, thisArg!, args!); }); }); -const defaultErrorStackGetter = Object.getOwnPropertyDescriptor(new Error(''), 'stack').get; +const defaultErrorStackGetter = ObjectCached.getOwnPropertyDescriptor(new Error(''), 'stack')!.get; function replaceErrorStackWithOriginal(object: unknown) { if (!object || typeof object !== 'object') { @@ -21,12 +22,15 @@ function replaceErrorStackWithOriginal(object: unknown) { if (object instanceof Error) { const nameDesc = - Object.getOwnPropertyDescriptor(object, 'name') ?? - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(object), 'name'); - const { message: msgDesc, stack: stackDesc } = Object.getOwnPropertyDescriptors(object); + ObjectCached.getOwnPropertyDescriptor(object, 'name') ?? + ObjectCached.getOwnPropertyDescriptor(ObjectCached.getPrototypeOf(object), 'name'); - const isSafeName = nameDesc.hasOwnProperty('value'); - const isSafeMsg = msgDesc.hasOwnProperty('value'); + const msgDesc = ObjectCached.getOwnPropertyDescriptor(object, 'message'); + const stackDesc = ObjectCached.getOwnPropertyDescriptor(object, 'stack'); + + const isSafeName = nameDesc!.hasOwnProperty('value'); + // const isSafeName = ObjectCached.hasOwn(nameDesc ?? {}, 'value'); + const isSafeMsg = ObjectCached.hasOwn(msgDesc ?? {}, 'value'); const isSafeStack = stackDesc?.get === defaultErrorStackGetter; if (isSafeName && isSafeMsg && isSafeStack) { diff --git a/plugins/default-browser-emulator/injected-scripts/error.ts b/plugins/default-browser-emulator/injected-scripts/error.ts index ab9a7b889..04f3c2301 100644 --- a/plugins/default-browser-emulator/injected-scripts/error.ts +++ b/plugins/default-browser-emulator/injected-scripts/error.ts @@ -14,19 +14,33 @@ if (typeof self === 'undefined') { } let stackTracelimit = Error.stackTraceLimit; -Error.stackTraceLimit = 200; +Error.stackTraceLimit = 1000; let customPrepareStackTrace: any | undefined; const proxyThisTrackerHere = proxyThisTracker; const proxyToTargetHere = proxyToTarget; +const proxyFunctionHere = proxyFunction; +const ReflectCachedHere = ReflectCached; +const internalCreateFnProxyHere = internalCreateFnProxy; const getPrototypeSafeHere = getPrototypeSafe; -function prepareStackAndStackTraces(error: Error, stackTraces?: NodeJS.CallSite[]) { + +type CorrectData = { getMethodName: string | null; getTypeName: string; toString: string }; +const stackTraceCorrectData = new WeakMap(); + +function prepareStackAndStackTraces( + error: Error, + stackTraces?: NodeJS.CallSite[], +): { stack?: string; stackTraces: NodeJS.CallSite[] } { + // TODO, handle the case where someone already sets a custom stack let stack = error.stack; + const safeStackTraces: NodeJS.CallSite[] = []; + if (!stack) return { stack, stackTraces: safeStackTraces }; + stackTraces ??= []; + let stackTraceProto; const lines = stack.split('\n'); - const safeLines = []; - const safeStackTraces = []; + const safeLines: string[] = []; // Chrome debugger generates these things one the fly for every letter you type in // devtools so it can dynamically generates previews, but this is super annoying when // working with debug points. Also we don't need to modify them because they only contain @@ -41,6 +55,7 @@ function prepareStackAndStackTraces(error: Error, stackTraces?: NodeJS.CallSite[ for (let i = 1; i < lines.length; i++) { let line = lines[i]; const stackTrace = stackTraces.at(i - 1); + if (stackTrace && !stackTraceProto) stackTraceProto = createStackTraceProto(stackTrace); // First line doesnt count for limit if (safeLines.length > stackTracelimit && typedArgs.applyStackTraceLimit) break; @@ -51,6 +66,7 @@ function prepareStackAndStackTraces(error: Error, stackTraces?: NodeJS.CallSite[ continue; } + // eslint-disable-next-line @typescript-eslint/no-loop-func const hideProxyLogic = () => { if (!typedArgs.modifyWrongProxyAndObjectString) return; if (!line.includes('Proxy') && !line.includes('Object')) return; @@ -75,8 +91,16 @@ function prepareStackAndStackTraces(error: Error, stackTraces?: NodeJS.CallSite[ const replacement = typeof originalThis === 'function' ? 'Function' : 'Object'; // Make sure to replace Object first, so we don't accidentally replace it. - line = line.replace('Object', replacement); - line = line.replace('Proxy', replacement); + line = line.replace('Object', replacement).replace('Proxy', replacement); + + if (stackTrace) { + stackTraceCorrectData.set(stackTrace, { + getMethodName: stackTrace.getFunctionName(), + getTypeName: replacement, + toString: line.trimStart().replace('at ', ''), + }); + Object.setPrototypeOf(stackTrace, stackTraceProto); + } }; hideProxyLogic(); @@ -91,6 +115,40 @@ function prepareStackAndStackTraces(error: Error, stackTraces?: NodeJS.CallSite[ return { stack, stackTraces: safeStackTraces }; } +function createStackTraceProto(stackTrace: NodeJS.CallSite) { + const proto = {}; + // Keep lower proto's the same + Object.setPrototypeOf(proto, Object.getPrototypeOf(Object.getPrototypeOf(stackTrace))); + + const createProxy = (key: keyof CorrectData) => + internalCreateFnProxy({ + target() {}, + inner: { + apply: (target, thisArg, argArray) => { + const correctData = stackTraceCorrectData.get(thisArg); + if (correctData) return correctData[key]; + return ReflectCachedHere.apply(target, thisArg, argArray); + }, + }, + }); + + // We need to create a new prototype because descriptors are froozen + Object.entries(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(stackTrace))).forEach( + ([key, desc]) => { + if (key === 'getMethodName') { + desc.value = createProxy('getMethodName'); + } else if (key === 'getTypeName') { + desc.value = createProxy('getTypeName'); + } else if (key === 'toString') { + desc.value = createProxy('toString'); + } + Object.defineProperty(proto, key, desc); + }, + ); + + return proto; +} + Error.prepareStackTrace = (error, stackTraces) => { const { stack, stackTraces: safeStackTraces } = prepareStackAndStackTraces(error, stackTraces); @@ -117,8 +175,6 @@ const ErrorProxy = internalCreateFnProxy({ target: Error, inner: { get: (target, p, receiver) => { - // Special property that other plugins can use to see if injected scripts are loaded - if (p === sourceUrl) return true; if (p === 'prepareStackTrace') return customPrepareStackTrace; if (p === 'stackTraceLimit') return stackTracelimit; return ReflectCached.get(target, p, receiver); diff --git a/plugins/default-browser-emulator/injected-scripts/navigator.deviceMemory.ts b/plugins/default-browser-emulator/injected-scripts/navigator.deviceMemory.ts index b4df87adf..c5afb2da1 100644 --- a/plugins/default-browser-emulator/injected-scripts/navigator.deviceMemory.ts +++ b/plugins/default-browser-emulator/injected-scripts/navigator.deviceMemory.ts @@ -1,30 +1,42 @@ +export type Args = { + memory: number; + maxHeapSize: number; + storageTib: number; +}; +const typedArgs = args as Args; + if ( 'WorkerGlobalScope' in self || self.location.protocol === 'https:' || 'deviceMemory' in navigator ) { - // @ts-ignore - proxyGetter(self.navigator, 'deviceMemory', () => args.memory, true); + proxyGetter( + self.navigator, + // @ts-expect-error + 'deviceMemory', + () => typedArgs.memory, + { overrideOnlyForInstance: true }, + ); } if ('WorkerGlobalScope' in self || self.location.protocol === 'https:') { - if ('storage' in navigator && navigator.storage && args.storageTib) { + if ('storage' in navigator && navigator.storage && typedArgs.storageTib) { proxyFunction( self.navigator.storage, 'estimate', async (target, thisArg, argArray) => { const result = await ReflectCached.apply(target, thisArg, argArray); - result.quota = Math.round(args.storageTib * 1024 * 1024 * 1024 * 1024 * 0.5); + result.quota = Math.round(typedArgs.storageTib * 1024 * 1024 * 1024 * 1024 * 0.5); return result; }, - true, + { overrideOnlyForInstance: true }, ); } if ( 'webkitTemporaryStorage' in navigator && 'queryUsageAndQuota' in (navigator as any).webkitTemporaryStorage && - args.storageTib + typedArgs.storageTib ) { proxyFunction( (self.navigator as any).webkitTemporaryStorage, @@ -34,36 +46,36 @@ if ('WorkerGlobalScope' in self || self.location.protocol === 'https:') { usage => { (argArray[0] as any)( usage, - Math.round(args.storageTib * 1024 * 1024 * 1024 * 1024 * 0.5), + Math.round(typedArgs.storageTib * 1024 * 1024 * 1024 * 1024 * 0.5), ); }, ]); }, - true, + { overrideOnlyForInstance: true }, ); } if ('memory' in performance && (performance as any).memory) { proxyGetter( self.performance, 'memory' as any, - function () { - const result = ReflectCached.apply(...arguments); - proxyGetter(result, 'jsHeapSizeLimit', () => args.maxHeapSize); + (...args) => { + const result = ReflectCached.apply(...args); + proxyGetter(result, 'jsHeapSizeLimit', () => typedArgs.maxHeapSize); return result; }, - true, + { overrideOnlyForInstance: true }, ); } if ('memory' in console && (console as any).memory) { proxyGetter( self.console, 'memory' as any, - function () { - const result = ReflectCached.apply(...arguments); - proxyGetter(result, 'jsHeapSizeLimit', () => args.maxHeapSize); + (...args) => { + const result = ReflectCached.apply(...args); + proxyGetter(result, 'jsHeapSizeLimit', () => typedArgs.maxHeapSize); return result; }, - true, + { overrideOnlyForInstance: true }, ); } } diff --git a/plugins/default-browser-emulator/injected-scripts/navigator.hardwareConcurrency.ts b/plugins/default-browser-emulator/injected-scripts/navigator.hardwareConcurrency.ts index c15da7125..a29e1532e 100644 --- a/plugins/default-browser-emulator/injected-scripts/navigator.hardwareConcurrency.ts +++ b/plugins/default-browser-emulator/injected-scripts/navigator.hardwareConcurrency.ts @@ -1 +1,8 @@ -proxyGetter(self.navigator, 'hardwareConcurrency', () => args.concurrency, true); +export type Args = { + concurrency: number; +}; +const typedArgs = args as Args; + +proxyGetter(self.navigator, 'hardwareConcurrency', () => typedArgs.concurrency, { + overrideOnlyForInstance: true, +}); diff --git a/plugins/default-browser-emulator/injected-scripts/navigator.ts b/plugins/default-browser-emulator/injected-scripts/navigator.ts index b0accdd1d..2d5bd93e4 100644 --- a/plugins/default-browser-emulator/injected-scripts/navigator.ts +++ b/plugins/default-browser-emulator/injected-scripts/navigator.ts @@ -1,35 +1,50 @@ -if (args.userAgentString) { - proxyGetter(self.navigator, 'userAgent', () => args.userAgentString, true); +export type Args = { + userAgentString: string; + platform: string; + headless: boolean; + pdfViewerEnabled: boolean; + userAgentData: any; + rtt: number; +}; +const typedArgs = args as Args; + +if (typedArgs.userAgentString) { + proxyGetter(self.navigator, 'userAgent', () => typedArgs.userAgentString, { + overrideOnlyForInstance: true, + }); proxyGetter( self.navigator, 'appVersion', - () => args.userAgentString.replace('Mozilla/', ''), - true, + () => typedArgs.userAgentString.replace('Mozilla/', ''), + { overrideOnlyForInstance: true }, ); } if ('NetworkInformation' in self) { - proxyGetter((self.NetworkInformation as any).prototype as any, 'rtt', () => args.rtt, false); + proxyGetter((self.NetworkInformation as any).prototype as any, 'rtt', () => typedArgs.rtt); } -if (args.userAgentData && 'userAgentData' in self.navigator) { +if (typedArgs.userAgentData && 'userAgentData' in self.navigator) { const userAgentData = self.navigator.userAgentData as any; function checkThisArg(thisArg, customMessage = '') { - // @ts-expect-error - if (Object.getPrototypeOf(thisArg) !== self.NavigatorUAData.prototype) { + if ( + Object.getPrototypeOf(thisArg) !== + // @ts-expect-error + self.NavigatorUAData.prototype + ) { throw new TypeError(`${customMessage}Illegal invocation`); } } proxyGetter(userAgentData, 'brands', (_, thisArg) => { checkThisArg(thisArg); - const clonedValues = args.userAgentData.brands.map(x => ({ ...x })); + const clonedValues = typedArgs.userAgentData.brands.map(x => ({ ...x })); return Object.seal(Object.freeze(clonedValues)); }); proxyGetter(userAgentData, 'platform', (_, thisArg) => { checkThisArg(thisArg); - return args.userAgentData.platform; + return typedArgs.userAgentData.platform; }); proxyFunction(userAgentData, 'getHighEntropyValues', async (target, thisArg, argArray) => { // TODO: pull Error messages directly from dom extraction files @@ -38,13 +53,13 @@ if (args.userAgentData && 'userAgentData' in self.navigator) { await ReflectCached.apply(target, thisArg, argArray); const props: any = { - brands: Object.seal(Object.freeze(args.userAgentData.brands)), + brands: Object.seal(Object.freeze(typedArgs.userAgentData.brands)), mobile: false, }; if (argArray.length && Array.isArray(argArray[0])) { for (const key of argArray[0]) { - if (key in args.userAgentData) { - props[key] = args.userAgentData[key]; + if (key in typedArgs.userAgentData) { + props[key] = typedArgs.userAgentData[key]; } } } @@ -53,12 +68,16 @@ if (args.userAgentData && 'userAgentData' in self.navigator) { }); } -if (args.pdfViewerEnabled && 'pdfViewerEnabled' in self.navigator) { - proxyGetter(self.navigator, 'pdfViewerEnabled', () => args.pdfViewerEnabled, true); +if (typedArgs.pdfViewerEnabled && 'pdfViewerEnabled' in self.navigator) { + proxyGetter(self.navigator, 'pdfViewerEnabled', () => typedArgs.pdfViewerEnabled, { + overrideOnlyForInstance: true, + }); } // always override -proxyGetter(self.navigator, 'platform', () => args.platform, true); +proxyGetter(self.navigator, 'platform', () => typedArgs.platform, { + overrideOnlyForInstance: true, +}); if ('setAppBadge' in self.navigator) { // @ts-ignore @@ -93,17 +112,17 @@ if ('clearAppBadge' in self.navigator) { }); } -if (args.headless === true && 'requestMediaKeySystemAccess' in self.navigator) { +if (typedArgs.headless === true && 'requestMediaKeySystemAccess' in self.navigator) { proxyFunction( self.navigator, 'requestMediaKeySystemAccess', async (target, thisArg, argArray) => { if (argArray.length < 2) { - return ProxyOverride.callOriginal; + return ReflectCached.apply(target, thisArg, argArray); } const [keySystem, configs] = argArray; if (keySystem !== 'com.widevine.alpha' || [...configs].length < 1) { - return ProxyOverride.callOriginal; + return ReflectCached.apply(target, thisArg, argArray); } const result = await ReflectCached.apply(target, thisArg, ['org.w3.clearkey', configs]); diff --git a/plugins/default-browser-emulator/injected-scripts/performance.ts b/plugins/default-browser-emulator/injected-scripts/performance.ts index d902b79a4..ed5fcb2e6 100644 --- a/plugins/default-browser-emulator/injected-scripts/performance.ts +++ b/plugins/default-browser-emulator/injected-scripts/performance.ts @@ -1,4 +1,5 @@ // This is here, because on Linux using Devtools, the lack of activationStart and renderBlockingStatus leads to blocking on some protections +export type Args = {}; proxyFunction( performance, diff --git a/plugins/default-browser-emulator/injected-scripts/polyfill.add.ts b/plugins/default-browser-emulator/injected-scripts/polyfill.add.ts index 366920d0d..4cdf587f2 100644 --- a/plugins/default-browser-emulator/injected-scripts/polyfill.add.ts +++ b/plugins/default-browser-emulator/injected-scripts/polyfill.add.ts @@ -1,4 +1,9 @@ -for (const itemToAdd of args.itemsToAdd || []) { +export type Args = { + itemsToAdd: any[]; +}; +const typedArgs = args as Args; + +for (const itemToAdd of typedArgs.itemsToAdd) { try { if (itemToAdd.propertyName === 'getVideoPlaybackQuality') { itemToAdd.property['_$$value()'] = function () { @@ -16,7 +21,11 @@ for (const itemToAdd of args.itemsToAdd || []) { ), ); } catch (err) { - console.log(`ERROR adding polyfill ${itemToAdd.path}.${itemToAdd.propertyName}\n${err.stack}`); + let log = `ERROR adding polyfill ${itemToAdd.path}.${itemToAdd.propertyName}`; + if (err instanceof Error) { + log += `\n${err.stack}`; + } + console.error(log); } } diff --git a/plugins/default-browser-emulator/injected-scripts/polyfill.modify.ts b/plugins/default-browser-emulator/injected-scripts/polyfill.modify.ts index b36fa982d..fa0f84e7d 100644 --- a/plugins/default-browser-emulator/injected-scripts/polyfill.modify.ts +++ b/plugins/default-browser-emulator/injected-scripts/polyfill.modify.ts @@ -1,4 +1,9 @@ -for (const itemToModify of args.itemsToModify || []) { +export type Args = { + itemsToModify: any[]; +}; +const typedArgs = args as Args; + +for (const itemToModify of typedArgs.itemsToModify) { try { if (itemToModify.propertyName === '_$function') { const func = getObjectAtPath(itemToModify.path); @@ -18,6 +23,7 @@ for (const itemToModify of args.itemsToModify || []) { } const parts = getParentAndProperty(itemToModify.path); + if (!parts) throw new Error('failed to find parent and property'); const property = parts.property; const parent = parts.parent; const descriptorInHierarchy = getDescriptorInHierarchy(parent, property); @@ -35,9 +41,9 @@ for (const itemToModify of args.itemsToModify || []) { Object.defineProperty(parent, property, descriptor); } } else if (itemToModify.propertyName === '_$get') { - overriddenFns.set(descriptor.get, itemToModify.property); + overriddenFns.set(descriptor.get!, itemToModify.property); } else if (itemToModify.propertyName === '_$set') { - overriddenFns.set(descriptor.set, itemToModify.property); + overriddenFns.set(descriptor.set!, itemToModify.property); } else if (itemToModify.propertyName.startsWith('_$otherInvocation')) { // TODO why is this needed, Im guessing since this is one big dump? const ReflectCachedHere = ReflectCached; @@ -65,9 +71,11 @@ for (const itemToModify of args.itemsToModify || []) { ); } } catch (err) { - console.log( - `WARN: error changing prop ${itemToModify.path}.${itemToModify.propertyName}\n${err.stack}`, - ); + let log = `ERROR changing prop ${itemToModify.path}.${itemToModify.propertyName}`; + if (err instanceof Error) { + log += `\n${err.stack}`; + } + console.error(log); } } diff --git a/plugins/default-browser-emulator/injected-scripts/polyfill.remove.ts b/plugins/default-browser-emulator/injected-scripts/polyfill.remove.ts index 1c7412a9f..e44d9c60a 100644 --- a/plugins/default-browser-emulator/injected-scripts/polyfill.remove.ts +++ b/plugins/default-browser-emulator/injected-scripts/polyfill.remove.ts @@ -1,8 +1,17 @@ -for (const itemToRemove of args.itemsToRemove || []) { +export type Args = { + itemsToRemove: any[]; +}; +const typedArgs = args as Args; + +for (const itemToRemove of typedArgs.itemsToRemove) { try { const parent = getObjectAtPath(itemToRemove.path); delete parent[itemToRemove.propertyName]; } catch (err) { - console.log(`ERROR deleting path ${itemToRemove.path}.${itemToRemove.propertyName}\n${err.toString()}`); + let log = `ERROR deleting prop ${itemToRemove.path}.${itemToRemove.propertyName}`; + if (err instanceof Error) { + log += `\n${err.stack}`; + } + console.error(log); } } diff --git a/plugins/default-browser-emulator/injected-scripts/polyfill.reorder.ts b/plugins/default-browser-emulator/injected-scripts/polyfill.reorder.ts index 96979bca2..9d070e142 100644 --- a/plugins/default-browser-emulator/injected-scripts/polyfill.reorder.ts +++ b/plugins/default-browser-emulator/injected-scripts/polyfill.reorder.ts @@ -1,4 +1,9 @@ -for (const { propertyName, prevProperty, throughProperty, path } of args.itemsToReorder || []) { +export type Args = { + itemsToReorder: any[]; +}; +const typedArgs = args as Args; + +for (const { propertyName, prevProperty, throughProperty, path } of typedArgs.itemsToReorder) { try { if (!path.includes('.prototype')) { reorderNonConfigurableDescriptors(path, propertyName, prevProperty, throughProperty); @@ -6,6 +11,10 @@ for (const { propertyName, prevProperty, throughProperty, path } of args.itemsTo } reorderDescriptor(path, propertyName, prevProperty, throughProperty); } catch (err) { - console.log(`ERROR adding order polyfill ${path}->${propertyName}\n${err.toString()}`); + let log = `ERROR adding order polyfill ${path}->${propertyName}`; + if (err instanceof Error) { + log += `\n${err.stack}`; + } + console.error(log); } } diff --git a/plugins/default-browser-emulator/injected-scripts/speechSynthesis.getVoices.ts b/plugins/default-browser-emulator/injected-scripts/speechSynthesis.getVoices.ts index e2844640a..05ca2a2f7 100644 --- a/plugins/default-browser-emulator/injected-scripts/speechSynthesis.getVoices.ts +++ b/plugins/default-browser-emulator/injected-scripts/speechSynthesis.getVoices.ts @@ -1,17 +1,22 @@ +export type Args = { + voices: any; +}; +const typedArgs = args as Args; + if ('speechSynthesis' in self) { // @ts-ignore - const { voices } = args; + const { voices } = typedArgs; proxyFunction(speechSynthesis, 'getVoices', (func, thisObj, ...args) => { const original = ReflectCached.apply(func, thisObj, args); if (!original.length) return original; const speechProto = ObjectCached.getPrototypeOf(original[0]); return voices.map(x => { const voice: SpeechSynthesisVoice = Object.create(speechProto); - proxyGetter(voice, 'name', () => x.name, true); - proxyGetter(voice, 'lang', () => x.lang, true); - proxyGetter(voice, 'default', () => x.default, true); - proxyGetter(voice, 'voiceURI', () => x.voiceURI, true); - proxyGetter(voice, 'localService', () => x.localService, true); + proxyGetter(voice, 'name', () => x.name, { overrideOnlyForInstance: true }); + proxyGetter(voice, 'lang', () => x.lang, { overrideOnlyForInstance: true }); + proxyGetter(voice, 'default', () => x.default, { overrideOnlyForInstance: true }); + proxyGetter(voice, 'voiceURI', () => x.voiceURI, { overrideOnlyForInstance: true }); + proxyGetter(voice, 'localService', () => x.localService, { overrideOnlyForInstance: true }); return voice; }); }); diff --git a/plugins/default-browser-emulator/injected-scripts/tsconfig.json b/plugins/default-browser-emulator/injected-scripts/tsconfig.json index 2e0508cb5..b3d8c5a20 100644 --- a/plugins/default-browser-emulator/injected-scripts/tsconfig.json +++ b/plugins/default-browser-emulator/injected-scripts/tsconfig.json @@ -2,11 +2,13 @@ "extends": "../../../tsconfig.json", "compileOnSave": true, "compilerOptions": { + "strict": true, "composite": true, "sourceMap": false, "inlineSourceMap": false, "removeComments": true, - "strictBindCallApply": false + "strictBindCallApply": false, + "lib": ["es2022", "dom"], }, "include": ["*.ts", "*.js", ".eslintrc.js"], "exclude": ["**/tsconfig*.json", "**/node_modules", "**/dist", "**/build*", "**/injected-scripts"] diff --git a/plugins/default-browser-emulator/injected-scripts/webrtc.ts b/plugins/default-browser-emulator/injected-scripts/webrtc.ts index 067db4a90..1b62e2d88 100644 --- a/plugins/default-browser-emulator/injected-scripts/webrtc.ts +++ b/plugins/default-browser-emulator/injected-scripts/webrtc.ts @@ -1,20 +1,27 @@ -const maskLocalIp = args.localIp; -const replacementIp = args.proxyIp; +export type Args = { + localIp: string; + proxyIp: string; +}; + +const typedArgs = args as Args; + +const maskLocalIp = typedArgs.localIp; +const replacementIp = typedArgs.proxyIp; if ('RTCIceCandidate' in self && RTCIceCandidate.prototype) { - proxyGetter(RTCIceCandidate.prototype, 'candidate', function () { - const result = ReflectCached.apply(...arguments); + proxyGetter(RTCIceCandidate.prototype, 'candidate', (target, thisArg) => { + const result = ReflectCached.apply(target, thisArg, []); return result.replace(maskLocalIp, replacementIp); }); if ('address' in RTCIceCandidate.prototype) { // @ts-ignore - proxyGetter(RTCIceCandidate.prototype, 'address', function () { - const result: string = ReflectCached.apply(...arguments); + proxyGetter(RTCIceCandidate.prototype, 'address', (...args) => { + const result: string = ReflectCached.apply(...args); return result.replace(maskLocalIp, replacementIp); }); } - proxyFunction(RTCIceCandidate.prototype, 'toJSON', function () { - const json = ReflectCached.apply(...arguments); + proxyFunction(RTCIceCandidate.prototype, 'toJSON', (...args) => { + const json = ReflectCached.apply(...args); if ('address' in json) json.address = json.address.replace(maskLocalIp, replacementIp); if ('candidate' in json) json.candidate = json.candidate.replace(maskLocalIp, replacementIp); return json; @@ -22,15 +29,15 @@ if ('RTCIceCandidate' in self && RTCIceCandidate.prototype) { } if ('RTCSessionDescription' in self && RTCSessionDescription.prototype) { - proxyGetter(RTCSessionDescription.prototype, 'sdp', function () { - let result = ReflectCached.apply(...arguments); + proxyGetter(RTCSessionDescription.prototype, 'sdp', (...args) => { + let result = ReflectCached.apply(...args); while (result.indexOf(maskLocalIp) !== -1) { result = result.replace(maskLocalIp, replacementIp); } return result; }); - proxyFunction(RTCSessionDescription.prototype, 'toJSON', function () { - const json = ReflectCached.apply(...arguments); + proxyFunction(RTCSessionDescription.prototype, 'toJSON', (...args) => { + const json = ReflectCached.apply(...args); if ('sdp' in json) json.sdp = json.sdp.replace(maskLocalIp, replacementIp); return json; }); diff --git a/plugins/default-browser-emulator/injected-scripts/window.screen.ts b/plugins/default-browser-emulator/injected-scripts/window.screen.ts index 07a49b1bf..021077f93 100644 --- a/plugins/default-browser-emulator/injected-scripts/window.screen.ts +++ b/plugins/default-browser-emulator/injected-scripts/window.screen.ts @@ -1,17 +1,26 @@ +export type Args = { + unAvailHeight?: number; + unAvailWidth?: number; + colorDepth?: number; +}; + +const typedArgs = args as Args; + proxyGetter( window.screen, 'availHeight', - () => window.screen.height - (args.unAvailHeight || 0), - true, + () => window.screen.height - (typedArgs.unAvailHeight ?? 0), + { overrideOnlyForInstance: true }, ); proxyGetter( window.screen, 'availWidth', - () => window.screen.width - (args.unAvailWidth || 0), - true, + () => window.screen.width - (typedArgs.unAvailWidth ?? 0), + { overrideOnlyForInstance: true }, ); -if (args.colorDepth) { - proxyGetter(window.screen, 'colorDepth', () => args.colorDepth, true); - proxyGetter(window.screen, 'pixelDepth', () => args.colorDepth, true); +const colorDepth = typedArgs.colorDepth; +if (colorDepth) { + proxyGetter(window.screen, 'colorDepth', () => colorDepth, { overrideOnlyForInstance: true }); + proxyGetter(window.screen, 'pixelDepth', () => colorDepth, { overrideOnlyForInstance: true }); } diff --git a/plugins/default-browser-emulator/interfaces/IBrowserData.ts b/plugins/default-browser-emulator/interfaces/IBrowserData.ts index d204fbec8..996e8ad19 100644 --- a/plugins/default-browser-emulator/interfaces/IBrowserData.ts +++ b/plugins/default-browser-emulator/interfaces/IBrowserData.ts @@ -25,10 +25,10 @@ export interface IDataWindowNavigator { } export interface IDataDomPolyfill { - add: any[]; - remove: any[]; - modify: any[]; - reorder: any[]; + add?: any[]; + remove?: any[]; + modify?: any[]; + reorder?: any[]; } export interface IDataWindowChrome { diff --git a/plugins/default-browser-emulator/interfaces/IBrowserEmulatorConfig.ts b/plugins/default-browser-emulator/interfaces/IBrowserEmulatorConfig.ts index 28c6bef77..6b9858df3 100644 --- a/plugins/default-browser-emulator/interfaces/IBrowserEmulatorConfig.ts +++ b/plugins/default-browser-emulator/interfaces/IBrowserEmulatorConfig.ts @@ -1,27 +1,50 @@ import type { Args as ConsoleArgs } from '../injected-scripts/console'; +import type { Args as CookieArgs } from '../injected-scripts/Document.prototype.cookie'; import type { Args as ErrorArgs } from '../injected-scripts/error'; +import type { Args as JSONArgs } from '../injected-scripts/JSON.stringify'; +import type { Args as MediaDevicesArgs } from '../injected-scripts/MediaDevices.prototype.enumerateDevices'; +import type { Args as DeviceMemoryArgs } from '../injected-scripts/navigator.deviceMemory'; +import type { Args as HardwareConcurrencyArgs } from '../injected-scripts/navigator.hardwareConcurrency'; +import type { Args as NavigatorArgs } from '../injected-scripts/navigator'; +import type { Args as PerformanceArgs } from '../injected-scripts/performance'; +import type { Args as PolyfillAddArgs } from '../injected-scripts/polyfill.add'; +import type { Args as PolyfillModifyArgs } from '../injected-scripts/polyfill.modify'; +import type { Args as PolyfillRemoveArgs } from '../injected-scripts/polyfill.remove'; +import type { Args as PolyfillReorderArgs } from '../injected-scripts/polyfill.reorder'; +import type { Args as RTCRtpSenderArgs } from '../injected-scripts/RTCRtpSender.getCapabilities'; +import type { Args as SharedWorkerArgs } from '../injected-scripts/SharedWorker.prototype'; +import type { Args as GetVoicesArgs } from '../injected-scripts/speechSynthesis.getVoices'; +import type { Args as UnhandledArgs } from '../injected-scripts/UnhandledErrorsAndRejections'; +import type { Args as WebGlRenderingArgs } from '../injected-scripts/WebGLRenderingContext.prototype.getParameter'; +import type { Args as WebRtcArgs } from '../injected-scripts/webrtc'; +import type { Args as WindowScreenArgs } from '../injected-scripts/window.screen'; + +// T = config used if provided +// true = plugin enabled and loadDomOverrides will provide default config +// false = plugin disabled +export type InjectedScriptConfig = T | boolean; export default interface IBrowserEmulatorConfig { [InjectedScript.CONSOLE]: InjectedScriptConfig; - [InjectedScript.DOCUMENT_PROTOTYPE_COOKIE]: InjectedScriptConfig; + [InjectedScript.DOCUMENT_PROTOTYPE_COOKIE]: InjectedScriptConfig; [InjectedScript.ERROR]: InjectedScriptConfig; - [InjectedScript.JSON_STRINGIFY]: InjectedScriptConfig; - [InjectedScript.MEDIA_DEVICES_PROTOTYPE_ENUMERATE_DEVICES]: InjectedScriptConfig; - [InjectedScript.NAVIGATOR_DEVICE_MEMORY]: InjectedScriptConfig; - [InjectedScript.NAVIGATOR_HARDWARE_CONCURRENCY]: InjectedScriptConfig; - [InjectedScript.NAVIGATOR]: InjectedScriptConfig; - [InjectedScript.PERFORMANCE]: InjectedScriptConfig; - [InjectedScript.POLYFILL_ADD]: InjectedScriptConfig; - [InjectedScript.POLYFILL_MODIFY]: InjectedScriptConfig; - [InjectedScript.POLYFILL_REMOVE]: InjectedScriptConfig; - [InjectedScript.POLYFILL_REORDER]: InjectedScriptConfig; - [InjectedScript.RTC_RTP_SENDER_GETCAPABILITIES]: InjectedScriptConfig; - [InjectedScript.SHAREDWORKER_PROTOTYPE]: InjectedScriptConfig; - [InjectedScript.SPEECH_SYNTHESIS_GETVOICES]: InjectedScriptConfig; - [InjectedScript.UNHANDLED_ERRORS_AND_REJECTIONS]: InjectedScriptConfig; - [InjectedScript.WEBGL_RENDERING_CONTEXT_PROTOTYPE_GETPARAMETERS]: InjectedScriptConfig; - [InjectedScript.WEBRTC]: InjectedScriptConfig; - [InjectedScript.WINDOW_SCREEN]: InjectedScriptConfig; + [InjectedScript.JSON_STRINGIFY]: InjectedScriptConfig; + [InjectedScript.MEDIA_DEVICES_PROTOTYPE_ENUMERATE_DEVICES]: InjectedScriptConfig; + [InjectedScript.NAVIGATOR_DEVICE_MEMORY]: InjectedScriptConfig; + [InjectedScript.NAVIGATOR_HARDWARE_CONCURRENCY]: InjectedScriptConfig; + [InjectedScript.NAVIGATOR]: InjectedScriptConfig; + [InjectedScript.PERFORMANCE]: InjectedScriptConfig; + [InjectedScript.POLYFILL_ADD]: InjectedScriptConfig; + [InjectedScript.POLYFILL_MODIFY]: InjectedScriptConfig; + [InjectedScript.POLYFILL_REMOVE]: InjectedScriptConfig; + [InjectedScript.POLYFILL_REORDER]: InjectedScriptConfig; + [InjectedScript.RTC_RTP_SENDER_GETCAPABILITIES]: InjectedScriptConfig; + [InjectedScript.SHAREDWORKER_PROTOTYPE]: InjectedScriptConfig; + [InjectedScript.SPEECH_SYNTHESIS_GETVOICES]: InjectedScriptConfig; + [InjectedScript.UNHANDLED_ERRORS_AND_REJECTIONS]: InjectedScriptConfig; + [InjectedScript.WEBGL_RENDERING_CONTEXT_PROTOTYPE_GETPARAMETERS]: InjectedScriptConfig; + [InjectedScript.WEBRTC]: InjectedScriptConfig; + [InjectedScript.WINDOW_SCREEN]: InjectedScriptConfig; } export enum InjectedScript { @@ -46,5 +69,3 @@ export enum InjectedScript { WEBRTC = 'webrtc', WINDOW_SCREEN = 'window.screen', } - -export type InjectedScriptConfig = T | boolean; diff --git a/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts b/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts index 9efae70d0..b99ebe31b 100644 --- a/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts +++ b/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { IFrame } from '@ulixee/unblocked-specification/agent/browser/IFrame'; import INewDocumentInjectedScript from '../interfaces/INewDocumentInjectedScript'; -import { InjectedScript } from '../interfaces/IBrowserEmulatorConfig'; +import IBrowserEmulatorConfig, { InjectedScript } from '../interfaces/IBrowserEmulatorConfig'; const injectedSourceUrl = ``; const cache: { [name: string]: string } = {}; @@ -20,6 +20,8 @@ export default class DomOverridesBuilder { private workerOverrides = new Set(); + constructor(private readonly config?: IBrowserEmulatorConfig) {} + public getWorkerOverrides(): string[] { return [...this.workerOverrides]; } @@ -108,6 +110,8 @@ export default class DomOverridesBuilder { } })(); + getSharedStorage().ready = true; + })(); //# sourceURL=${injectedSourceUrl}`.replace(/\/\/# sourceMap.+/g, ''), }; @@ -169,6 +173,26 @@ export default class DomOverridesBuilder { }); } + public addOverrideAndUseConfig( + injectedScript: T, + defaultConfig: IBrowserEmulatorConfig[T], + opts?: { registerWorkerOverride?: boolean }, + ): void { + if (!this.config) + throw new Error( + 'This method can only be used when creating domOverriderBuilder with a config', + ); + + const scriptConfig = this.config[injectedScript]; + if (!scriptConfig) return; + + this.add( + injectedScript, + scriptConfig === true ? defaultConfig : scriptConfig, + opts?.registerWorkerOverride ?? false, + ); + } + public cleanup(): void { this.alwaysPageScripts.clear(); this.alwaysWorkerScripts.clear(); diff --git a/plugins/default-browser-emulator/lib/loadDomOverrides.ts b/plugins/default-browser-emulator/lib/loadDomOverrides.ts index 2e26d0458..a6fb0df7e 100644 --- a/plugins/default-browser-emulator/lib/loadDomOverrides.ts +++ b/plugins/default-browser-emulator/lib/loadDomOverrides.ts @@ -10,22 +10,7 @@ export default function loadDomOverrides( data: IBrowserData, userAgentData: IUserAgentData, ): DomOverridesBuilder { - const domOverrides = new DomOverridesBuilder(); - - const addOverrideWithConfigOrDefault = ( - injectedScript: T, - defaultConfig: IBrowserEmulatorConfig[T], - registerWorkerOverride = false, - ): void => { - const scriptConfig = config[injectedScript]; - if (!scriptConfig) return; - - domOverrides.add( - injectedScript, - scriptConfig === true ? defaultConfig : scriptConfig, - registerWorkerOverride, - ); - }; + const domOverrides = new DomOverridesBuilder(config); const deviceProfile = emulationProfile.deviceProfile; const isHeadless = @@ -40,7 +25,7 @@ export default function loadDomOverrides( const domPolyfill = data.domPolyfill; - addOverrideWithConfigOrDefault( + domOverrides.addOverrideAndUseConfig( InjectedScript.ERROR, { removeInjectedLines: true, @@ -48,123 +33,101 @@ export default function loadDomOverrides( skipDuplicateSetPrototypeLines: true, applyStackTraceLimit: true, }, - true, + { registerWorkerOverride: true }, ); - addOverrideWithConfigOrDefault(InjectedScript.CONSOLE, { mode: 'patchLeaks' }, true); - // TODO migrate others to new logic. This first requires proper types for all Plugin Args. - // This will also allow us to configure everything in special ways. In most occasions - // you would never want to do this, but this is very helpful for specific use-cases, e.g. testing. + domOverrides.addOverrideAndUseConfig( + InjectedScript.CONSOLE, + { mode: 'patchLeaks' }, + { registerWorkerOverride: true }, + ); - if (config[InjectedScript.JSON_STRINGIFY]) { - domOverrides.add(InjectedScript.JSON_STRINGIFY, undefined, true); - } + domOverrides.addOverrideAndUseConfig( + InjectedScript.JSON_STRINGIFY, + {}, + { registerWorkerOverride: true }, + ); - if (config[InjectedScript.MEDIA_DEVICES_PROTOTYPE_ENUMERATE_DEVICES]) { - domOverrides.add(InjectedScript.MEDIA_DEVICES_PROTOTYPE_ENUMERATE_DEVICES, { - videoDevice: deviceProfile.videoDevice, - }); - } + domOverrides.addOverrideAndUseConfig(InjectedScript.MEDIA_DEVICES_PROTOTYPE_ENUMERATE_DEVICES, { + groupId: deviceProfile.videoDevice?.groupId, + deviceId: deviceProfile.videoDevice?.deviceId, + }); - if (config[InjectedScript.NAVIGATOR]) { - domOverrides.add( - InjectedScript.NAVIGATOR, - { - userAgentString: emulationProfile.userAgentOption.string, - platform: emulationProfile.windowNavigatorPlatform, - headless: isHeadless, - pdfViewerEnabled: data.windowNavigator.navigator.pdfViewerEnabled?._$value, - userAgentData, - rtt: emulationProfile.deviceProfile.rtt, - }, - true, - ); - } + domOverrides.addOverrideAndUseConfig( + InjectedScript.NAVIGATOR, + { + userAgentString: emulationProfile.userAgentOption.string, + platform: emulationProfile.windowNavigatorPlatform, + headless: isHeadless, + pdfViewerEnabled: data.windowNavigator.navigator.pdfViewerEnabled?._$value, + userAgentData, + rtt: emulationProfile.deviceProfile.rtt, + }, + { registerWorkerOverride: true }, + ); - if (config[InjectedScript.NAVIGATOR_DEVICE_MEMORY]) { - domOverrides.add( - InjectedScript.NAVIGATOR_DEVICE_MEMORY, - { - memory: deviceProfile.deviceMemory, - storageTib: deviceProfile.deviceStorageTib, - maxHeapSize: deviceProfile.maxHeapSize, - }, - true, - ); - } + domOverrides.addOverrideAndUseConfig( + InjectedScript.NAVIGATOR_DEVICE_MEMORY, + { + memory: deviceProfile.deviceMemory, + storageTib: deviceProfile.deviceStorageTib, + maxHeapSize: deviceProfile.maxHeapSize, + }, + { registerWorkerOverride: true }, + ); - if (config[InjectedScript.NAVIGATOR_HARDWARE_CONCURRENCY]) { - domOverrides.add( - InjectedScript.NAVIGATOR_HARDWARE_CONCURRENCY, - { - concurrency: deviceProfile.hardwareConcurrency, - }, - true, - ); - } + domOverrides.addOverrideAndUseConfig( + InjectedScript.NAVIGATOR_HARDWARE_CONCURRENCY, + { + concurrency: deviceProfile.hardwareConcurrency, + }, + { registerWorkerOverride: true }, + ); - if ( - config[InjectedScript.PERFORMANCE] && - Number(emulationProfile.browserEngine.fullVersion.split('.')[0]) >= 109 - ) { - domOverrides.add(InjectedScript.PERFORMANCE); + if (Number(emulationProfile.browserEngine.fullVersion.split('.')[0]) >= 109) { + domOverrides.addOverrideAndUseConfig(InjectedScript.PERFORMANCE, {}); } - if (domPolyfill) { - if (config[InjectedScript.POLYFILL_ADD] && domPolyfill?.add?.length) { - domOverrides.add(InjectedScript.POLYFILL_ADD, { - itemsToAdd: domPolyfill.add, - }); - } - - if (config[InjectedScript.POLYFILL_MODIFY] && domPolyfill?.modify?.length) { - domOverrides.add(InjectedScript.POLYFILL_MODIFY, { - itemsToAdd: domPolyfill.modify, - }); - } - - if (config[InjectedScript.POLYFILL_REMOVE] && domPolyfill?.remove?.length) { - domOverrides.add(InjectedScript.POLYFILL_REMOVE, { - itemsToRemove: domPolyfill.remove, - }); - } - - if (config[InjectedScript.POLYFILL_REORDER] && domPolyfill?.reorder?.length) { - domOverrides.add(InjectedScript.POLYFILL_REORDER, { - itemsToReorder: domPolyfill.add, - }); - } - } + domOverrides.addOverrideAndUseConfig(InjectedScript.POLYFILL_ADD, { + itemsToAdd: domPolyfill?.add ?? [], + }); - if (config[InjectedScript.SHAREDWORKER_PROTOTYPE]) { - domOverrides.add(InjectedScript.SHAREDWORKER_PROTOTYPE, undefined, true); - } + domOverrides.addOverrideAndUseConfig(InjectedScript.POLYFILL_MODIFY, { + itemsToModify: domPolyfill?.modify ?? [], + }); - if (config[InjectedScript.SPEECH_SYNTHESIS_GETVOICES] && voices?.length) { - domOverrides.add(InjectedScript.SPEECH_SYNTHESIS_GETVOICES, { voices }); - } + domOverrides.addOverrideAndUseConfig(InjectedScript.POLYFILL_REMOVE, { + itemsToRemove: domPolyfill?.remove ?? [], + }); - if (config[InjectedScript.WINDOW_SCREEN]) { - const frame = data.windowFraming; - domOverrides.add(InjectedScript.WINDOW_SCREEN, { - unAvailHeight: frame.screenGapTop + frame.screenGapBottom, - unAvailWidth: frame.screenGapLeft + frame.screenGapRight, - colorDepth: emulationProfile.viewport.colorDepth ?? frame.colorDepth, - }); - } + domOverrides.addOverrideAndUseConfig(InjectedScript.POLYFILL_REORDER, { + itemsToReorder: domPolyfill?.reorder ?? [], + }); - if (config[InjectedScript.UNHANDLED_ERRORS_AND_REJECTIONS]) { - domOverrides.add(InjectedScript.UNHANDLED_ERRORS_AND_REJECTIONS); - domOverrides.registerWorkerOverrides(InjectedScript.UNHANDLED_ERRORS_AND_REJECTIONS); - } + domOverrides.addOverrideAndUseConfig(InjectedScript.SHAREDWORKER_PROTOTYPE, undefined, { + registerWorkerOverride: true, + }); - if (config[InjectedScript.WEBGL_RENDERING_CONTEXT_PROTOTYPE_GETPARAMETERS]) { - domOverrides.add( - InjectedScript.WEBGL_RENDERING_CONTEXT_PROTOTYPE_GETPARAMETERS, - deviceProfile.webGlParameters, - true, - ); - } + domOverrides.addOverrideAndUseConfig(InjectedScript.SPEECH_SYNTHESIS_GETVOICES, { voices }); + + const frame = data.windowFraming; + domOverrides.addOverrideAndUseConfig(InjectedScript.WINDOW_SCREEN, { + unAvailHeight: frame.screenGapTop + frame.screenGapBottom, + unAvailWidth: frame.screenGapLeft + frame.screenGapRight, + colorDepth: emulationProfile.viewport.colorDepth ?? frame.colorDepth, + }); + + domOverrides.addOverrideAndUseConfig( + InjectedScript.UNHANDLED_ERRORS_AND_REJECTIONS, + { preventDefaultUncaughtError: true, preventDefaultUnhandledRejection: true }, + { registerWorkerOverride: true }, + ); + + domOverrides.addOverrideAndUseConfig( + InjectedScript.WEBGL_RENDERING_CONTEXT_PROTOTYPE_GETPARAMETERS, + deviceProfile.webGlParameters, + { registerWorkerOverride: true }, + ); return domOverrides; } diff --git a/plugins/default-browser-emulator/test/proxyLeak.test.ts b/plugins/default-browser-emulator/test/proxyLeak.test.ts index aabe13474..cfeef4aa1 100644 --- a/plugins/default-browser-emulator/test/proxyLeak.test.ts +++ b/plugins/default-browser-emulator/test/proxyLeak.test.ts @@ -256,8 +256,7 @@ test('Errors should not leak proxy objects, second simple edition: looking at Er expect(output).toEqual(referenceOutput); }); -// TODO hide this test somewhere as it seems they don't know this yet??? But we did fix so doesn't really hurt? -test.failing( +test( 'Errors should not leak proxy objects, advanced edition: looking at Error.prepareStackTrace', async () => { function script() { @@ -392,6 +391,20 @@ test('Error should also not leak when using bind with a proxy', async () => { expect(output).toEqual(referenceOutput); }); +test('Bind should work as expected', async () => { + function script() { + function bla() { + return this.data; + } + const t = { data: 'test' }; + + return bla.bind(t).bind(undefined)(); + } + + const { output, referenceOutput } = await runScriptWithReference(script); + expect(output).toEqual(referenceOutput); +}); + test('should not leak modified get wrapper for functions on proxy instance', async () => { function script() { const constructor = console.info.constructor;