From 11786348a2b24bb87d0cf9d628f12c9fed267c85 Mon Sep 17 00:00:00 2001 From: alexr_cybcube Date: Fri, 20 Jun 2025 03:40:38 +0300 Subject: [PATCH 1/8] chore: initital changes --- package-lock.json | 99 +++++ package.json | 2 + .../src/fingerprint-injector.ts | 87 +++++ packages/fingerprint-injector/src/utils.js | 1 + .../fingerprint-injector/crossbrowser.test.ts | 51 ++- .../fingerprint-injector.test.ts | 340 ++++++++++++++++-- 6 files changed, 543 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index b844bcaa..782a799b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.23.0", "@types/adm-zip": "^0.5.0", + "@types/chrome-remote-interface": "^0.31.14", "@types/jest": "^29.0.0", "@types/node": "^22.0.0", "@types/node-fetch": "^2.6.1", "@types/puppeteer": "^7.0.0", "@types/useragent": "^2.3.1", "browserslist": "^4.21.1", + "chrome-remote-interface": "^0.33.3", "eslint": "^9.23.0", "eslint-config-prettier": "^10.1.1", "fast-csv": "^5.0.0", @@ -1744,6 +1746,21 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chrome-remote-interface": { + "version": "0.31.14", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz", + "integrity": "sha512-H9hTcLu1y+Ms6GDPXXeGhgxaOSD69yEo674vjJw5EeW1tTwYo8fEkf7A9nWlnO6ArJsS7c41iZeX6mRDQ1LhEw==", + "dev": true, + "dependencies": { + "devtools-protocol": "0.0.927104" + } + }, + "node_modules/@types/chrome-remote-interface/node_modules/devtools-protocol": { + "version": "0.0.927104", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", + "integrity": "sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA==", + "dev": true + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2888,6 +2905,40 @@ "node": ">=10" } }, + "node_modules/chrome-remote-interface": { + "version": "0.33.3", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", + "integrity": "sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==", + "dev": true, + "dependencies": { + "commander": "2.11.x", + "ws": "^7.2.0" + }, + "bin": { + "chrome-remote-interface": "bin/client.js" + } + }, + "node_modules/chrome-remote-interface/node_modules/ws": { + "version": "7.5.10", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/chromium-bidi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", @@ -3000,6 +3051,12 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.11.0", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -10903,6 +10960,23 @@ "@babel/types": "^7.20.7" } }, + "@types/chrome-remote-interface": { + "version": "0.31.14", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz", + "integrity": "sha512-H9hTcLu1y+Ms6GDPXXeGhgxaOSD69yEo674vjJw5EeW1tTwYo8fEkf7A9nWlnO6ArJsS7c41iZeX6mRDQ1LhEw==", + "dev": true, + "requires": { + "devtools-protocol": "0.0.927104" + }, + "dependencies": { + "devtools-protocol": { + "version": "0.0.927104", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", + "integrity": "sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA==", + "dev": true + } + } + }, "@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -11734,6 +11808,25 @@ "dev": true, "peer": true }, + "chrome-remote-interface": { + "version": "0.33.3", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", + "integrity": "sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==", + "dev": true, + "requires": { + "commander": "2.11.x", + "ws": "^7.2.0" + }, + "dependencies": { + "ws": { + "version": "7.5.10", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "requires": {} + } + } + }, "chromium-bidi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", @@ -11818,6 +11911,12 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.11.0", + "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", diff --git a/package.json b/package.json index 92f396fa..ea582ea5 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,14 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.23.0", "@types/adm-zip": "^0.5.0", + "@types/chrome-remote-interface": "^0.31.14", "@types/jest": "^29.0.0", "@types/node": "^22.0.0", "@types/node-fetch": "^2.6.1", "@types/puppeteer": "^7.0.0", "@types/useragent": "^2.3.1", "browserslist": "^4.21.1", + "chrome-remote-interface": "^0.33.3", "eslint": "^9.23.0", "eslint-config-prettier": "^10.1.1", "fast-csv": "^5.0.0", diff --git a/packages/fingerprint-injector/src/fingerprint-injector.ts b/packages/fingerprint-injector/src/fingerprint-injector.ts index 71ff881d..2cf13529 100644 --- a/packages/fingerprint-injector/src/fingerprint-injector.ts +++ b/packages/fingerprint-injector/src/fingerprint-injector.ts @@ -1,4 +1,6 @@ import { readFileSync } from 'fs'; +import * as ptc from "devtools-protocol"; +import CDP from 'chrome-remote-interface'; import { BrowserFingerprintWithHeaders, @@ -19,6 +21,13 @@ interface EnhancedFingerprint extends Fingerprint { historyLength: number; } +type AttachFingerprintToCDPparams = { + page: CDP.StableDomains['Page']; + network: CDP.StableDomains['Network']; + emulation: CDP.StableDomains['Emulation']; + browser: CDP.StableDomains['Browser']; +} + declare function overrideInstancePrototype( instance: T, overrideObj: Partial, @@ -166,6 +175,70 @@ export class FingerprintInjector { ); } + /** + * Adds script that is evaluated before every document creation. + * Sets User-Agent and viewport using native puppeteer interface + * @param params AttachFingerprintToCDPparams `AttachFingerprintToCDPparams` object to be injected with the fingerprint. + * @param fingerprint Fingerprint from [`fingerprint-generator`](https://github.com/apify/fingerprint-generator). + */ + async attachFingerprintToCDP( + { page, network, emulation, browser }: AttachFingerprintToCDPparams, + browserFingerprintWithHeaders: BrowserFingerprintWithHeaders, + ): Promise { + const { fingerprint, headers } = browserFingerprintWithHeaders; + const enhancedFingerprint = this._enhanceFingerprint(fingerprint); + const { screen, userAgent } = enhancedFingerprint; + + await network.setUserAgentOverride({ userAgent }); + const { product: browserVersion } = await browser.getVersion(); + + if (!browserVersion.toLowerCase().includes('firefox')) { + page.setDeviceMetricsOverride({ + screenHeight: screen.height, + screenWidth: screen.width, + width: screen.width, + height: screen.height, + mobile: /phone|android|mobile/i.test(userAgent), + screenOrientation: + screen.height > screen.width + ? { angle: 0, type: 'portraitPrimary' } + : { angle: 90, type: 'landscapePrimary' }, + deviceScaleFactor: screen.devicePixelRatio, + }); + + await network.setExtraHTTPHeaders({ + headers: this.onlyInjectableHeaders(headers, browserVersion), + }); + + await emulation.setEmulatedMedia({ + features: [{ name: 'prefers-color-scheme', value: 'dark' }], + }); + } + + // const frameTreeResponse = await page.getFrameTree(); + // const frames = [frameTreeResponse.frameTree.frame.id]; + // + // const frameTreeResolver = ( + // {frame, childFrames}: ptc.Protocol.Page.FrameTree + // ): void => { + // if (frame) { + // frames.push(frame.id); + // for (const childFrame of childFrames ?? []) { + // frameTreeResolver(childFrame); + // } + // } + // } + // if (frameTreeResponse.frameTree.childFrames) { + // for (const childFrame of frameTreeResponse.frameTree.childFrames) { + // frameTreeResolver(childFrame); + // } + // } + + await page.addScriptToEvaluateOnNewDocument({ + source: this.getInjectableFingerprintFunction(enhancedFingerprint), + }); + } + /** * Gets the override script that should be evaluated in the browser. */ @@ -375,3 +448,17 @@ export async function newInjectedPage( return page; } + +export async function newCDPInjector(args: AttachFingerprintToCDPparams, options?: { + fingerprint?: BrowserFingerprintWithHeaders; + fingerprintOptions?: Partial; +}): Promise { + + const generator = new FingerprintGenerator(); + const fingerprintWithHeaders = + options?.fingerprint ?? + generator.getFingerprint(options?.fingerprintOptions ?? {}); + const injector = new FingerprintInjector(); + await injector.attachFingerprintToCDP(args, fingerprintWithHeaders); +} + diff --git a/packages/fingerprint-injector/src/utils.js b/packages/fingerprint-injector/src/utils.js index 669e4ee7..ec0929af 100644 --- a/packages/fingerprint-injector/src/utils.js +++ b/packages/fingerprint-injector/src/utils.js @@ -525,6 +525,7 @@ function replace(target, key, value) { // Replaces all the WebRTC related methods with a recursive ES6 Proxy // This way, we don't have to model a mock WebRTC API and we still don't get any exceptions. function blockWebRTC() { + console.log("Blocking WebRTC"); const handler = { get: () => { return new Proxy(() => {}, handler); diff --git a/test/fingerprint-injector/crossbrowser.test.ts b/test/fingerprint-injector/crossbrowser.test.ts index 53f77f5b..c681b37a 100644 --- a/test/fingerprint-injector/crossbrowser.test.ts +++ b/test/fingerprint-injector/crossbrowser.test.ts @@ -1,10 +1,12 @@ import { FingerprintInjector, + newCDPInjector, newInjectedContext, newInjectedPage, } from 'fingerprint-injector'; import playwright from 'playwright'; -import puppeteer from 'puppeteer'; +import puppeteer, { Browser } from 'puppeteer'; +import CDP from "chrome-remote-interface"; function generateCartesianMatrix(A: any, B: any) { const matrix = []; @@ -81,3 +83,50 @@ describe('Puppeteer controlled instances', () => { }, ); }); +describe('CDP controller instances', () => { + const fingerprintBrowsers = [ + 'chrome', + 'firefox', + 'safari', + 'edge', + ] as const; + test.each(fingerprintBrowsers)( + `should inject %s fingerprint`, + async (fingerprintBrowser: (typeof fingerprintBrowsers)[number]) => { + const puppeteer_browser = await puppeteer.launch({ + browser: 'chrome', + debuggingPort: 9222, // need to specify it explicitly or puppeteer launcher won't open it up + headless: true, + }); + const webSocketDebuggerUrl = puppeteer_browser.wsEndpoint(); + const client = await CDP({ target: webSocketDebuggerUrl }); + const { Target } = client; + + // getting the default 'about:blank' page + const { targetInfos} = await Target.getTargets(); + const ctx_client = await CDP({ target: targetInfos[0].targetId }); + + const { Network, Page, Browser, Emulation } = ctx_client; + await Network.enable(); + await Page.enable(); + + await newCDPInjector({ + network: Network, + page: Page, + browser: Browser, + emulation: Emulation, + }, { + fingerprintOptions: { + browsers: [fingerprintBrowser], + }, + }); + + const { frameId } = await Page.navigate({ + url: 'http://example.com', + }); + expect(frameId).toBeDefined(); + await client.close(); + await puppeteer_browser.close(); + }, + ); +}); diff --git a/test/fingerprint-injector/fingerprint-injector.test.ts b/test/fingerprint-injector/fingerprint-injector.test.ts index 28e80a2a..b7daef4c 100644 --- a/test/fingerprint-injector/fingerprint-injector.test.ts +++ b/test/fingerprint-injector/fingerprint-injector.test.ts @@ -12,33 +12,61 @@ import { } from 'fingerprint-injector'; import playwright, { chromium, type Browser as PWBrowser } from 'playwright'; import puppeteer, { Browser as PPBrowser } from 'puppeteer'; +import CDP from 'chrome-remote-interface'; +import * as ptc from "devtools-protocol"; const cases = [ + // [ + // 'Playwright', + // [ + // { + // name: 'Firefox', + // launcher: playwright.firefox, + // options: {}, + // fingerprintGeneratorOptions: { + // browsers: [{ name: 'firefox', minVersion: 96 }], + // }, + // }, + // { + // name: 'Chrome', + // launcher: playwright.chromium, + // options: { + // channel: 'chrome', + // }, + // fingerprintGeneratorOptions: { + // browsers: [{ name: 'chrome', minVersion: 90 }], + // }, + // }, + // ], + // ], + // [ + // 'Puppeteer', + // [ + // { + // name: 'Chrome', + // launcher: puppeteer, + // options: { + // args: ['--no-sandbox', '--use-gl=desktop'], + // channel: 'chrome', + // }, + // fingerprintGeneratorOptions: { + // browsers: [{ name: 'chrome', minVersion: 90 }], + // }, + // }, + // { + // name: 'Chromium', + // launcher: puppeteer, + // options: { + // args: ['--no-sandbox', '--use-gl=desktop'], + // }, + // fingerprintGeneratorOptions: { + // browsers: [{ name: 'chrome', minVersion: 90 }], + // }, + // }, + // ], + // ], [ - 'Playwright', - [ - { - name: 'Firefox', - launcher: playwright.firefox, - options: {}, - fingerprintGeneratorOptions: { - browsers: [{ name: 'firefox', minVersion: 96 }], - }, - }, - { - name: 'Chrome', - launcher: playwright.chromium, - options: { - channel: 'chrome', - }, - fingerprintGeneratorOptions: { - browsers: [{ name: 'chrome', minVersion: 90 }], - }, - }, - ], - ], - [ - 'Puppeteer', + 'CDP', [ { name: 'Chrome', @@ -46,16 +74,8 @@ const cases = [ options: { args: ['--no-sandbox', '--use-gl=desktop'], channel: 'chrome', - }, - fingerprintGeneratorOptions: { - browsers: [{ name: 'chrome', minVersion: 90 }], - }, - }, - { - name: 'Chromium', - launcher: puppeteer, - options: { - args: ['--no-sandbox', '--use-gl=desktop'], + headless: false, + debuggingPort: 9222, }, fingerprintGeneratorOptions: { browsers: [{ name: 'chrome', minVersion: 90 }], @@ -76,7 +96,6 @@ describe('FingerprintInjector', () => { // eslint-disable-next-line dot-notation expect(fpInjector['utilsJs']).toBeTruthy(); }); - // @ts-expect-error test only describe.each(cases)('%s', (frameworkName, testCases) => { // @ts-expect-error test only describe.each(testCases)( @@ -90,6 +109,7 @@ describe('FingerprintInjector', () => { let fingerprintWithHeaders: BrowserFingerprintWithHeaders; let fingerprint: Fingerprint; let context: any; + let client: CDP.Client | undefined; beforeAll(async () => { fingerprintGenerator = new FingerprintGenerator({ @@ -108,6 +128,8 @@ describe('FingerprintInjector', () => { if (frameworkName === 'Playwright') { browser = (await launcher.launch({ headless: false, + browser: 'chrome', + debuggingPort: 9222, ...options, })) as import('playwright').Browser; @@ -135,6 +157,149 @@ describe('FingerprintInjector', () => { fingerprintWithHeaders, ); + response = await page.goto( + `file://${__dirname}/test.html`, + ); + } else if (frameworkName === 'CDP') { + browser = await launcher.launch({ + headless: false, + debuggingPort: 9222, + ...options, + }); + + const client = await CDP({ + target: browser.wsEndpoint(), + }); + + const { targetInfos } = + await client.Target.getTargets(); + + const ctx_client = await CDP({ + target: targetInfos[0].targetId, + }); + + const { Page, Network, Emulation, Runtime, Target, Fetch } = + ctx_client; + + await Page.enable(); + await Network.enable(); + + // TODO: remove after testing, undefined on per-case runs + fpInjector ??= new FingerprintInjector(); + + await fpInjector.attachFingerprintToCDP( + { + page: Page, + network: Network, + emulation: Emulation, + browser: client.Browser, + }, + fingerprintWithHeaders, + ); + + let ctx = new Map< + string, + { + sessionId: string; + contextId: number; + } + >(); + const contextDetacher = async ({ + targetInfo, + }: { + targetInfo?: ptc.Protocol.Target.TargetInfo; + }) => { + Runtime.on( + 'executionContextCreated', + async ({ context }) => { + if (!targetInfo) { + ({ targetInfo } = + await Target.getTargetInfo()); + } + const { sessionId } = + await client.Target.attachToTarget({ + targetId: targetInfo.targetId, + flatten: true, + }); + ctx.set(targetInfo.targetId, { + sessionId, + contextId: context.id, + }); + await Runtime.disable(); + }, + ); + await Runtime.enable(); + }; + await Promise.all( + targetInfos.map((ti) => + contextDetacher({ targetInfo: ti }), + ), + ); + + const responseHeaders = new Map>(); + Network.on('responseReceived', (params) => { + if (params.type === 'Document') { + console.log( + 'Received response for frame:', + params.frameId, + ); + responseHeaders.set( + params.frameId, + params.response.headers, + ); + } + }); + const requestHeaders = new Map>(); + Network.requestWillBeSent((params) => { + if ( + params.type === 'Document' + ) { + let lowerCase: Record = {}; + for (const header of Object.keys(params.request.headers)) { + lowerCase[header.toLowerCase()] = params.request.headers[header]; + } + requestHeaders.set( + params.frameId, + lowerCase, + ); + } + }); + + page = { + evaluate: async ( + fn: (...args: unknown[]) => unknown, + ...args: unknown[] + ) => { + const stringified = stringifyFunction(fn); + const { targetInfo: ti } = + await Target.getTargetInfo(); + const sess = ctx.get(ti.targetId); + const evaluated = await Runtime.callFunctionOn({ + functionDeclaration: stringified, + ...(sess + ? { executionContextId: sess.contextId } + : {}), + arguments: args.map((a) => ({ value: a })), + awaitPromise: true, + + returnByValue: true, + }); + return evaluated.result.value; + }, + goto: async (url: string) => { + console.log('Navigating to:', url); + const { frameId } = await Page.navigate({ + url, + }); + await Page.loadEventFired(); + + return { + request: () => ({ + headers: () => requestHeaders.get(frameId), + }), + }; + }, + }; response = await page.goto( `file://${__dirname}/test.html`, ); @@ -143,6 +308,9 @@ describe('FingerprintInjector', () => { }); afterAll(async () => { + if (client) { + await client.close() + } if (browser) { await browser.close(); } @@ -377,6 +545,7 @@ describe('FingerprintInjector', () => { const requestHeaders = (await requestObject.allHeaders?.()) ?? requestObject.headers?.(); + console.log('req headers: ',requestHeaders) const { headers } = fingerprintWithHeaders; // eslint-disable-next-line dot-notation @@ -385,6 +554,7 @@ describe('FingerprintInjector', () => { ]; for (const header of Object.keys(onlyInjectable(headers))) { + console.log('header: ', header, 'value: ', headers[header]) expect(requestHeaders[header]).toBe(headers[header]); } }); @@ -408,7 +578,6 @@ describe('FingerprintInjector', () => { ); }); - // @ts-expect-error test only describe.each(cases)('%s', (frameworkName, testCases) => { // @ts-expect-error test only describe.each(testCases)( @@ -438,6 +607,72 @@ describe('FingerprintInjector', () => { await fpInjector.attachFingerprintToPuppeteer(page, fp); return page; } + if (frameworkName === 'CDP') { + const client = await CDP({ + target: (browser as PPBrowser).wsEndpoint(), + }); + const orig_close = browser.close; + const { targetInfos } = + await client.Target.getTargets(); + + const ctx_client = await CDP({ + target: targetInfos[0].targetId, + }); + + browser.close = async () => { + await client.close(); + await ctx_client.close(); + return orig_close.call(browser); + }; + + const { Page, Network, Emulation, Runtime } = + ctx_client; + await Page.addScriptToEvaluateOnNewDocument({ + source: ` + window.$ = s => document.querySelector(s); + window.$$ = s => Array.from(document.querySelectorAll(s)); + `, + }) + await Network.enable(); + await fpInjector.attachFingerprintToCDP( + { + page: Page, + network: Network, + emulation: Emulation, + browser: client.Browser, + }, + fp, + ); + return { + evaluate: async ( + fn: (...args: unknown[]) => unknown, + ...args: unknown[] + ) => { + const stringified = stringifyFunction(fn); + const evaluated = await Runtime.callFunctionOn({ + functionDeclaration: stringified, + arguments: args.map((a) => ({ value: a })), + awaitPromise: true, + + returnByValue: true, + }); + return evaluated.result.value; + }, + goto: async (url: string) => { + await Page.navigate({ url }); + return; + }, + + $: async (selector: string) => { + console.log('Selecting element:', selector); + const { result } = await Runtime.evaluate({ + expression: `document.querySelector('${selector}')`, + returnByValue: true, + }); + return result.value; + }, + }; + } throw new Error(`Unknown framework name ${frameworkName}`); }; @@ -555,3 +790,36 @@ describe('FingerprintInjector', () => { }); }); }); + +// from https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/util/Function.ts#L30 +function stringifyFunction(fn: (...args: never) => unknown): string { + let value = fn.toString(); + try { + new Function(`(${value})`); + } catch (err) { + if ( + (err as Error).message.includes( + `Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive`, + ) + ) { + // The content security policy does not allow Function eval. Let's + // assume the value might be valid as is. + return value; + } + // This means we might have a function shorthand (e.g. `test(){}`). Let's + // try prefixing. + let prefix = 'function '; + if (value.startsWith('async ')) { + prefix = `async ${prefix}`; + value = value.substring('async '.length); + } + value = `${prefix}${value}`; + try { + new Function(`(${value})`); + } catch { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function cannot be serialized!'); + } + } + return value; + } From 695378afd1747a015e24ff68074a298bd21f0550 Mon Sep 17 00:00:00 2001 From: alexr_cybcube Date: Fri, 20 Jun 2025 06:00:32 +0300 Subject: [PATCH 2/8] chore: cleanup --- .../src/fingerprint-injector.ts | 35 +--- packages/fingerprint-injector/src/utils.js | 1 - .../fingerprint-injector.test.ts | 168 +++++++----------- 3 files changed, 70 insertions(+), 134 deletions(-) diff --git a/packages/fingerprint-injector/src/fingerprint-injector.ts b/packages/fingerprint-injector/src/fingerprint-injector.ts index 2cf13529..a11a8f95 100644 --- a/packages/fingerprint-injector/src/fingerprint-injector.ts +++ b/packages/fingerprint-injector/src/fingerprint-injector.ts @@ -1,5 +1,4 @@ import { readFileSync } from 'fs'; -import * as ptc from "devtools-protocol"; import CDP from 'chrome-remote-interface'; import { @@ -26,7 +25,7 @@ type AttachFingerprintToCDPparams = { network: CDP.StableDomains['Network']; emulation: CDP.StableDomains['Emulation']; browser: CDP.StableDomains['Browser']; -} +}; declare function overrideInstancePrototype( instance: T, @@ -215,25 +214,6 @@ export class FingerprintInjector { }); } - // const frameTreeResponse = await page.getFrameTree(); - // const frames = [frameTreeResponse.frameTree.frame.id]; - // - // const frameTreeResolver = ( - // {frame, childFrames}: ptc.Protocol.Page.FrameTree - // ): void => { - // if (frame) { - // frames.push(frame.id); - // for (const childFrame of childFrames ?? []) { - // frameTreeResolver(childFrame); - // } - // } - // } - // if (frameTreeResponse.frameTree.childFrames) { - // for (const childFrame of frameTreeResponse.frameTree.childFrames) { - // frameTreeResolver(childFrame); - // } - // } - await page.addScriptToEvaluateOnNewDocument({ source: this.getInjectableFingerprintFunction(enhancedFingerprint), }); @@ -449,11 +429,13 @@ export async function newInjectedPage( return page; } -export async function newCDPInjector(args: AttachFingerprintToCDPparams, options?: { - fingerprint?: BrowserFingerprintWithHeaders; - fingerprintOptions?: Partial; -}): Promise { - +export async function newCDPInjector( + args: AttachFingerprintToCDPparams, + options?: { + fingerprint?: BrowserFingerprintWithHeaders; + fingerprintOptions?: Partial; + }, +): Promise { const generator = new FingerprintGenerator(); const fingerprintWithHeaders = options?.fingerprint ?? @@ -461,4 +443,3 @@ export async function newCDPInjector(args: AttachFingerprintToCDPparams, options const injector = new FingerprintInjector(); await injector.attachFingerprintToCDP(args, fingerprintWithHeaders); } - diff --git a/packages/fingerprint-injector/src/utils.js b/packages/fingerprint-injector/src/utils.js index ec0929af..669e4ee7 100644 --- a/packages/fingerprint-injector/src/utils.js +++ b/packages/fingerprint-injector/src/utils.js @@ -525,7 +525,6 @@ function replace(target, key, value) { // Replaces all the WebRTC related methods with a recursive ES6 Proxy // This way, we don't have to model a mock WebRTC API and we still don't get any exceptions. function blockWebRTC() { - console.log("Blocking WebRTC"); const handler = { get: () => { return new Proxy(() => {}, handler); diff --git a/test/fingerprint-injector/fingerprint-injector.test.ts b/test/fingerprint-injector/fingerprint-injector.test.ts index b7daef4c..9b37cfb2 100644 --- a/test/fingerprint-injector/fingerprint-injector.test.ts +++ b/test/fingerprint-injector/fingerprint-injector.test.ts @@ -13,58 +13,57 @@ import { import playwright, { chromium, type Browser as PWBrowser } from 'playwright'; import puppeteer, { Browser as PPBrowser } from 'puppeteer'; import CDP from 'chrome-remote-interface'; -import * as ptc from "devtools-protocol"; const cases = [ - // [ - // 'Playwright', - // [ - // { - // name: 'Firefox', - // launcher: playwright.firefox, - // options: {}, - // fingerprintGeneratorOptions: { - // browsers: [{ name: 'firefox', minVersion: 96 }], - // }, - // }, - // { - // name: 'Chrome', - // launcher: playwright.chromium, - // options: { - // channel: 'chrome', - // }, - // fingerprintGeneratorOptions: { - // browsers: [{ name: 'chrome', minVersion: 90 }], - // }, - // }, - // ], - // ], - // [ - // 'Puppeteer', - // [ - // { - // name: 'Chrome', - // launcher: puppeteer, - // options: { - // args: ['--no-sandbox', '--use-gl=desktop'], - // channel: 'chrome', - // }, - // fingerprintGeneratorOptions: { - // browsers: [{ name: 'chrome', minVersion: 90 }], - // }, - // }, - // { - // name: 'Chromium', - // launcher: puppeteer, - // options: { - // args: ['--no-sandbox', '--use-gl=desktop'], - // }, - // fingerprintGeneratorOptions: { - // browsers: [{ name: 'chrome', minVersion: 90 }], - // }, - // }, - // ], - // ], + [ + 'Playwright', + [ + { + name: 'Firefox', + launcher: playwright.firefox, + options: {}, + fingerprintGeneratorOptions: { + browsers: [{ name: 'firefox', minVersion: 96 }], + }, + }, + { + name: 'Chrome', + launcher: playwright.chromium, + options: { + channel: 'chrome', + }, + fingerprintGeneratorOptions: { + browsers: [{ name: 'chrome', minVersion: 90 }], + }, + }, + ], + ], + [ + 'Puppeteer', + [ + { + name: 'Chrome', + launcher: puppeteer, + options: { + args: ['--no-sandbox', '--use-gl=desktop'], + channel: 'chrome', + }, + fingerprintGeneratorOptions: { + browsers: [{ name: 'chrome', minVersion: 90 }], + }, + }, + { + name: 'Chromium', + launcher: puppeteer, + options: { + args: ['--no-sandbox', '--use-gl=desktop'], + }, + fingerprintGeneratorOptions: { + browsers: [{ name: 'chrome', minVersion: 90 }], + }, + }, + ], + ], [ 'CDP', [ @@ -83,7 +82,7 @@ const cases = [ }, ], ], -]; +] as const; describe('FingerprintInjector', () => { let fpInjector: FingerprintInjector; @@ -178,14 +177,14 @@ describe('FingerprintInjector', () => { target: targetInfos[0].targetId, }); - const { Page, Network, Emulation, Runtime, Target, Fetch } = + const { Page, Network, Emulation, Runtime, Target } = ctx_client; await Page.enable(); await Network.enable(); // TODO: remove after testing, undefined on per-case runs - fpInjector ??= new FingerprintInjector(); + // fpInjector ??= new FingerprintInjector(); await fpInjector.attachFingerprintToCDP( { @@ -197,52 +196,9 @@ describe('FingerprintInjector', () => { fingerprintWithHeaders, ); - let ctx = new Map< - string, - { - sessionId: string; - contextId: number; - } - >(); - const contextDetacher = async ({ - targetInfo, - }: { - targetInfo?: ptc.Protocol.Target.TargetInfo; - }) => { - Runtime.on( - 'executionContextCreated', - async ({ context }) => { - if (!targetInfo) { - ({ targetInfo } = - await Target.getTargetInfo()); - } - const { sessionId } = - await client.Target.attachToTarget({ - targetId: targetInfo.targetId, - flatten: true, - }); - ctx.set(targetInfo.targetId, { - sessionId, - contextId: context.id, - }); - await Runtime.disable(); - }, - ); - await Runtime.enable(); - }; - await Promise.all( - targetInfos.map((ti) => - contextDetacher({ targetInfo: ti }), - ), - ); - const responseHeaders = new Map>(); Network.on('responseReceived', (params) => { if (params.type === 'Document') { - console.log( - 'Received response for frame:', - params.frameId, - ); responseHeaders.set( params.frameId, params.response.headers, @@ -273,21 +229,24 @@ describe('FingerprintInjector', () => { const stringified = stringifyFunction(fn); const { targetInfo: ti } = await Target.getTargetInfo(); - const sess = ctx.get(ti.targetId); + const { sessionId} = await Target.attachToTarget({ + targetId: ti.targetId, + flatten: true, + }) + ctx_client.send('Runtime.enable', undefined, sessionId) + const ctx = await Runtime.executionContextCreated() const evaluated = await Runtime.callFunctionOn({ functionDeclaration: stringified, - ...(sess - ? { executionContextId: sess.contextId } - : {}), + executionContextId: ctx.context.id, arguments: args.map((a) => ({ value: a })), awaitPromise: true, returnByValue: true, - }); + }, sessionId); + await Runtime.disable(); return evaluated.result.value; }, goto: async (url: string) => { - console.log('Navigating to:', url); const { frameId } = await Page.navigate({ url, }); @@ -545,7 +504,6 @@ describe('FingerprintInjector', () => { const requestHeaders = (await requestObject.allHeaders?.()) ?? requestObject.headers?.(); - console.log('req headers: ',requestHeaders) const { headers } = fingerprintWithHeaders; // eslint-disable-next-line dot-notation @@ -554,7 +512,6 @@ describe('FingerprintInjector', () => { ]; for (const header of Object.keys(onlyInjectable(headers))) { - console.log('header: ', header, 'value: ', headers[header]) expect(requestHeaders[header]).toBe(headers[header]); } }); @@ -627,6 +584,7 @@ describe('FingerprintInjector', () => { const { Page, Network, Emulation, Runtime } = ctx_client; + await Page.enable(); await Page.addScriptToEvaluateOnNewDocument({ source: ` window.$ = s => document.querySelector(s); @@ -660,11 +618,9 @@ describe('FingerprintInjector', () => { }, goto: async (url: string) => { await Page.navigate({ url }); - return; }, $: async (selector: string) => { - console.log('Selecting element:', selector); const { result } = await Runtime.evaluate({ expression: `document.querySelector('${selector}')`, returnByValue: true, From 9a19fc0ab564ea5df9c9e9fc15da5e8e479f6e9a Mon Sep 17 00:00:00 2001 From: alexr_cybcube Date: Fri, 20 Jun 2025 06:03:31 +0300 Subject: [PATCH 3/8] fix: internal repo links --- package-lock.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 782a799b..e2901f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1748,7 +1748,7 @@ }, "node_modules/@types/chrome-remote-interface": { "version": "0.31.14", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz", + "resolved": "https://registry.npmjs.org/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz", "integrity": "sha512-H9hTcLu1y+Ms6GDPXXeGhgxaOSD69yEo674vjJw5EeW1tTwYo8fEkf7A9nWlnO6ArJsS7c41iZeX6mRDQ1LhEw==", "dev": true, "dependencies": { @@ -1757,7 +1757,7 @@ }, "node_modules/@types/chrome-remote-interface/node_modules/devtools-protocol": { "version": "0.0.927104", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", "integrity": "sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA==", "dev": true }, @@ -2907,7 +2907,7 @@ }, "node_modules/chrome-remote-interface": { "version": "0.33.3", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", "integrity": "sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==", "dev": true, "dependencies": { @@ -2920,7 +2920,7 @@ }, "node_modules/chrome-remote-interface/node_modules/ws": { "version": "7.5.10", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/ws/-/ws-7.5.10.tgz", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { @@ -3053,7 +3053,7 @@ }, "node_modules/commander": { "version": "2.11.0", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/commander/-/commander-2.11.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", "dev": true }, @@ -10962,7 +10962,7 @@ }, "@types/chrome-remote-interface": { "version": "0.31.14", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz", + "resolved": "https://registry.npmjs.org/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz", "integrity": "sha512-H9hTcLu1y+Ms6GDPXXeGhgxaOSD69yEo674vjJw5EeW1tTwYo8fEkf7A9nWlnO6ArJsS7c41iZeX6mRDQ1LhEw==", "dev": true, "requires": { @@ -10971,7 +10971,7 @@ "dependencies": { "devtools-protocol": { "version": "0.0.927104", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", "integrity": "sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA==", "dev": true } @@ -11810,7 +11810,7 @@ }, "chrome-remote-interface": { "version": "0.33.3", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", "integrity": "sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==", "dev": true, "requires": { @@ -11820,7 +11820,7 @@ "dependencies": { "ws": { "version": "7.5.10", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/ws/-/ws-7.5.10.tgz", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "requires": {} @@ -11913,7 +11913,7 @@ }, "commander": { "version": "2.11.0", - "resolved": "http://nexus.internal.cybcube.com/repository/npm-cybcube/commander/-/commander-2.11.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", "dev": true }, From 3e668921e064a65a132c508d9f8132ca031c218d Mon Sep 17 00:00:00 2001 From: alexr_cybcube Date: Fri, 20 Jun 2025 06:16:55 +0300 Subject: [PATCH 4/8] chore: more cleanup --- .../fingerprint-injector.test.ts | 18 +----------------- .../testNetworkDefinition.zip | Bin 14682 -> 14659 bytes 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/test/fingerprint-injector/fingerprint-injector.test.ts b/test/fingerprint-injector/fingerprint-injector.test.ts index 9b37cfb2..cd65e12b 100644 --- a/test/fingerprint-injector/fingerprint-injector.test.ts +++ b/test/fingerprint-injector/fingerprint-injector.test.ts @@ -127,8 +127,6 @@ describe('FingerprintInjector', () => { if (frameworkName === 'Playwright') { browser = (await launcher.launch({ headless: false, - browser: 'chrome', - debuggingPort: 9222, ...options, })) as import('playwright').Browser; @@ -565,10 +563,10 @@ describe('FingerprintInjector', () => { return page; } if (frameworkName === 'CDP') { + const orig_close = browser.close; const client = await CDP({ target: (browser as PPBrowser).wsEndpoint(), }); - const orig_close = browser.close; const { targetInfos } = await client.Target.getTargets(); @@ -602,20 +600,6 @@ describe('FingerprintInjector', () => { fp, ); return { - evaluate: async ( - fn: (...args: unknown[]) => unknown, - ...args: unknown[] - ) => { - const stringified = stringifyFunction(fn); - const evaluated = await Runtime.callFunctionOn({ - functionDeclaration: stringified, - arguments: args.map((a) => ({ value: a })), - awaitPromise: true, - - returnByValue: true, - }); - return evaluated.result.value; - }, goto: async (url: string) => { await Page.navigate({ url }); }, diff --git a/test/generative-bayesian-network/testNetworkDefinition.zip b/test/generative-bayesian-network/testNetworkDefinition.zip index 6ec3ce563e90884c981894e261ae07d189c3ddb3..f01070e2527a122470efae543731d0030beb93c5 100644 GIT binary patch literal 14659 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 Date: Fri, 20 Jun 2025 06:45:51 +0300 Subject: [PATCH 5/8] chore: cleanup unneded code --- .../fingerprint-injector.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/fingerprint-injector/fingerprint-injector.test.ts b/test/fingerprint-injector/fingerprint-injector.test.ts index cd65e12b..20664f26 100644 --- a/test/fingerprint-injector/fingerprint-injector.test.ts +++ b/test/fingerprint-injector/fingerprint-injector.test.ts @@ -194,15 +194,6 @@ describe('FingerprintInjector', () => { fingerprintWithHeaders, ); - const responseHeaders = new Map>(); - Network.on('responseReceived', (params) => { - if (params.type === 'Document') { - responseHeaders.set( - params.frameId, - params.response.headers, - ); - } - }); const requestHeaders = new Map>(); Network.requestWillBeSent((params) => { if ( @@ -583,12 +574,6 @@ describe('FingerprintInjector', () => { const { Page, Network, Emulation, Runtime } = ctx_client; await Page.enable(); - await Page.addScriptToEvaluateOnNewDocument({ - source: ` - window.$ = s => document.querySelector(s); - window.$$ = s => Array.from(document.querySelectorAll(s)); - `, - }) await Network.enable(); await fpInjector.attachFingerprintToCDP( { From ea07d0fcd7632850b6f084488025551fefb63b8c Mon Sep 17 00:00:00 2001 From: EmpiresHQ Date: Sat, 21 Jun 2025 17:49:03 +0300 Subject: [PATCH 6/8] chore: import types instad of lib --- packages/fingerprint-injector/src/fingerprint-injector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fingerprint-injector/src/fingerprint-injector.ts b/packages/fingerprint-injector/src/fingerprint-injector.ts index a11a8f95..1091b1a7 100644 --- a/packages/fingerprint-injector/src/fingerprint-injector.ts +++ b/packages/fingerprint-injector/src/fingerprint-injector.ts @@ -1,5 +1,5 @@ import { readFileSync } from 'fs'; -import CDP from 'chrome-remote-interface'; +import type CDP from 'chrome-remote-interface'; import { BrowserFingerprintWithHeaders, From 99b95a4c6cce105b50e6951d1d74d594e5c66dc5 Mon Sep 17 00:00:00 2001 From: EmpiresHQ Date: Sat, 21 Jun 2025 17:54:52 +0300 Subject: [PATCH 7/8] fix: lint --- .../fingerprint-injector/crossbrowser.test.ts | 25 ++-- .../fingerprint-injector.test.ts | 125 ++++++++++-------- 2 files changed, 83 insertions(+), 67 deletions(-) diff --git a/test/fingerprint-injector/crossbrowser.test.ts b/test/fingerprint-injector/crossbrowser.test.ts index c681b37a..a1102d37 100644 --- a/test/fingerprint-injector/crossbrowser.test.ts +++ b/test/fingerprint-injector/crossbrowser.test.ts @@ -6,7 +6,7 @@ import { } from 'fingerprint-injector'; import playwright from 'playwright'; import puppeteer, { Browser } from 'puppeteer'; -import CDP from "chrome-remote-interface"; +import CDP from 'chrome-remote-interface'; function generateCartesianMatrix(A: any, B: any) { const matrix = []; @@ -103,23 +103,26 @@ describe('CDP controller instances', () => { const { Target } = client; // getting the default 'about:blank' page - const { targetInfos} = await Target.getTargets(); + const { targetInfos } = await Target.getTargets(); const ctx_client = await CDP({ target: targetInfos[0].targetId }); const { Network, Page, Browser, Emulation } = ctx_client; await Network.enable(); await Page.enable(); - await newCDPInjector({ - network: Network, - page: Page, - browser: Browser, - emulation: Emulation, - }, { - fingerprintOptions: { - browsers: [fingerprintBrowser], + await newCDPInjector( + { + network: Network, + page: Page, + browser: Browser, + emulation: Emulation, + }, + { + fingerprintOptions: { + browsers: [fingerprintBrowser], + }, }, - }); + ); const { frameId } = await Page.navigate({ url: 'http://example.com', diff --git a/test/fingerprint-injector/fingerprint-injector.test.ts b/test/fingerprint-injector/fingerprint-injector.test.ts index 20664f26..0a12119d 100644 --- a/test/fingerprint-injector/fingerprint-injector.test.ts +++ b/test/fingerprint-injector/fingerprint-injector.test.ts @@ -194,19 +194,20 @@ describe('FingerprintInjector', () => { fingerprintWithHeaders, ); - const requestHeaders = new Map>(); + const requestHeaders = new Map< + string, + Record + >(); Network.requestWillBeSent((params) => { - if ( - params.type === 'Document' - ) { + if (params.type === 'Document') { let lowerCase: Record = {}; - for (const header of Object.keys(params.request.headers)) { - lowerCase[header.toLowerCase()] = params.request.headers[header]; + for (const header of Object.keys( + params.request.headers, + )) { + lowerCase[header.toLowerCase()] = + params.request.headers[header]; } - requestHeaders.set( - params.frameId, - lowerCase, - ); + requestHeaders.set(params.frameId, lowerCase); } }); @@ -218,20 +219,31 @@ describe('FingerprintInjector', () => { const stringified = stringifyFunction(fn); const { targetInfo: ti } = await Target.getTargetInfo(); - const { sessionId} = await Target.attachToTarget({ - targetId: ti.targetId, - flatten: true, - }) - ctx_client.send('Runtime.enable', undefined, sessionId) - const ctx = await Runtime.executionContextCreated() - const evaluated = await Runtime.callFunctionOn({ - functionDeclaration: stringified, - executionContextId: ctx.context.id, - arguments: args.map((a) => ({ value: a })), - awaitPromise: true, - - returnByValue: true, - }, sessionId); + const { sessionId } = + await Target.attachToTarget({ + targetId: ti.targetId, + flatten: true, + }); + ctx_client.send( + 'Runtime.enable', + undefined, + sessionId, + ); + const ctx = + await Runtime.executionContextCreated(); + const evaluated = await Runtime.callFunctionOn( + { + functionDeclaration: stringified, + executionContextId: ctx.context.id, + arguments: args.map((a) => ({ + value: a, + })), + awaitPromise: true, + + returnByValue: true, + }, + sessionId, + ); await Runtime.disable(); return evaluated.result.value; }, @@ -243,7 +255,8 @@ describe('FingerprintInjector', () => { return { request: () => ({ - headers: () => requestHeaders.get(frameId), + headers: () => + requestHeaders.get(frameId), }), }; }, @@ -257,7 +270,7 @@ describe('FingerprintInjector', () => { afterAll(async () => { if (client) { - await client.close() + await client.close(); } if (browser) { await browser.close(); @@ -718,33 +731,33 @@ describe('FingerprintInjector', () => { // from https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/util/Function.ts#L30 function stringifyFunction(fn: (...args: never) => unknown): string { - let value = fn.toString(); - try { - new Function(`(${value})`); - } catch (err) { - if ( - (err as Error).message.includes( - `Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive`, - ) - ) { - // The content security policy does not allow Function eval. Let's - // assume the value might be valid as is. - return value; - } - // This means we might have a function shorthand (e.g. `test(){}`). Let's - // try prefixing. - let prefix = 'function '; - if (value.startsWith('async ')) { - prefix = `async ${prefix}`; - value = value.substring('async '.length); - } - value = `${prefix}${value}`; - try { - new Function(`(${value})`); - } catch { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function cannot be serialized!'); - } - } - return value; - } + let value = fn.toString(); + try { + new Function(`(${value})`); + } catch (err) { + if ( + (err as Error).message.includes( + `Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive`, + ) + ) { + // The content security policy does not allow Function eval. Let's + // assume the value might be valid as is. + return value; + } + // This means we might have a function shorthand (e.g. `test(){}`). Let's + // try prefixing. + let prefix = 'function '; + if (value.startsWith('async ')) { + prefix = `async ${prefix}`; + value = value.substring('async '.length); + } + value = `${prefix}${value}`; + try { + new Function(`(${value})`); + } catch { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function cannot be serialized!'); + } + } + return value; +} From 2d397f524d8797b805b65a19cf0903e7bc1b6485 Mon Sep 17 00:00:00 2001 From: EmpiresHQ Date: Sat, 28 Jun 2025 23:31:06 +0300 Subject: [PATCH 8/8] chore: updates related to PR review + some imprvements --- package.json | 1 - packages/fingerprint-injector/package.json | 6 +++++- .../src/fingerprint-injector.ts | 17 ++--------------- .../fingerprint-injector.test.ts | 1 - 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index ea582ea5..ccc6b60b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.23.0", "@types/adm-zip": "^0.5.0", - "@types/chrome-remote-interface": "^0.31.14", "@types/jest": "^29.0.0", "@types/node": "^22.0.0", "@types/node-fetch": "^2.6.1", diff --git a/packages/fingerprint-injector/package.json b/packages/fingerprint-injector/package.json index bdd9f964..b6d4571a 100644 --- a/packages/fingerprint-injector/package.json +++ b/packages/fingerprint-injector/package.json @@ -22,7 +22,8 @@ }, "peerDependencies": { "playwright": "^1.22.2", - "puppeteer": ">= 9.x" + "puppeteer": ">= 9.x", + "@types/chrome-remote-interface": "^0.31.14" }, "peerDependenciesMeta": { "playwright": { @@ -30,6 +31,9 @@ }, "puppeteer": { "optional": true + }, + "@types/chrome-remote-interface" : { + "optional": true } }, "scripts": { diff --git a/packages/fingerprint-injector/src/fingerprint-injector.ts b/packages/fingerprint-injector/src/fingerprint-injector.ts index 1091b1a7..ff2d7253 100644 --- a/packages/fingerprint-injector/src/fingerprint-injector.ts +++ b/packages/fingerprint-injector/src/fingerprint-injector.ts @@ -216,6 +216,8 @@ export class FingerprintInjector { await page.addScriptToEvaluateOnNewDocument({ source: this.getInjectableFingerprintFunction(enhancedFingerprint), + // @ts-ignore unfortunately types are too old, its essential to guarantee script is executed immediately + runImmediately: true, }); } @@ -428,18 +430,3 @@ export async function newInjectedPage( return page; } - -export async function newCDPInjector( - args: AttachFingerprintToCDPparams, - options?: { - fingerprint?: BrowserFingerprintWithHeaders; - fingerprintOptions?: Partial; - }, -): Promise { - const generator = new FingerprintGenerator(); - const fingerprintWithHeaders = - options?.fingerprint ?? - generator.getFingerprint(options?.fingerprintOptions ?? {}); - const injector = new FingerprintInjector(); - await injector.attachFingerprintToCDP(args, fingerprintWithHeaders); -} diff --git a/test/fingerprint-injector/fingerprint-injector.test.ts b/test/fingerprint-injector/fingerprint-injector.test.ts index 0a12119d..b29cbc5a 100644 --- a/test/fingerprint-injector/fingerprint-injector.test.ts +++ b/test/fingerprint-injector/fingerprint-injector.test.ts @@ -73,7 +73,6 @@ const cases = [ options: { args: ['--no-sandbox', '--use-gl=desktop'], channel: 'chrome', - headless: false, debuggingPort: 9222, }, fingerprintGeneratorOptions: {