html-renderer-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTEGRATION.md +319 -0
- package/QUICKSTART.md +270 -0
- package/package.json +19 -0
- package/readme.md +317 -0
- package/src/config/setup.sh +60 -0
- package/src/durable-objects/browser-durable-object.ts +534 -0
- package/src/scrapers/scraper-captcha.ts +479 -0
- package/src/scrapers/scraper-cloudflare.ts +93 -0
- package/src/scrapers/scraper-login.ts +271 -0
- package/src/scrapers/scraper-openapi.ts +363 -0
- package/src/scrapers/scraper-stealth.ts +521 -0
- package/src/utils/scraper-utils.ts +219 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file scraper-stealth.ts
|
|
3
|
+
* @description Browser stealth evasion techniques injected into every new
|
|
4
|
+
* Puppeteer page before navigation. Each evasion patches a specific browser
|
|
5
|
+
* API that bot-detection services inspect (navigator.webdriver, plugins,
|
|
6
|
+
* WebGL, canvas fingerprinting, etc.) so the headless Chrome instance
|
|
7
|
+
* presents as an ordinary desktop browser.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Page } from "@cloudflare/puppeteer";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Applies 24 stealth evasion patches to `page` via `evaluateOnNewDocument`
|
|
14
|
+
* so they take effect before any page script runs.
|
|
15
|
+
* Must be called before the first `page.goto()`.
|
|
16
|
+
*
|
|
17
|
+
* @param page - Puppeteer {@link Page} instance to patch.
|
|
18
|
+
* @returns Resolves once all patches have been registered.
|
|
19
|
+
*/
|
|
20
|
+
export async function applyStealthEvasions(page: Page): Promise<void> {
|
|
21
|
+
// Evasion 1: Override navigator.webdriver - CRITICAL for reCAPTCHA
|
|
22
|
+
await page.evaluateOnNewDocument(() => {
|
|
23
|
+
Object.defineProperty(navigator, "webdriver", {
|
|
24
|
+
get: () => false,
|
|
25
|
+
configurable: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (navigator.webdriver) {
|
|
29
|
+
delete (Object.getPrototypeOf(navigator) as Record<string, unknown>)
|
|
30
|
+
.webdriver;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const originalQuery = Object.getOwnPropertyDescriptor(
|
|
34
|
+
Navigator.prototype,
|
|
35
|
+
"webdriver",
|
|
36
|
+
);
|
|
37
|
+
if (originalQuery) {
|
|
38
|
+
Object.defineProperty(Navigator.prototype, "webdriver", {
|
|
39
|
+
get: () => false,
|
|
40
|
+
configurable: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Evasion 2: Remove automation-related properties from window
|
|
46
|
+
await page.evaluateOnNewDocument(() => {
|
|
47
|
+
const props = Object.keys(window).filter(
|
|
48
|
+
(k) => k.startsWith("cdc_") || k.startsWith("$cdc_"),
|
|
49
|
+
);
|
|
50
|
+
props.forEach((prop) => {
|
|
51
|
+
delete (window as unknown as Record<string, unknown>)[prop];
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const w = window as unknown as Record<string, unknown>;
|
|
55
|
+
const d = document as unknown as Record<string, unknown>;
|
|
56
|
+
delete w.callPhantom;
|
|
57
|
+
delete w._phantom;
|
|
58
|
+
delete w.__nightmare;
|
|
59
|
+
delete w.domAutomation;
|
|
60
|
+
delete w.domAutomationController;
|
|
61
|
+
delete w._Selenium_IDE_Recorder;
|
|
62
|
+
delete w._selenium;
|
|
63
|
+
delete w.__webdriver_script_fn;
|
|
64
|
+
delete w.__driver_evaluate;
|
|
65
|
+
delete w.__webdriver_evaluate;
|
|
66
|
+
delete w.__selenium_evaluate;
|
|
67
|
+
delete w.__fxdriver_evaluate;
|
|
68
|
+
delete w.__driver_unwrapped;
|
|
69
|
+
delete w.__webdriver_unwrapped;
|
|
70
|
+
delete w.__selenium_unwrapped;
|
|
71
|
+
delete w.__fxdriver_unwrapped;
|
|
72
|
+
delete w.__webdriver_script_func;
|
|
73
|
+
delete w.__webdriver_script_function;
|
|
74
|
+
delete d.__webdriver_evaluate;
|
|
75
|
+
delete d.__selenium_evaluate;
|
|
76
|
+
delete d.__webdriver_script_function;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Evasion 3: Mock chrome runtime to appear as a real Chrome install
|
|
80
|
+
await page.evaluateOnNewDocument(() => {
|
|
81
|
+
(window as Record<string, unknown>).chrome = {
|
|
82
|
+
runtime: {
|
|
83
|
+
onConnect: {
|
|
84
|
+
addListener: function () {},
|
|
85
|
+
removeListener: function () {},
|
|
86
|
+
},
|
|
87
|
+
onMessage: {
|
|
88
|
+
addListener: function () {},
|
|
89
|
+
removeListener: function () {},
|
|
90
|
+
},
|
|
91
|
+
connect: function () {
|
|
92
|
+
return {
|
|
93
|
+
onMessage: { addListener: function () {} },
|
|
94
|
+
postMessage: function () {},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
sendMessage: function () {},
|
|
98
|
+
id: undefined,
|
|
99
|
+
getPlatformInfo: function (cb: (info: object) => void) {
|
|
100
|
+
cb({ os: "win", arch: "x86-64", nacl_arch: "x86-64" });
|
|
101
|
+
},
|
|
102
|
+
getManifest: function () {
|
|
103
|
+
return {};
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
loadTimes: function () {
|
|
107
|
+
return {
|
|
108
|
+
requestTime: Date.now() * 0.001,
|
|
109
|
+
startLoadTime: Date.now() * 0.001,
|
|
110
|
+
commitLoadTime: Date.now() * 0.001,
|
|
111
|
+
finishDocumentLoadTime: Date.now() * 0.001,
|
|
112
|
+
finishLoadTime: Date.now() * 0.001,
|
|
113
|
+
firstPaintTime: Date.now() * 0.001,
|
|
114
|
+
firstPaintAfterLoadTime: 0,
|
|
115
|
+
navigationType: "Other",
|
|
116
|
+
wasFetchedViaSpdy: false,
|
|
117
|
+
wasNpnNegotiated: false,
|
|
118
|
+
npnNegotiatedProtocol: "unknown",
|
|
119
|
+
wasAlternateProtocolAvailable: false,
|
|
120
|
+
connectionInfo: "http/1.1",
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
csi: function () {
|
|
124
|
+
return {
|
|
125
|
+
onloadT: Date.now(),
|
|
126
|
+
pageT: Date.now() - 1000,
|
|
127
|
+
startE: Date.now() - 2000,
|
|
128
|
+
tran: 15,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
app: {
|
|
132
|
+
isInstalled: false,
|
|
133
|
+
InstallState: {
|
|
134
|
+
DISABLED: "disabled",
|
|
135
|
+
INSTALLED: "installed",
|
|
136
|
+
NOT_INSTALLED: "not_installed",
|
|
137
|
+
},
|
|
138
|
+
RunningState: {
|
|
139
|
+
CANNOT_RUN: "cannot_run",
|
|
140
|
+
READY_TO_RUN: "ready_to_run",
|
|
141
|
+
RUNNING: "running",
|
|
142
|
+
},
|
|
143
|
+
getDetails: function () {
|
|
144
|
+
return null;
|
|
145
|
+
},
|
|
146
|
+
getIsInstalled: function () {
|
|
147
|
+
return false;
|
|
148
|
+
},
|
|
149
|
+
runningState: function () {
|
|
150
|
+
return "cannot_run";
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Evasion 4: Override navigator.plugins to match a real Chrome install
|
|
157
|
+
await page.evaluateOnNewDocument(() => {
|
|
158
|
+
const makePlugin = (
|
|
159
|
+
name: string,
|
|
160
|
+
description: string,
|
|
161
|
+
filename: string,
|
|
162
|
+
mimeTypes: Array<{ type: string; suffixes: string; description: string }>,
|
|
163
|
+
) => {
|
|
164
|
+
const plugin = Object.create(Plugin.prototype);
|
|
165
|
+
Object.defineProperties(plugin, {
|
|
166
|
+
name: { value: name, enumerable: true },
|
|
167
|
+
description: { value: description, enumerable: true },
|
|
168
|
+
filename: { value: filename, enumerable: true },
|
|
169
|
+
length: { value: mimeTypes.length, enumerable: true },
|
|
170
|
+
});
|
|
171
|
+
mimeTypes.forEach((mt, i) => {
|
|
172
|
+
const mimeType = Object.create(MimeType.prototype);
|
|
173
|
+
Object.defineProperties(mimeType, {
|
|
174
|
+
type: { value: mt.type, enumerable: true },
|
|
175
|
+
suffixes: { value: mt.suffixes, enumerable: true },
|
|
176
|
+
description: { value: mt.description, enumerable: true },
|
|
177
|
+
enabledPlugin: { value: plugin, enumerable: true },
|
|
178
|
+
});
|
|
179
|
+
Object.defineProperty(plugin, i, { value: mimeType, enumerable: true });
|
|
180
|
+
Object.defineProperty(plugin, mt.type, {
|
|
181
|
+
value: mimeType,
|
|
182
|
+
enumerable: false,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
return plugin;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const plugins = [
|
|
189
|
+
makePlugin(
|
|
190
|
+
"Chrome PDF Plugin",
|
|
191
|
+
"Portable Document Format",
|
|
192
|
+
"internal-pdf-viewer",
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
type: "application/x-google-chrome-pdf",
|
|
196
|
+
suffixes: "pdf",
|
|
197
|
+
description: "Portable Document Format",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
),
|
|
201
|
+
makePlugin("Chrome PDF Viewer", "", "mhjfbmdgcfjbbpaeojofohoefgiehjai", [
|
|
202
|
+
{ type: "application/pdf", suffixes: "pdf", description: "" },
|
|
203
|
+
]),
|
|
204
|
+
makePlugin("Native Client", "", "internal-nacl-plugin", [
|
|
205
|
+
{
|
|
206
|
+
type: "application/x-nacl",
|
|
207
|
+
suffixes: "",
|
|
208
|
+
description: "Native Client Executable",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
type: "application/x-pnacl",
|
|
212
|
+
suffixes: "",
|
|
213
|
+
description: "Portable Native Client Executable",
|
|
214
|
+
},
|
|
215
|
+
]),
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const pluginArray = Object.create(PluginArray.prototype);
|
|
219
|
+
plugins.forEach((plugin, i) => {
|
|
220
|
+
Object.defineProperty(pluginArray, i, {
|
|
221
|
+
value: plugin,
|
|
222
|
+
enumerable: true,
|
|
223
|
+
});
|
|
224
|
+
Object.defineProperty(pluginArray, plugin.name, {
|
|
225
|
+
value: plugin,
|
|
226
|
+
enumerable: false,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
Object.defineProperty(pluginArray, "length", {
|
|
230
|
+
value: plugins.length,
|
|
231
|
+
enumerable: true,
|
|
232
|
+
});
|
|
233
|
+
Object.defineProperty(pluginArray, "item", {
|
|
234
|
+
value: function (i: number) {
|
|
235
|
+
return (pluginArray as Record<number, Plugin>)[i] ?? null;
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
Object.defineProperty(pluginArray, "namedItem", {
|
|
239
|
+
value: function (name: string) {
|
|
240
|
+
return (pluginArray as Record<string, Plugin>)[name] ?? null;
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
Object.defineProperty(pluginArray, "refresh", { value: function () {} });
|
|
244
|
+
|
|
245
|
+
Object.defineProperty(navigator, "plugins", {
|
|
246
|
+
get: () => pluginArray,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Evasion 5: Override navigator.languages
|
|
251
|
+
await page.evaluateOnNewDocument(() => {
|
|
252
|
+
Object.defineProperty(navigator, "languages", {
|
|
253
|
+
get: () => ["en-US", "en"],
|
|
254
|
+
});
|
|
255
|
+
Object.defineProperty(navigator, "language", {
|
|
256
|
+
get: () => "en-US",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Evasion 6: Override permissions API - important for reCAPTCHA
|
|
261
|
+
await page.evaluateOnNewDocument(() => {
|
|
262
|
+
const originalQuery = window.navigator.permissions.query.bind(
|
|
263
|
+
window.navigator.permissions,
|
|
264
|
+
);
|
|
265
|
+
window.navigator.permissions.query = (parameters: PermissionDescriptor) => {
|
|
266
|
+
if (parameters.name === "notifications") {
|
|
267
|
+
return Promise.resolve({
|
|
268
|
+
state: Notification.permission as PermissionState,
|
|
269
|
+
onchange: null,
|
|
270
|
+
} as PermissionStatus);
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
(parameters.name as string) === "midi" ||
|
|
274
|
+
(parameters.name as string) === "camera" ||
|
|
275
|
+
(parameters.name as string) === "microphone"
|
|
276
|
+
) {
|
|
277
|
+
return Promise.resolve({
|
|
278
|
+
state: "prompt" as PermissionState,
|
|
279
|
+
onchange: null,
|
|
280
|
+
} as PermissionStatus);
|
|
281
|
+
}
|
|
282
|
+
return originalQuery(parameters);
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Evasion 7: Override navigator.hardwareConcurrency
|
|
287
|
+
await page.evaluateOnNewDocument(() => {
|
|
288
|
+
Object.defineProperty(navigator, "hardwareConcurrency", { get: () => 8 });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Evasion 8: Override navigator.deviceMemory
|
|
292
|
+
await page.evaluateOnNewDocument(() => {
|
|
293
|
+
Object.defineProperty(navigator, "deviceMemory", { get: () => 8 });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Evasion 9: Fix iframe contentWindow - critical for reCAPTCHA
|
|
297
|
+
await page.evaluateOnNewDocument(() => {
|
|
298
|
+
const originalAppendChild = Element.prototype.appendChild;
|
|
299
|
+
Element.prototype.appendChild = function <T extends Node>(child: T): T {
|
|
300
|
+
return originalAppendChild.call(this, child) as T;
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Evasion 10: Mock WebGL vendor and renderer with realistic GPU values
|
|
305
|
+
await page.evaluateOnNewDocument(() => {
|
|
306
|
+
const getParameterProxyHandler: ProxyHandler<
|
|
307
|
+
WebGLRenderingContext["getParameter"]
|
|
308
|
+
> = {
|
|
309
|
+
apply: function (target, ctx, args) {
|
|
310
|
+
const param = args[0] as number;
|
|
311
|
+
const result = Reflect.apply(
|
|
312
|
+
target as (...a: unknown[]) => unknown,
|
|
313
|
+
ctx,
|
|
314
|
+
args,
|
|
315
|
+
);
|
|
316
|
+
if (param === 37445) return "Google Inc. (NVIDIA)";
|
|
317
|
+
if (param === 37446)
|
|
318
|
+
return "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Direct3D11 vs_5_0 ps_5_0, D3D11)";
|
|
319
|
+
if (param === 7938) return "WebGL 1.0 (OpenGL ES 2.0 Chromium)";
|
|
320
|
+
if (param === 35724)
|
|
321
|
+
return "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)";
|
|
322
|
+
return result;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
WebGLRenderingContext.prototype.getParameter = new Proxy(
|
|
328
|
+
WebGLRenderingContext.prototype.getParameter,
|
|
329
|
+
getParameterProxyHandler as ProxyHandler<
|
|
330
|
+
typeof WebGLRenderingContext.prototype.getParameter
|
|
331
|
+
>,
|
|
332
|
+
);
|
|
333
|
+
} catch (_e) {}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
WebGL2RenderingContext.prototype.getParameter = new Proxy(
|
|
337
|
+
WebGL2RenderingContext.prototype.getParameter,
|
|
338
|
+
getParameterProxyHandler as ProxyHandler<
|
|
339
|
+
typeof WebGL2RenderingContext.prototype.getParameter
|
|
340
|
+
>,
|
|
341
|
+
);
|
|
342
|
+
} catch (_e) {}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Evasion 11: Override navigator.platform
|
|
346
|
+
await page.evaluateOnNewDocument(() => {
|
|
347
|
+
Object.defineProperty(navigator, "platform", { get: () => "Win32" });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Evasion 12: Override navigator.maxTouchPoints (desktop = 0)
|
|
351
|
+
await page.evaluateOnNewDocument(() => {
|
|
352
|
+
Object.defineProperty(navigator, "maxTouchPoints", { get: () => 0 });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Evasion 13: Override navigator.connection with realistic 4G values
|
|
356
|
+
await page.evaluateOnNewDocument(() => {
|
|
357
|
+
Object.defineProperty(navigator, "connection", {
|
|
358
|
+
get: () => ({
|
|
359
|
+
effectiveType: "4g",
|
|
360
|
+
rtt: 50,
|
|
361
|
+
downlink: 10,
|
|
362
|
+
saveData: false,
|
|
363
|
+
onchange: null,
|
|
364
|
+
addEventListener: function () {},
|
|
365
|
+
removeEventListener: function () {},
|
|
366
|
+
}),
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Evasion 14: Override navigator.vendor
|
|
371
|
+
await page.evaluateOnNewDocument(() => {
|
|
372
|
+
Object.defineProperty(navigator, "vendor", { get: () => "Google Inc." });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Evasion 15: Mock battery API
|
|
376
|
+
await page.evaluateOnNewDocument(() => {
|
|
377
|
+
(navigator as Navigator & { getBattery?: () => Promise<unknown> }).getBattery =
|
|
378
|
+
() =>
|
|
379
|
+
Promise.resolve({
|
|
380
|
+
charging: true,
|
|
381
|
+
chargingTime: 0,
|
|
382
|
+
dischargingTime: Infinity,
|
|
383
|
+
level: 0.95 + Math.random() * 0.05,
|
|
384
|
+
addEventListener: function () {},
|
|
385
|
+
removeEventListener: function () {},
|
|
386
|
+
onchargingchange: null,
|
|
387
|
+
onchargingtimechange: null,
|
|
388
|
+
ondischargingtimechange: null,
|
|
389
|
+
onlevelchange: null,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Evasion 16: Fix prototype chain and toStringTag
|
|
394
|
+
await page.evaluateOnNewDocument(() => {
|
|
395
|
+
try {
|
|
396
|
+
Object.defineProperty(Navigator.prototype, Symbol.toStringTag, {
|
|
397
|
+
get: () => "Navigator",
|
|
398
|
+
configurable: true,
|
|
399
|
+
});
|
|
400
|
+
} catch (_e) {}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Evasion 17: Override Notification.permission
|
|
404
|
+
await page.evaluateOnNewDocument(() => {
|
|
405
|
+
Object.defineProperty(Notification, "permission", {
|
|
406
|
+
get: () => "default",
|
|
407
|
+
configurable: true,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Evasion 18: Align outerWidth/outerHeight with innerWidth/innerHeight
|
|
412
|
+
await page.evaluateOnNewDocument(() => {
|
|
413
|
+
Object.defineProperty(window, "outerWidth", {
|
|
414
|
+
get: () => window.innerWidth,
|
|
415
|
+
configurable: true,
|
|
416
|
+
});
|
|
417
|
+
Object.defineProperty(window, "outerHeight", {
|
|
418
|
+
get: () => window.innerHeight + 85,
|
|
419
|
+
configurable: true,
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Evasion 19: Fix Function.prototype.toString to avoid proxy detection
|
|
424
|
+
await page.evaluateOnNewDocument(() => {
|
|
425
|
+
const originalToString = Function.prototype.toString;
|
|
426
|
+
Function.prototype.toString = function (this: Function) {
|
|
427
|
+
if (this === Function.prototype.toString) {
|
|
428
|
+
return "function toString() { [native code] }";
|
|
429
|
+
}
|
|
430
|
+
return originalToString.call(this);
|
|
431
|
+
};
|
|
432
|
+
Object.defineProperty(Function.prototype.toString, "name", {
|
|
433
|
+
value: "toString",
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Evasion 20: Add subtle noise to canvas toDataURL for fingerprint resistance
|
|
438
|
+
await page.evaluateOnNewDocument(() => {
|
|
439
|
+
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
440
|
+
HTMLCanvasElement.prototype.toDataURL = function (
|
|
441
|
+
this: HTMLCanvasElement,
|
|
442
|
+
type?: string,
|
|
443
|
+
...args: unknown[]
|
|
444
|
+
) {
|
|
445
|
+
if (type === "image/png" && this.width === 220 && this.height === 30) {
|
|
446
|
+
const ctx = this.getContext("2d");
|
|
447
|
+
if (ctx) {
|
|
448
|
+
const imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
449
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
450
|
+
imageData.data[i] += Math.floor(Math.random() * 2);
|
|
451
|
+
imageData.data[i + 1] += Math.floor(Math.random() * 2);
|
|
452
|
+
imageData.data[i + 2] += Math.floor(Math.random() * 2);
|
|
453
|
+
}
|
|
454
|
+
ctx.putImageData(imageData, 0, 0);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return originalToDataURL.apply(this, [type, ...args] as [
|
|
458
|
+
string?,
|
|
459
|
+
...unknown[]
|
|
460
|
+
]);
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Evasion 21: Add noise to AudioContext analyser for fingerprint resistance
|
|
465
|
+
await page.evaluateOnNewDocument(() => {
|
|
466
|
+
const w = window as unknown as Record<string, unknown>;
|
|
467
|
+
const AudioContextClass = (w.AudioContext ?? w.webkitAudioContext) as unknown;
|
|
468
|
+
if (AudioContextClass && typeof AudioContextClass === 'object') {
|
|
469
|
+
const proto = (AudioContextClass as Record<string, unknown>).prototype as Record<string, unknown>;
|
|
470
|
+
const originalCreateAnalyser = proto.createAnalyser as (this: unknown) => AnalyserNode;
|
|
471
|
+
if (originalCreateAnalyser) {
|
|
472
|
+
proto.createAnalyser = function (this: unknown) {
|
|
473
|
+
const analyser = originalCreateAnalyser.call(this) as AnalyserNode;
|
|
474
|
+
const originalGetFloatFrequencyData =
|
|
475
|
+
analyser.getFloatFrequencyData.bind(analyser);
|
|
476
|
+
analyser.getFloatFrequencyData = function (array: Float32Array) {
|
|
477
|
+
originalGetFloatFrequencyData(array);
|
|
478
|
+
for (let i = 0; i < array.length; i++) {
|
|
479
|
+
array[i] += Math.random() * 0.0001;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
return analyser;
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Evasion 22: Stub getClientRects (noise hook reserved for future use)
|
|
489
|
+
await page.evaluateOnNewDocument(() => {
|
|
490
|
+
const originalGetClientRects = Element.prototype.getClientRects;
|
|
491
|
+
Element.prototype.getClientRects = function (this: Element) {
|
|
492
|
+
return originalGetClientRects.call(this);
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Evasion 23: Remove navigator.brave to avoid Brave-browser fingerprinting
|
|
497
|
+
await page.evaluateOnNewDocument(() => {
|
|
498
|
+
Object.defineProperty(navigator, "brave", {
|
|
499
|
+
get: () => undefined,
|
|
500
|
+
configurable: true,
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Evasion 24: Lock screen dimensions to a standard 1920×1080 desktop
|
|
505
|
+
await page.evaluateOnNewDocument(() => {
|
|
506
|
+
const screenProps: Record<string, number> = {
|
|
507
|
+
availWidth: 1920,
|
|
508
|
+
availHeight: 1040,
|
|
509
|
+
width: 1920,
|
|
510
|
+
height: 1080,
|
|
511
|
+
colorDepth: 24,
|
|
512
|
+
pixelDepth: 24,
|
|
513
|
+
};
|
|
514
|
+
for (const [key, value] of Object.entries(screenProps)) {
|
|
515
|
+
Object.defineProperty(screen, key, {
|
|
516
|
+
get: () => value,
|
|
517
|
+
configurable: true,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|