UNPKG

fakebrowser

Version:

🤖 Fake fingerprints to bypass anti-bot systems. Simulate mouse and keyboard operations to make behavior like a real person.

1,219 lines (1,035 loc) 41.6 kB
// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols,JSUnresolvedVariable,JSNonASCIINames,NonAsciiCharacters /** * A set of shared utility functions specifically for the purpose of modifying native browser APIs without leaving traces. * * Meant to be passed down in puppeteer and used in the context of the page (everything in here runs in NodeJS as well as a browser). * * Note: If for whatever reason you need to use this outside of `puppeteer-extra`: * Just remove the `module.exports` statement at the very bottom, the rest can be copy pasted into any browser context. * * Alternatively take a look at the `extract-stealth-evasions` package to create a finished bundle which includes these utilities. * */ const utils = {}; utils.init = () => { utils._preloadCache(); utils._preloadEnv(); utils._preloadGlobalVariables(); utils._hookObjectPrototype(); }; /** * Preload a cache of function copies and data. * * For a determined enough observer it would be possible to overwrite and sniff usage of functions * we use in our internal Proxies, to combat that we use a cached copy of those functions. * * Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before, * by executing `utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups). * * This is evaluated once per execution context (e.g. window) */ utils._preloadCache = () => { if (utils.cache) { return; } utils.cache = OffscreenCanvas.prototype.constructor.__$cache; if (utils.cache) { return; } class ɵɵɵɵPromise extends Promise { } OffscreenCanvas.prototype.constructor.__$cache = utils.cache = { // Used in `makeNativeString` nativeToStringStr: Function.toString + '', // => `function toString() { [native code] }` // Used in our proxies Reflect: { get: Reflect.get.bind(Reflect), set: Reflect.set.bind(Reflect), apply: Reflect.apply.bind(Reflect), ownKeys: Reflect.ownKeys.bind(Reflect), getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor.bind(Reflect), setPrototypeOf: Reflect.setPrototypeOf.bind(Reflect), }, Promise: ɵɵɵɵPromise, Object: { setPrototypeOf: Object.setPrototypeOf.bind(Object), getPrototypeOf: Object.getPrototypeOf.bind(Object), getOwnPropertyDescriptors: Object.getOwnPropertyDescriptors.bind(Object), getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor.bind(Object), entries: Object.entries.bind(Object), fromEntries: Object.fromEntries.bind(Object), defineProperty: Object.defineProperty.bind(Object), defineProperties: Object.defineProperties.bind(Object), getOwnPropertyNames: Object.getOwnPropertyNames.bind(Object), create: Object.create.bind(Object), keys: Object.keys.bind(Object), values: Object.values.bind(Object), }, Function: { prototype: { toString: Function.prototype.toString, }, }, global: 'undefined' !== typeof window ? window : globalThis, window: { getComputedStyle: ('undefined' !== typeof window) && window.getComputedStyle.bind(window), eval: ('undefined' !== typeof window) ? window.eval.bind(window) : (globalThis ? globalThis.eval.bind(globalThis) : undefined), navigator: ('undefined' !== typeof window) ? window.navigator : (globalThis ? globalThis.navigator : undefined), }, OffscreenCanvas: { prototype: { getContext: ('undefined' !== typeof OffscreenCanvas) && OffscreenCanvas.prototype.getContext, }, }, HTMLCanvasElement: { prototype: { getContext: ('undefined' !== typeof HTMLCanvasElement) && HTMLCanvasElement.prototype.getContext, }, }, Descriptor: {}, }; const cacheDescriptors = (objPath, propertyKeys) => { // get obj from path const objPaths = objPath.split('.'); let _global = utils.cache.global; let descObj = utils.cache.Descriptor; for (const part of objPaths) { if (_global) { // noinspection JSUnresolvedFunction if (!Object.hasOwn(_global, part)) { _global = undefined; } else { _global = _global[part]; } } const subCacheObj = descObj[part] || {}; descObj[part] = subCacheObj; descObj = subCacheObj; } for (const key of propertyKeys) { descObj[key] = _global ? Object.getOwnPropertyDescriptor(_global, key) : undefined; } }; cacheDescriptors('window', ['alert']); cacheDescriptors('Navigator.prototype', ['webdriver']); cacheDescriptors('WorkerNavigator.prototype', ['webdriver']); cacheDescriptors('HTMLElement.prototype', ['style']); cacheDescriptors('CSSStyleDeclaration.prototype', ['setProperty']); cacheDescriptors('FontFace.prototype', ['load']); cacheDescriptors('WebGLShaderPrecisionFormat.prototype', ['rangeMin', 'rangeMax', 'precision']); }; utils._preloadGlobalVariables = () => { if (utils.variables) { return; } utils.variables = OffscreenCanvas.prototype.constructor.__$variables; if (utils.variables) { return; } OffscreenCanvas.prototype.constructor.__$variables = utils.variables = { proxies: [], toStringPatchObjs: [], toStringRedirectObjs: [], renderingContextWithOperators: [], taskData: {}, }; }; utils._preloadEnv = () => { if (utils.env) { return; } utils.env = OffscreenCanvas.prototype.constructor.__$env; if (utils.env) { return; } OffscreenCanvas.prototype.constructor.__$env = utils.env = { isWorker: !globalThis.document && !!globalThis.WorkerGlobalScope, isSharedWorker: !!globalThis.SharedWorkerGlobalScope, isServiceWorker: !!globalThis.ServiceWorkerGlobalScope, }; }; utils._hookObjectPrototype = () => { if (utils.objHooked) { return; } utils.objHooked = OffscreenCanvas.prototype.constructor.__$objHooked; if (utils.objHooked) { return; } utils.objHooked = OffscreenCanvas.prototype.constructor.__$objHooked = true; const _Object = utils.cache.Object; const _Reflect = utils.cache.Reflect; // setPrototypeOf utils.replaceWithProxy(Object, 'setPrototypeOf', { apply(target, thisArg, args) { args[0] = utils.getProxyTarget(args[0]); args[1] = utils.getProxyTarget(args[1]); return _Reflect.apply(target, thisArg, args); }, }); // Function.prototype toString const toStringProxy = utils.newProxyInstance( Function.prototype.toString, utils.stripProxyFromErrors({ apply: function (target, thisArg, args) { // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""` if (thisArg === Function.prototype.toString) { return utils.makeNativeString('toString'); } // toStringPatch const toStringPatchObj = utils.variables.toStringPatchObjs.find( e => e.obj === thisArg, ); if (toStringPatchObj) { // `toString` targeted at our proxied Object detected // We either return the optional string verbatim or derive the most desired result automatically return toStringPatchObj.str || utils.makeNativeString(toStringPatchObj.obj.name); } // toStringRedirect const toStringRedirectObj = utils.variables.toStringRedirectObjs.find( e => e.proxyObj === thisArg, ); if (toStringRedirectObj) { const {proxyObj, originalObj} = toStringRedirectObj; const fallback = () => originalObj && originalObj.name ? utils.makeNativeString(originalObj.name) : utils.makeNativeString(proxyObj.name); // Return the toString representation of our original object if possible return originalObj + '' || fallback(); } if (typeof thisArg === 'undefined' || thisArg === null) { return _Reflect.apply(target, thisArg, args); } // Check if the toString protype of the context is the same as the global prototype, // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case const hasSameProto = _Object.getPrototypeOf( Function.prototype.toString, ).isPrototypeOf(thisArg.toString); // eslint-disable-line no-prototype-builtins if (!hasSameProto) { // Pass the call on to the local Function.prototype.toString instead return thisArg.toString(); } return _Reflect.apply(target, thisArg, args); }, }), ); utils.replaceProperty(Function.prototype, 'toString', { value: toStringProxy, }); // Object create utils.replaceWithProxy(Object, 'create', { apply(target, thisArg, args) { if (args[0] === toStringProxy) { args[0] = utils.cache.Function.prototype.toString; } return _Reflect.apply(target, thisArg, args); }, }); }; utils.removeTempVariables = () => { const tmpVarNames = Object.getOwnPropertyNames( OffscreenCanvas.prototype.constructor, ).filter(e => e.startsWith('__$')); tmpVarNames.forEach(e => { delete OffscreenCanvas.prototype.constructor[e]; }); }; utils.newProxyInstance = (target, handler) => { // const newTarget = utils.getProxyTarget(target); const result = new Proxy(target, handler); utils.variables.proxies.push({proxy: result, target}); return result; }; utils.getProxyTarget = (proxy) => { const cache = utils.variables.proxies.find(e => e.proxy === proxy); if (!cache) { return proxy; } return cache.target; }; utils.patchError = (err, trap) => { //0: "TypeError: Failed to execute 'query' on 'Permissions': Failed to read the 'name' property from 'PermissionDescriptor': The provided value 'speaker' is not a valid enum value of type PermissionName." // 1: " at eval (eval at <anonymous> (:15:24), <anonymous>:32:50)" // 2: " at new Promise (<anonymous>)" // 3: " at new ɵɵɵɵPromise (eval at <anonymous> (:10:49), <anonymous>:11:5)" // 4: " at Object.apply (eval at <anonymous> (:15:24), <anonymous>:20:24)" // 5: " at Object.ɵɵɵɵnewHandler.<computed> [as apply] (eval at <anonymous> (:10:49), <anonymous>:23:38)" // 6: " at e (https://www.n // 7: " at https://www.n // 8: " at Array.map (<anonymous>)" // 9: " at Object.np (https://www.ni // 10: " at Object.bpd (https://www.ni // call stack: // eval @ VM2999:32 // ɵɵɵɵPromise @ VM2324:11 // apply @ VM2999:20 // ɵɵɵɵnewHandler.<computed> @ VM2966:23 // e @ G2IB:1 // (anonymous) @ G2IB:1 // np @ G2IB:1 // bpd @ G2IB:1 // startTracking @ G2IB:1 // (anonymous) @ G2IB:1 // === // 0: "TypeError: Illegal invocation" // 1: " at Object.apply (eval at <anonymous> (:15:24), <anonymous>:23:48)" // 2: " at Object.ɵɵɵɵnewHandler.<computed> [as apply] (eval at <anonymous> (:10:49), <anonymous>:23:38)" // 3: " at https://api.ni // 4: " at j (https://api.ni // 5: " at Object.epk (https://api.ni // 6: " at https://api.n // 7: " at https://api.nik // apply @ VM6234:23 // ɵɵɵɵnewHandler.<computed> @ VM6201:23 // (anonymous) @ ips.js?ak_bmsc_nke-2…K9HsrXz4ZcCIkpl4a:1 // j @ ips.js?ak_bmsc_nke-2…K9HsrXz4ZcCIkpl4a:1 // epk @ ips.js?ak_bmsc_nke-2…K9HsrXz4ZcCIkpl4a:1 // (anonymous) @ ips.js?ak_bmsc_nke-2…K9HsrXz4ZcCIkpl4a:1 // (anonymous) @ ips.js?ak_bmsc_nke-2…9HsrXz4ZcCIkpl4a:52 // === // 0: "TypeError: Function.prototype.toString requires that 'this' be a Function" // 1: " at Function.toString (<anonymous>)" // 2: " at Object.apply (<anonymous>:215:33)" // 3: " at Object.ɵɵɵɵnewHandler.<computed> [as apply] (<anonymous>:492:38)" // 4: " at getNewObjectToStringTypeErrorLie (https://abrahamjuliot.github.io/creepjs/creepworker.js:223:31)" // 5: " at getLies (https://abrahamjuliot.github.io/creepjs/creepworker.js:317:38)" // 6: " at https://abrahamjuliot.github.io/creepjs/creepworker.js:379:16" // 7: " at Array.forEach (<anonymous>)" // 8: " at searchLies (https://abrahamjuliot.github.io/creepjs/creepworker.js:357:17)" // 9: " at getPrototypeLies (https://abrahamjuliot.github.io/creepjs/creepworker.js:424:2)" // 10: " at getWorkerData (https://abrahamjuliot.github.io/creepjs/creepworker.js:1009:6)" // apply @ VM3:215 // ɵɵɵɵnewHandler.<computed> @ VM3:492 // getNewObjectToStringTypeErrorLie @ creepworker.js:223 // getLies @ creepworker.js:317 // (anonymous) @ creepworker.js:379 // searchLies @ creepworker.js:357 // getPrototypeLies @ creepworker.js:424 // getWorkerData @ creepworker.js:1009 // async function (async) // getWorkerData @ creepworker.js:896 // (anonymous) @ creepworker.js:1095 // === // Replacement logics: // 1 -- * First, detect ``at Object.ɵɵɵɵnewHandler.<computed> [as `` // 1.1 ---- ``ɵɵɵɵnewHandler`` may be used by the anti-bot system (fakebrowser is open source code :D ), TODO: so we need replace ``ɵɵɵɵ`` in utils.js with new random string // 1.2 ---- save the line number eg: :10:49, :23:38 => fbCodeStackLineNumbers.push() // 2 -- use regex to find ``apply``, save apply as variable ${realTrap} // 3 -- Check that the next line of code is: ``at Object.${realTrap} (`` // 3.1 ---- save the line number from this line of code => fbCodeStackLineNumbers.push() // 4 -- remove the line corresponding to ``at new ɵɵɵɵPromise (eval at <anonymous>`` (replacing ``ɵɵɵɵ`` with another string) // 4.1 -- check next line is ``at new Promise (<anonymous>)`` ? remove it. // 5 -- delete the lines containing line numbers from fbCodeStackLineNumbers, and add the line numbers contained in those lines to fbCodeStackLineNumbers // 6 -- fin if (!err || !err.stack || !err.stack.includes(`at `)) { return err; } // Special cases due to our nested toString proxies err.stack = err.stack.replace( 'at Object.toString (', 'at Function.toString (', ); // 1 let realTrap = ''; let stackLines = err.stack.split('\n'); let lineIndex = stackLines.findIndex(e => { const matches = e.match(/Object\.ɵɵɵɵnewHandler\.<computed> \[as (.*)]/); if (matches && matches[1]) { // 2 realTrap = matches[1]; return true; } return false; }); if (lineIndex < 0 || !realTrap) { return err; } // let's start const fbCodeStackLineNumbers = []; const dumpLineNumbers = (line, add) => { // in ===> Object.ɵɵɵɵnewHandler.<computed> [as apply] (eval at <anonymous> (:10:49), <anonymous>:23:38) // out ===> array([':10:49', ':23:38']) // really don't know what those two numbers mean, caller? called? Whatever. // assert? const result = line.match(/:[0-9]+:[0-9]+/g) || []; if (add) { fbCodeStackLineNumbers.push(...result); } return result; }; // 1.2 dumpLineNumbers(stackLines[lineIndex], true); stackLines.splice(lineIndex, 1); // 3 --lineIndex; if (stackLines[lineIndex].includes(`at Object.${realTrap} (`)) { // 3.1 dumpLineNumbers(stackLines[lineIndex], true); stackLines.splice(lineIndex, 1); } for (let n = lineIndex - 1; n >= 0; --n) { const line = stackLines[n]; // 4 if (line.includes(`at new ɵɵɵɵPromise (eval at <anonymous>`)) { stackLines.splice(n, 1); // 4.1 if (stackLines[n - 1] && stackLines[n - 1].includes(`at new Promise (<anonymous>)`)) { --n; stackLines.splice(n, 1); } continue; } // 5 const lineNums = dumpLineNumbers(line, false); if (utils.intersectionSet(lineNums, fbCodeStackLineNumbers).size > 0) { fbCodeStackLineNumbers.push(...lineNums); stackLines.splice(n, 1); } } // 6 err.stack = stackLines.join('\n'); return err; }; /** * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw. * * The presence of a JS Proxy can be revealed as it shows up in error stack traces. * * @param {object} handler - The JS Proxy handler to wrap */ utils.stripProxyFromErrors = (handler = {}) => { const _Object = utils.cache.Object; const _Reflect = utils.cache.Reflect; const ɵɵɵɵnewHandler = { setPrototypeOf: function (target, proto) { if (proto === null) throw new TypeError('Cannot convert object to primitive value'); if (_Object.getPrototypeOf(target) === _Object.getPrototypeOf(proto)) { throw new TypeError('Cyclic __proto__ value'); } return _Reflect.setPrototypeOf(target, proto); }, }; // We wrap each trap in the handler in a try/catch and modify the error stack if they throw const traps = _Object.getOwnPropertyNames(handler); traps.forEach(trap => { ɵɵɵɵnewHandler[trap] = function () { try { // Forward the call to the defined proxy handler return handler[trap].apply(this, arguments || []); } catch (err) { err = utils.patchError(err, trap); throw err; } }; }); return ɵɵɵɵnewHandler; }; /** * Strip error lines from stack traces until (and including) a known line the stack. * * @param {object} err - The error to sanitize * @param {string} anchor - The string the anchor line starts with */ utils.stripErrorWithAnchor = (err, anchor) => { const stackArr = err.stack.split('\n'); const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor)); if (anchorIndex === -1) { return err; // 404, anchor not found } // Strip everything from the top until we reach the anchor line (remove anchor line as well) // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`) stackArr.splice(1, anchorIndex); err.stack = stackArr.join('\n'); return err; }; /** * Replace the property of an object in a stealthy way. * * Note: You also want to work on the prototype of an object most often, * as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)). * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty * * @example * replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" }) * // or * replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] }) * * @param {object} obj - The object which has the property to replace * @param {string | Symbol} propName - The property name to replace * @param {object} descriptorOverrides - e.g. { value: "alice" } */ utils.replaceProperty = (obj, propName, descriptorOverrides = {}) => { const _Object = utils.cache.Object; const descriptors = _Object.getOwnPropertyDescriptor(obj, propName) || {}; // if (propName !== 'toString' && propName !== Symbol.toStringTag) { // // noinspection JSUnusedLocalSymbols // for (const [key, value] of _Object.entries(descriptorOverrides)) { // if (descriptors[key]) { // utils.redirectToString(descriptorOverrides[key], descriptors[key]); // } // } // } return _Object.defineProperty(obj, propName, { // Copy over the existing descriptors (writable, enumerable, configurable, etc) ...descriptors, // Add our overrides (e.g. value, get()) ...descriptorOverrides, }); }; /** * Utility function to generate a cross-browser `toString` result representing native code. * * There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings. * To future-proof this we use an existing native toString result as the basis. * * The only advantage we have over the other team is that our JS runs first, hence we cache the result * of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it. * * @example * makeNativeString('foobar') // => `function foobar() { [native code] }` * * @param {string} [name] - Optional function name */ utils.makeNativeString = (name = '') => { return utils.cache.nativeToStringStr.replace('toString', name || ''); }; /** * Helper function to modify the `toString()` result of the provided object. * * Note: Use `utils.redirectToString` instead when possible. * * There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object. * If no string is provided we will generate a `[native code]` thing based on the name of the property object. * * @example * patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }') * * @param {object} obj - The object for which to modify the `toString()` representation * @param {string} str - Optional string used as a return value */ utils.patchToString = (obj, str = '') => { utils.variables.toStringPatchObjs.push({obj, str}); }; /** * Make all nested functions of an object native. * * @param {object} obj */ utils.patchToStringNested = (obj = {}) => { return utils.execRecursively(obj, ['function'], utils.patchToString); }; /** * Redirect toString requests from one object to another. * * @param {object} proxyObj - The object that toString will be called on * @param {object} originalObj - The object which toString result we wan to return */ utils.redirectToString = (proxyObj, originalObj) => { utils.variables.toStringRedirectObjs.push({proxyObj, originalObj}); }; /** * All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps. * * Will stealthify these aspects (strip error stack traces, redirect toString, etc). * Note: This is meant to modify native Browser APIs and works best with prototype objects. * * @example * replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler) * * @param {object} obj - The object which has the property to replace * @param {string | Symbol} propName - The name of the property to replace * @param {object} handler - The JS Proxy handler to use */ utils.replaceWithProxy = (obj, propName, handler) => { const originalObj = obj[propName]; const _Reflect = utils.cache.Reflect; if (!originalObj) { return false; } // if (!handler.get) { // handler.get = function ɵɵɵɵget(target, property, receiver) { // debugger; // return _Reflect.get(target, property, receiver); // } // } const proxyObj = utils.newProxyInstance(originalObj, utils.stripProxyFromErrors(handler)); utils.replaceProperty(obj, propName, {value: proxyObj}); utils.redirectToString(proxyObj, originalObj); return true; }; /** * All-in-one method to replace a getter with a JS Proxy using the provided Proxy handler with traps. * * @example * replaceGetterWithProxy(Object.getPrototypeOf(navigator), 'vendor', proxyHandler) * * @param {object} obj - The object which has the property to replace * @param {string} propName - The name of the property to replace * @param {object} handler - The JS Proxy handler to use */ utils.replaceGetterWithProxy = (obj, propName, handler) => { const desc = utils.cache.Object.getOwnPropertyDescriptor(obj, propName) if (desc) { const fn = utils.cache.Object.getOwnPropertyDescriptor(obj, propName).get; const fnStr = fn.toString(); // special getter function string const proxyObj = utils.newProxyInstance(fn, utils.stripProxyFromErrors(handler)); utils.replaceProperty(obj, propName, {get: proxyObj}); utils.patchToString(proxyObj, fnStr); return true; } else { return false; } }; utils.replaceSetterWithProxy = (obj, propName, handler) => { const desc = utils.cache.Object.getOwnPropertyDescriptor(obj, propName) if (desc) { const fn = utils.cache.Object.getOwnPropertyDescriptor(obj, propName).set; const fnStr = fn.toString(); // special setter function string const proxyObj = utils.newProxyInstance(fn, utils.stripProxyFromErrors(handler)); utils.replaceProperty(obj, propName, {set: proxyObj}); utils.patchToString(proxyObj, fnStr); return true; } else { return false; } }; /** * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps. * * Will stealthify these aspects (strip error stack traces, redirect toString, etc). * * @example * mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler) * * @param {object} obj - The object which has the property to replace * @param {string} propName - The name of the property to replace or create * @param {object} pseudoTarget - The JS Proxy target to use as a basis * @param {object} descriptorOverrides Overwrite writable, enumerable, configurable, etc * @param {object} handler - The JS Proxy handler to use */ utils.mockWithProxy = (obj, propName, pseudoTarget, descriptorOverrides, handler) => { const _Reflect = utils.cache.Reflect; if (!handler.get) { handler.get = function ɵɵɵɵget(target, property, receiver) { if (property === 'name') { return propName; } return _Reflect.get(target, property, receiver); }; } const proxyObj = pseudoTarget ? utils.newProxyInstance(pseudoTarget, utils.stripProxyFromErrors(handler)) : utils.stripProxyFromErrors(handler); utils.replaceProperty(obj, propName, { ...descriptorOverrides, value: proxyObj, }); utils.patchToString(proxyObj); return true; }; utils.mockGetterWithProxy = (obj, propName, pseudoTarget, descriptorOverrides, handler) => { const _Reflect = utils.cache.Reflect; if (!handler.get) { handler.get = function ɵɵɵɵget(target, property, receiver) { if (property === 'name') { return `get ${propName}`; } if (property === 'length') { return 0; } return _Reflect.get(target, property, receiver); }; } const proxyObj = pseudoTarget ? utils.newProxyInstance(pseudoTarget, utils.stripProxyFromErrors(handler)) : utils.stripProxyFromErrors(handler); utils.replaceProperty(obj, propName, { ...descriptorOverrides, get: proxyObj, }); utils.patchToString(proxyObj, `function get ${propName}() { [native code] }`); return true; }; utils.mockSetterWithProxy = (obj, propName, pseudoTarget, descriptorOverrides, handler) => { const _Reflect = utils.cache.Reflect; if (!handler.get) { handler.get = function ɵɵɵɵget(target, property, receiver) { if (property === 'name') { return `set ${propName}`; } if (property === 'length') { return 1; } return _Reflect.get(target, property, receiver); }; } const proxyObj = pseudoTarget ? utils.newProxyInstance(pseudoTarget, utils.stripProxyFromErrors(handler)) : utils.stripProxyFromErrors(handler); utils.replaceProperty(obj, propName, { ...descriptorOverrides, set: proxyObj, }); utils.patchToString(proxyObj, `function set ${propName}() { [native code] }`); return true; }; /** * All-in-one method to create a new JS Proxy with stealth tweaks. * * This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property. * * Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc). * * @example * createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy * * @param {object} pseudoTarget - The JS Proxy target to use as a basis * @param {object} handler - The JS Proxy handler to use */ utils.createProxy = (pseudoTarget, handler) => { const proxyObj = utils.newProxyInstance( pseudoTarget, utils.stripProxyFromErrors(handler), ); utils.patchToString(proxyObj); return proxyObj; }; /** * Helper function to split a full path to an Object into the first part and property. * * @example * splitObjPath(`HTMLMediaElement.prototype.canPlayType`) * // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"} * * @param {string} objPath - The full path to an object as dot notation string */ utils.splitObjPath = objPath => ({ // Remove last dot entry (property) ==> `HTMLMediaElement.prototype` objName: objPath.split('.').slice(0, -1).join('.'), // Extract last dot entry ==> `canPlayType` propName: objPath.split('.').slice(-1)[0], }); /** * Convenience method to replace a property with a JS Proxy using the provided objPath. * * Supports a full path (dot notation) to the object as string here, in case that makes it easier. * * @example * replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler) * * @param {string} objPath - The full path to an object (dot notation string) to replace * @param {object} handler - The JS Proxy handler to use */ utils.replaceObjPathWithProxy = (objPath, handler) => { const {objName, propName} = utils.splitObjPath(objPath); const obj = eval(objName); // eslint-disable-line no-eval return utils.replaceWithProxy(obj, propName, handler); }; /** * Traverse nested properties of an object recursively and apply the given function on a whitelist of value types. * * @param {object} obj * @param {array} typeFilter - e.g. `['function']` * @param {Function} fn - e.g. `utils.patchToString` */ utils.execRecursively = (obj = {}, typeFilter = [], fn) => { function recurse(obj) { for (const key in obj) { if (obj[key] === undefined) { continue; } if (obj[key] && typeof obj[key] === 'object') { recurse(obj[key]); } else { if (obj[key] && typeFilter.includes(typeof obj[key])) { fn.call(this, obj[key]); } } } } recurse(obj); return obj; }; /** * Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one. * That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter. * * Unfortunately the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process. * This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings. * * We use this to pass down our utility functions as well as any other functions (to be able to split up code better). * * @see utils.materializeFns * * @param {object} fnObj - An object containing functions as properties */ utils.stringifyFns = (fnObj = {hello: () => 'world'}) => { // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine // https://github.com/feross/fromentries function fromEntries(iterable) { return [...iterable].reduce((obj, [key, val]) => { obj[key] = val; return obj; }, {}); } // noinspection JSUnusedLocalSymbols return (Object.fromEntries || fromEntries)( Object.entries(fnObj) .filter( ([key, value]) => typeof value === 'function', ) .map(([key, value]) => [ key, value.toString(), ]), ); }; /** * Utility function to reverse the process of `utils.stringifyFns`. * Will materialize an object with stringified functions (supports classic and fat arrow functions). * * @param {object} fnStrObj - An object containing stringified functions as properties */ utils.materializeFns = (fnStrObj = {hello: '() => \'world\''}) => { return Object.fromEntries( Object.entries(fnStrObj).map(([key, value]) => { if (value.startsWith('function')) { // some trickery is needed to make oldschool functions work :-) return [key, eval(`() => ${value}`)()]; // eslint-disable-line no-eval } else { // arrow functions just work return [key, eval(value)]; // eslint-disable-line no-eval } }), ); }; // Proxy handler templates for re-usability utils.makeHandler = () => ({ // Used by simple `navigator` getter evasions getterValue: value => ({ apply(target, thisArg, args) { const _Reflect = utils.cache.Reflect; // Let's fetch the value first, to trigger and escalate potential errors // Illegal invocations like `navigator.__proto__.vendor` will throw here _Reflect.apply(...arguments); return value; }, }), }); utils.sleep = (ms) => { return new utils.cache.Promise(resolve => setTimeout(resolve, ms)); }; utils.random = (a, b) => { const c = b - a + 1; return Math.floor(Math.random() * c + a); }; utils.isHex = (str) => { try { if (str && 'string' === typeof str) { if (str.startsWith('0x')) { str = str.substr(2); } return /^[A-F0-9]+$/i.test(str); } } catch (_) { } return false; }; utils.isInt = (str) => { try { const isHex = utils.isHex(str); if (isHex) { return true; } return ('' + parseInt(str)) === ('' + str); } catch (_) { } return false; }; utils.isUUID = (str) => { try { if ('string' === typeof str) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str); } } catch (_) { } return false; }; // utils.isSequence('haha') ===> false // utils.isSequence([]) ===> true // utils.isSequence(new Int8Array()) ===> true // utils.isSequence(new Set()) ===> true utils.isSequence = (obj) => { const _Object = utils.cache.Object; let desc = null; for (; obj && !!(desc = _Object.getOwnPropertyDescriptors(obj)); ) { if (desc.forEach) { return true; } obj = _Object.getPrototypeOf(obj); } return false; }; utils.intersectionSet = (a, b) => { if (b instanceof Array) { b = new Set(b); } return new Set([...a].filter(x => b.has(x))); }; utils.unionSet = (a, b) => { return new Set([...a, ...b]); }; utils.differenceABSet = (a, b) => { if (b instanceof Array) { b = new Set(b); } return new Set([...a].filter(x => !b.has(x))); }; utils.makeFuncName = (len) => { if (!len) { len = 4; } let result = ''; for (let n = 0; n < len; ++n) { result += String.fromCharCode(utils.random(65, 90)); } return result; }; utils.getCurrentScriptPath = () => { let a = {}, stack; try { a.b(); } catch (e) { // noinspection JSUnresolvedVariable stack = e.stack || e.sourceURL || e.stacktrace; } let rExtractUri = /(?:http|https|file):\/\/.*?\/.+?\.js/, absPath = rExtractUri.exec(stack); if (!absPath) { absPath = /(?:http|https|file):\/\/.*?\/.+?:?/.exec(stack); if (absPath) { absPath[0] = absPath[0].substr(0, absPath[0].length - 1); } } return (absPath && absPath[0]) || ''; }; utils.makePseudoClass = ( root, name, pseudoTarget, parentClass, ) => { const _Object = utils.cache.Object; const result = new Proxy( pseudoTarget || function () { throw utils.patchError(new TypeError(`Illegal constructor`), 'construct'); }, { // noinspection JSUnusedLocalSymbols construct(target, args) { throw utils.patchError(new TypeError(`Illegal constructor`), 'construct'); }, }, ); root[name] = result; _Object.defineProperty(result, 'name', { configurable: true, enumerable: false, writable: false, value: name, }); _Object.defineProperty(result, 'prototype', { configurable: false, enumerable: false, writable: false, value: result.prototype, }); utils.patchToString(result, `function ${name}() { [native code] }`); utils.patchToString(result.prototype.constructor, `function ${name}() { [native code] }`); _Object.defineProperty(result.prototype, Symbol.toStringTag, { configurable: true, enumerable: false, writable: false, value: name, }); if (parentClass && parentClass.prototype) { _Object.setPrototypeOf(result.prototype, parentClass.prototype); } return result; }; /** * The context is saved when the canvas.getContext is created. * @param context * @param operatorName * @returns {number} */ utils.markRenderingContextOperator = (context, operatorName) => { const result = utils.variables.renderingContextWithOperators.findIndex(e => e.context === context); if (result >= 0) { const operators = utils.variables.renderingContextWithOperators[result]; if (operators) { operators.operators[operatorName] = true; } } return result; }; /** * Find the context created by the external based on the canvas * @param canvas * @returns {{context: *, contextIndex: number}|{context: null, contextIndex: number}} */ utils.findRenderingContextIndex = (canvas) => { const contextIds = [ '2d', 'webgl', 'experimental-webgl', 'webgl2', 'experimental-webgl2', 'bitmaprenderer', ]; for (let contextId of contextIds) { let context = null; if (utils.cache.Object.getPrototypeOf(canvas) === OffscreenCanvas.prototype) { context = utils.cache.OffscreenCanvas.prototype.getContext.call(canvas, contextId); } else { context = utils.cache.HTMLCanvasElement.prototype.getContext.call(canvas, contextId); } const contextIndex = utils.variables.renderingContextWithOperators.findIndex(e => e.context === context); if (contextIndex >= 0) { return {context, contextIndex}; } } return {context: null, contextIndex: -1}; }; utils.osType = (userAgent) => { // https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform let result = 'Unknown' const OSArray = { 'Windows': false, 'macOS': false, 'Linux': false, 'iPhone': false, 'iPod': false, 'iPad': false, 'Android': false, } userAgent = userAgent.toLowerCase() OSArray['Windows'] = userAgent.includes('win32') || userAgent.includes('win64') || userAgent.includes('windows') OSArray['macOS'] = userAgent.includes('macintosh') || userAgent.includes('mac68k') || userAgent.includes('macppc') || userAgent.includes('macintosh') OSArray['Linux'] = userAgent.includes('linux') OSArray['iPhone'] = userAgent.includes('iphone') OSArray['iPod'] = userAgent.includes('ipod') OSArray['iPad'] = userAgent.includes('ipad') OSArray['Android'] = userAgent.includes('android') for (const i in OSArray) { if (OSArray[i]) { result = i } } return result } module.exports = utils;