@webcrawlerapi/stealth 1.0.1 → 1.0.3
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/README.md +10 -9
- package/package.json +1 -1
- package/stealth.d.ts +7 -5
- package/stealth.js +163 -63
package/README.md
CHANGED
|
@@ -21,14 +21,14 @@ The two functions you normally need are:
|
|
|
21
21
|
| `Function.prototype.toString` + anonymous injected globals | native-looking `toString` via a `WeakSet` cloak |
|
|
22
22
|
| `navigator.webdriver` | deleted + faked to `false` |
|
|
23
23
|
| WebDriver / Selenium / Playwright residue (`cdc_*`, `__webdriver_*`, `__selenium_*`, `__pw*`, `__playwright_*`) | removed |
|
|
24
|
-
| `navigator.languages / platform / hardwareConcurrency / deviceMemory
|
|
24
|
+
| `navigator.languages / platform / hardwareConcurrency / deviceMemory` | left as the host reports them; overridable via `opts` |
|
|
25
25
|
| `window.chrome` (`runtime`, `loadTimes`, `csi`, `app`) | faked when missing |
|
|
26
|
-
| `navigator.userAgentData` (`brands`, `getHighEntropyValues`) | faked |
|
|
26
|
+
| `navigator.userAgentData` (`brands`, `getHighEntropyValues`) | faked **only when missing**; a real UA-CH object is left untouched |
|
|
27
27
|
| `navigator.plugins` / `mimeTypes` (when empty) | faked |
|
|
28
28
|
| `navigator.permissions.query` | notifications → `default` |
|
|
29
|
-
| WebGL `UNMASKED_VENDOR_WEBGL` / `UNMASKED_RENDERER_WEBGL` | spoofed
|
|
29
|
+
| WebGL `UNMASKED_VENDOR_WEBGL` / `UNMASKED_RENDERER_WEBGL` | spoofed with an **OS-consistent** GPU only when software GL (SwiftShader / llvmpipe) is detected, or when `webglRenderer`/`webglVendor` is set. A real GPU is reported as-is |
|
|
30
30
|
| `matchMedia` pointer / hover | normalized |
|
|
31
|
-
| `window.outerWidth` / `outerHeight` | corrected
|
|
31
|
+
| `window.outerWidth` / `outerHeight` | corrected only when smaller than the inner size |
|
|
32
32
|
| `Error.prepareStackTrace` | installed if absent |
|
|
33
33
|
| `User-Agent` header + `navigator.userAgent` (`HeadlessChrome` token) | read from `browser.userAgent()` and `HeadlessChrome` → `Chrome` via `page.setUserAgent`; never hardcoded, so the version always matches the real engine. Skip with `fixUserAgent: false`, or pass `userAgent` |
|
|
34
34
|
|
|
@@ -189,11 +189,12 @@ Optional launcher. Requires puppeteer via `opts.puppeteer` or resolved lazily
|
|
|
189
189
|
|
|
190
190
|
| Key | Default | Used by |
|
|
191
191
|
|-----|---------|---------|
|
|
192
|
-
| `webglRenderer` / `renderer` |
|
|
193
|
-
| `webglVendor` / `vendor` |
|
|
194
|
-
| `hardwareConcurrency` |
|
|
195
|
-
| `deviceMemory` |
|
|
196
|
-
| `
|
|
192
|
+
| `webglRenderer` / `renderer` | OS-appropriate GPU (only applied when software GL is detected) | all |
|
|
193
|
+
| `webglVendor` / `vendor` | OS-appropriate vendor (only applied when software GL is detected) | all |
|
|
194
|
+
| `hardwareConcurrency` | host value | all |
|
|
195
|
+
| `deviceMemory` | host value | all |
|
|
196
|
+
| `languages` | host value | all |
|
|
197
|
+
| `platform` | host value | all |
|
|
197
198
|
| `origin` / `permissions` | — | `newPage` |
|
|
198
199
|
| `puppeteer` | lazily resolved | `launch` |
|
|
199
200
|
| `executablePath` | `$CHROME_PATH` | `launch` |
|
package/package.json
CHANGED
package/stealth.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export interface StealthOptions {
|
|
2
|
-
/** WebGL UNMASKED_RENDERER_WEBGL
|
|
2
|
+
/** WebGL UNMASKED_RENDERER_WEBGL to report. Only applied when software GL is detected or this is set; defaults to an OS-appropriate renderer. */
|
|
3
3
|
webglRenderer?: string;
|
|
4
|
-
/** WebGL UNMASKED_VENDOR_WEBGL
|
|
4
|
+
/** WebGL UNMASKED_VENDOR_WEBGL to report. Only applied when software GL is detected or this is set; defaults to an OS-appropriate vendor. */
|
|
5
5
|
webglVendor?: string;
|
|
6
|
-
/** navigator.hardwareConcurrency override. */
|
|
6
|
+
/** navigator.hardwareConcurrency override. Left as the host value when omitted. */
|
|
7
7
|
hardwareConcurrency?: number;
|
|
8
|
-
/** navigator.deviceMemory override. */
|
|
8
|
+
/** navigator.deviceMemory override. Left as the host value when omitted. */
|
|
9
9
|
deviceMemory?: number;
|
|
10
|
-
/** navigator.
|
|
10
|
+
/** navigator.languages / language override. Left as the host value when omitted. */
|
|
11
|
+
languages?: string[];
|
|
12
|
+
/** navigator.platform override. Left as the host value when omitted. */
|
|
11
13
|
platform?: string;
|
|
12
14
|
/** Launch only: injected puppeteer instance. */
|
|
13
15
|
puppeteer?: unknown;
|
package/stealth.js
CHANGED
|
@@ -21,11 +21,7 @@ const UA =
|
|
|
21
21
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
|
|
22
22
|
'(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36';
|
|
23
23
|
|
|
24
|
-
const VERSION = '1.0.
|
|
25
|
-
|
|
26
|
-
const DEFAULT_RENDERER =
|
|
27
|
-
'ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)';
|
|
28
|
-
const DEFAULT_VENDOR = 'Google Inc. (Intel)';
|
|
24
|
+
const VERSION = '1.0.3';
|
|
29
25
|
|
|
30
26
|
/* ------------------------------------------------------------------ *
|
|
31
27
|
* The in-page bootstrap. Runs inside every document before page JS. *
|
|
@@ -34,8 +30,59 @@ const DEFAULT_VENDOR = 'Google Inc. (Intel)';
|
|
|
34
30
|
* ------------------------------------------------------------------ */
|
|
35
31
|
function buildInitScript(cfg) {
|
|
36
32
|
cfg = cfg || {};
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
|
|
34
|
+
// One coherent profile per OS family. The values inside a profile are chosen
|
|
35
|
+
// together so the platform string, UA-CH hints, WebGL strings and geometry
|
|
36
|
+
// all agree with each other *and* with the real Chrome build running the page.
|
|
37
|
+
// (A Windows-only renderer like "Direct3D11" on a Linux platform is a
|
|
38
|
+
// contradiction a fingerprinting script can spot, so we never mix families.)
|
|
39
|
+
const PROFILES = {
|
|
40
|
+
windows: {
|
|
41
|
+
platform: 'Win32',
|
|
42
|
+
uaPlatform: 'Windows',
|
|
43
|
+
platformVersion: '10.0.0',
|
|
44
|
+
architecture: 'x86',
|
|
45
|
+
bitness: '64',
|
|
46
|
+
renderer: 'ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
47
|
+
vendor: 'Google Inc. (Intel)',
|
|
48
|
+
},
|
|
49
|
+
mac: {
|
|
50
|
+
platform: 'MacIntel',
|
|
51
|
+
uaPlatform: 'macOS',
|
|
52
|
+
platformVersion: '15.0.0',
|
|
53
|
+
architecture: 'arm',
|
|
54
|
+
bitness: '64',
|
|
55
|
+
renderer: 'ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)',
|
|
56
|
+
vendor: 'Google Inc. (Apple)',
|
|
57
|
+
},
|
|
58
|
+
linux: {
|
|
59
|
+
platform: 'Linux x86_64',
|
|
60
|
+
uaPlatform: 'Linux',
|
|
61
|
+
platformVersion: '',
|
|
62
|
+
architecture: 'x86',
|
|
63
|
+
bitness: '64',
|
|
64
|
+
renderer: 'ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6 (Core Profile) Mesa 22.0.5)',
|
|
65
|
+
vendor: 'Google Inc. (Intel)',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Derive everything from the real browser so the profile can never drift
|
|
70
|
+
// away from the engine that is actually running.
|
|
71
|
+
const realUA = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
|
|
72
|
+
const chromeVer = (realUA.match(/Chrome\/(\d+)/) || [])[1] || '120';
|
|
73
|
+
const fullVersion = chromeVer + '.0.0.0';
|
|
74
|
+
const osFamily = /Windows/i.test(realUA)
|
|
75
|
+
? 'windows'
|
|
76
|
+
: /Macintosh|Mac OS X/i.test(realUA)
|
|
77
|
+
? 'mac'
|
|
78
|
+
: 'linux';
|
|
79
|
+
const profile = PROFILES[osFamily];
|
|
80
|
+
|
|
81
|
+
// Optional overrides. Anything not supplied keeps the host's real value, so
|
|
82
|
+
// the page never contradicts the machine it is running on.
|
|
83
|
+
const renderer = cfg.renderer || profile.renderer;
|
|
84
|
+
const vendor = cfg.vendor || profile.vendor;
|
|
85
|
+
const languages = cfg.languages;
|
|
39
86
|
|
|
40
87
|
const patched = new WeakSet();
|
|
41
88
|
|
|
@@ -117,17 +164,27 @@ function buildInitScript(cfg) {
|
|
|
117
164
|
};
|
|
118
165
|
}
|
|
119
166
|
|
|
120
|
-
// --- navigator.languages / language ---
|
|
121
|
-
|
|
122
|
-
|
|
167
|
+
// --- navigator.languages / language (only when the caller asked for it) ---
|
|
168
|
+
// Overriding these while leaving the Accept-Language request header alone
|
|
169
|
+
// would make the JS view disagree with the network view.
|
|
170
|
+
if (Array.isArray(languages) && languages.length) {
|
|
171
|
+
try { getter(Navigator.prototype, 'languages', Object.freeze(languages.slice()), 'languages'); } catch (_) {}
|
|
172
|
+
try { getter(Navigator.prototype, 'language', languages[0], 'language'); } catch (_) {}
|
|
173
|
+
}
|
|
123
174
|
|
|
124
|
-
// ---
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
175
|
+
// --- platform / vendor / hardware ---
|
|
176
|
+
// These already match the real engine, so they are left untouched unless an
|
|
177
|
+
// explicit override is supplied. Forcing a value the host does not actually
|
|
178
|
+
// have (e.g. hardwareConcurrency 8 on a 4-core box) is itself a tell.
|
|
179
|
+
if (cfg.platform) {
|
|
180
|
+
try { getter(Navigator.prototype, 'platform', cfg.platform, 'platform'); } catch (_) {}
|
|
181
|
+
}
|
|
182
|
+
if (cfg.hardwareConcurrency) {
|
|
183
|
+
try { getter(Navigator.prototype, 'hardwareConcurrency', cfg.hardwareConcurrency, 'hardwareConcurrency'); } catch (_) {}
|
|
184
|
+
}
|
|
185
|
+
if (cfg.deviceMemory) {
|
|
186
|
+
try { getter(Navigator.prototype, 'deviceMemory', cfg.deviceMemory, 'deviceMemory'); } catch (_) {}
|
|
187
|
+
}
|
|
131
188
|
|
|
132
189
|
// --- plugins / mimeTypes (only when empty) ---
|
|
133
190
|
try {
|
|
@@ -195,55 +252,92 @@ function buildInitScript(cfg) {
|
|
|
195
252
|
} catch (_) {}
|
|
196
253
|
|
|
197
254
|
// --- userAgentData ---
|
|
255
|
+
// When the engine exposes UA-CH it is already consistent with the UA string
|
|
256
|
+
// and the sec-ch-ua headers, so it is left alone. The synthetic object is only
|
|
257
|
+
// built when it is genuinely missing, and then from the same profile.
|
|
198
258
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
259
|
+
if (!navigator.userAgentData) {
|
|
260
|
+
const grease = '24';
|
|
261
|
+
const brands = [
|
|
262
|
+
{ brand: 'Chromium', version: chromeVer },
|
|
263
|
+
{ brand: 'Google Chrome', version: chromeVer },
|
|
264
|
+
{ brand: 'Not?A_Brand', version: grease },
|
|
265
|
+
];
|
|
266
|
+
const fullVersionList = [
|
|
267
|
+
{ brand: 'Chromium', version: fullVersion },
|
|
268
|
+
{ brand: 'Google Chrome', version: fullVersion },
|
|
269
|
+
{ brand: 'Not?A_Brand', version: grease + '.0.0.0' },
|
|
270
|
+
];
|
|
271
|
+
const highEntropy = {
|
|
272
|
+
architecture: profile.architecture,
|
|
273
|
+
bitness: profile.bitness,
|
|
274
|
+
brands,
|
|
275
|
+
fullVersionList,
|
|
276
|
+
mobile: false,
|
|
277
|
+
model: '',
|
|
278
|
+
platform: profile.uaPlatform,
|
|
279
|
+
platformVersion: profile.platformVersion,
|
|
280
|
+
uaFullVersion: fullVersion,
|
|
281
|
+
wow64: false,
|
|
282
|
+
};
|
|
283
|
+
const data = {
|
|
284
|
+
brands, mobile: false, platform: profile.uaPlatform,
|
|
285
|
+
getHighEntropyValues: cloak(function getHighEntropyValues(hints) {
|
|
286
|
+
const out = { brands, mobile: false, platform: profile.uaPlatform };
|
|
287
|
+
(hints || []).forEach((h) => { if (h in highEntropy) out[h] = highEntropy[h]; });
|
|
288
|
+
return Promise.resolve(out);
|
|
289
|
+
}, 'getHighEntropyValues'),
|
|
290
|
+
toJSON: cloak(function toJSON() { return { brands, mobile: false, platform: profile.uaPlatform }; }, 'toJSON'),
|
|
291
|
+
};
|
|
223
292
|
getter(Navigator.prototype, 'userAgentData', data, 'userAgentData');
|
|
224
293
|
}
|
|
225
294
|
} catch (_) {}
|
|
226
295
|
|
|
227
296
|
// --- WebGL vendor / renderer ---
|
|
297
|
+
// Spoofing is applied only when the renderer gives the host away (software
|
|
298
|
+
// rendering such as SwiftShader / llvmpipe / Mesa OffScreen, or a server /
|
|
299
|
+
// datacenter GPU such as an NVIDIA L40S, A100, H100, T4 or vGPU slice) or the
|
|
300
|
+
// caller explicitly asked for it. When the engine reports an ordinary consumer
|
|
301
|
+
// GPU we leave it alone: swapping a real, coherent renderer for a different one
|
|
302
|
+
// only creates contradictions. A datacenter GPU is not a consumer GPU: real
|
|
303
|
+
// cloud-browser hosts (e.g. Kernel) expose one and bot protection (Reuters)
|
|
304
|
+
// blocks on it.
|
|
228
305
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
306
|
+
let softwareGL = false;
|
|
307
|
+
try {
|
|
308
|
+
const probe = document.createElement('canvas');
|
|
309
|
+
const gl = probe.getContext('webgl') || probe.getContext('experimental-webgl');
|
|
310
|
+
if (gl) {
|
|
311
|
+
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
|
312
|
+
const real = dbg
|
|
313
|
+
? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) || '')
|
|
314
|
+
: String(gl.getParameter(gl.RENDERER) || '');
|
|
315
|
+
softwareGL =
|
|
316
|
+
/swiftshader|llvmpipe|software|mesa offscreen|angle \(google/i.test(real) ||
|
|
317
|
+
/\b(?:[AHL]\d{2,3}[A-Z]?|T4|V100|K80|P100|P4|GRID|Tesla|vGPU)\b|-\d+Q\b|NVIDIA (?:A|H|L)\d/i.test(real);
|
|
318
|
+
}
|
|
319
|
+
} catch (_) {}
|
|
320
|
+
|
|
321
|
+
if (softwareGL || cfg.renderer || cfg.vendor) {
|
|
322
|
+
for (const name of ['WebGLRenderingContext', 'WebGL2RenderingContext']) {
|
|
323
|
+
const Ctor = window[name];
|
|
324
|
+
if (!Ctor || !Ctor.prototype) continue;
|
|
325
|
+
const origGetParameter = Ctor.prototype.getParameter;
|
|
326
|
+
const origGetExtension = Ctor.prototype.getExtension;
|
|
327
|
+
const getParameter = cloak(function getParameter(p) {
|
|
328
|
+
if (p === 37445) return vendor;
|
|
329
|
+
if (p === 37446) return renderer;
|
|
330
|
+
return origGetParameter.call(this, p);
|
|
331
|
+
}, 'getParameter');
|
|
332
|
+
const getExtension = cloak(function getExtension(n) {
|
|
333
|
+
if (n === 'WEBGL_debug_renderer_info') {
|
|
334
|
+
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
335
|
+
}
|
|
336
|
+
return origGetExtension.call(this, n);
|
|
337
|
+
}, 'getExtension');
|
|
338
|
+
Object.defineProperty(Ctor.prototype, 'getParameter', { value: getParameter, writable: true, configurable: true });
|
|
339
|
+
Object.defineProperty(Ctor.prototype, 'getExtension', { value: getExtension, writable: true, configurable: true });
|
|
340
|
+
}
|
|
247
341
|
}
|
|
248
342
|
} catch (_) {}
|
|
249
343
|
|
|
@@ -307,11 +401,14 @@ function buildInitScript(cfg) {
|
|
|
307
401
|
} catch (_) {}
|
|
308
402
|
|
|
309
403
|
// --- window geometry drift ---
|
|
404
|
+
// outerWidth/outerHeight must be >= the inner ones. Only correct the cases
|
|
405
|
+
// that are actually broken instead of forcing a value that may contradict the
|
|
406
|
+
// real window manager.
|
|
310
407
|
try {
|
|
311
408
|
const innerH = window.innerHeight || 900;
|
|
312
409
|
const innerW = window.innerWidth || 1366;
|
|
313
|
-
getter(window, '
|
|
314
|
-
getter(window, '
|
|
410
|
+
if (!window.outerWidth || window.outerWidth < innerW) getter(window, 'outerWidth', innerW + 16, 'outerWidth');
|
|
411
|
+
if (!window.outerHeight || window.outerHeight < innerH) getter(window, 'outerHeight', innerH + 88, 'outerHeight');
|
|
315
412
|
} catch (_) {}
|
|
316
413
|
}
|
|
317
414
|
|
|
@@ -321,12 +418,15 @@ function buildInitScript(cfg) {
|
|
|
321
418
|
|
|
322
419
|
function resolveConfig(opts) {
|
|
323
420
|
opts = opts || {};
|
|
421
|
+
// Only carry explicit overrides. Defaults are resolved in-page from the real
|
|
422
|
+
// browser (see buildInitScript) so the profile cannot contradict the engine.
|
|
324
423
|
return {
|
|
325
|
-
renderer: opts.webglRenderer || opts.renderer
|
|
326
|
-
vendor: opts.webglVendor || opts.vendor
|
|
424
|
+
renderer: opts.webglRenderer || opts.renderer,
|
|
425
|
+
vendor: opts.webglVendor || opts.vendor,
|
|
327
426
|
hardwareConcurrency: opts.hardwareConcurrency,
|
|
328
427
|
deviceMemory: opts.deviceMemory,
|
|
329
428
|
platform: opts.platform,
|
|
429
|
+
languages: opts.languages,
|
|
330
430
|
};
|
|
331
431
|
}
|
|
332
432
|
|