@webcrawlerapi/stealth 1.0.0 → 1.0.2
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 +11 -9
- package/package.json +1 -1
- package/stealth.d.ts +12 -5
- package/stealth.js +191 -63
package/README.md
CHANGED
|
@@ -21,15 +21,16 @@ 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
|
+
| `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` |
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
@@ -188,11 +189,12 @@ Optional launcher. Requires puppeteer via `opts.puppeteer` or resolved lazily
|
|
|
188
189
|
|
|
189
190
|
| Key | Default | Used by |
|
|
190
191
|
|-----|---------|---------|
|
|
191
|
-
| `webglRenderer` / `renderer` |
|
|
192
|
-
| `webglVendor` / `vendor` |
|
|
193
|
-
| `hardwareConcurrency` |
|
|
194
|
-
| `deviceMemory` |
|
|
195
|
-
| `
|
|
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 |
|
|
196
198
|
| `origin` / `permissions` | — | `newPage` |
|
|
197
199
|
| `puppeteer` | lazily resolved | `launch` |
|
|
198
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;
|
|
@@ -19,6 +21,10 @@ export interface StealthOptions {
|
|
|
19
21
|
userDataDir?: string;
|
|
20
22
|
/** Launch only: extra chrome args. */
|
|
21
23
|
args?: string[];
|
|
24
|
+
/** Explicit User-Agent for the page. Default: browser UA with "HeadlessChrome" replaced by "Chrome". */
|
|
25
|
+
userAgent?: string;
|
|
26
|
+
/** Set to false to skip the User-Agent fix. Default true. */
|
|
27
|
+
fixUserAgent?: boolean;
|
|
22
28
|
/** newPage only: origin for overridePermissions. */
|
|
23
29
|
origin?: string;
|
|
24
30
|
/** newPage only: permissions to grant. */
|
|
@@ -47,6 +53,7 @@ export declare const UA: string;
|
|
|
47
53
|
export declare function buildInitScript(cfg?: StealthOptions): void;
|
|
48
54
|
export declare function buildSource(opts?: StealthOptions): string;
|
|
49
55
|
export declare function installStealth(page: PageLike, opts?: StealthOptions): Promise<PageLike>;
|
|
56
|
+
export declare function fixUserAgent(page: PageLike, opts?: StealthOptions): Promise<PageLike>;
|
|
50
57
|
export declare function injectNow(page: PageLike, opts?: StealthOptions): Promise<PageLike>;
|
|
51
58
|
export declare function installOopifHook(browser: BrowserLike, opts?: StealthOptions): BrowserLike;
|
|
52
59
|
export declare function newPage(browser: BrowserLike, opts?: StealthOptions): Promise<PageLike>;
|
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.2';
|
|
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,86 @@ 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 software rendering is detected (SwiftShader /
|
|
298
|
+
// llvmpipe / Mesa OffScreen) or the caller explicitly asked for it. When the
|
|
299
|
+
// engine already reports a real GPU we leave it alone: swapping a real, coherent
|
|
300
|
+
// renderer for a different one only creates contradictions.
|
|
228
301
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
302
|
+
let softwareGL = false;
|
|
303
|
+
try {
|
|
304
|
+
const probe = document.createElement('canvas');
|
|
305
|
+
const gl = probe.getContext('webgl') || probe.getContext('experimental-webgl');
|
|
306
|
+
if (gl) {
|
|
307
|
+
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
|
308
|
+
const real = dbg
|
|
309
|
+
? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) || '')
|
|
310
|
+
: String(gl.getParameter(gl.RENDERER) || '');
|
|
311
|
+
softwareGL = /swiftshader|llvmpipe|software|mesa offscreen|angle \(google/i.test(real);
|
|
312
|
+
}
|
|
313
|
+
} catch (_) {}
|
|
314
|
+
|
|
315
|
+
if (softwareGL || cfg.renderer || cfg.vendor) {
|
|
316
|
+
for (const name of ['WebGLRenderingContext', 'WebGL2RenderingContext']) {
|
|
317
|
+
const Ctor = window[name];
|
|
318
|
+
if (!Ctor || !Ctor.prototype) continue;
|
|
319
|
+
const origGetParameter = Ctor.prototype.getParameter;
|
|
320
|
+
const origGetExtension = Ctor.prototype.getExtension;
|
|
321
|
+
const getParameter = cloak(function getParameter(p) {
|
|
322
|
+
if (p === 37445) return vendor;
|
|
323
|
+
if (p === 37446) return renderer;
|
|
324
|
+
return origGetParameter.call(this, p);
|
|
325
|
+
}, 'getParameter');
|
|
326
|
+
const getExtension = cloak(function getExtension(n) {
|
|
327
|
+
if (n === 'WEBGL_debug_renderer_info') {
|
|
328
|
+
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
329
|
+
}
|
|
330
|
+
return origGetExtension.call(this, n);
|
|
331
|
+
}, 'getExtension');
|
|
332
|
+
Object.defineProperty(Ctor.prototype, 'getParameter', { value: getParameter, writable: true, configurable: true });
|
|
333
|
+
Object.defineProperty(Ctor.prototype, 'getExtension', { value: getExtension, writable: true, configurable: true });
|
|
334
|
+
}
|
|
247
335
|
}
|
|
248
336
|
} catch (_) {}
|
|
249
337
|
|
|
@@ -307,11 +395,14 @@ function buildInitScript(cfg) {
|
|
|
307
395
|
} catch (_) {}
|
|
308
396
|
|
|
309
397
|
// --- window geometry drift ---
|
|
398
|
+
// outerWidth/outerHeight must be >= the inner ones. Only correct the cases
|
|
399
|
+
// that are actually broken instead of forcing a value that may contradict the
|
|
400
|
+
// real window manager.
|
|
310
401
|
try {
|
|
311
402
|
const innerH = window.innerHeight || 900;
|
|
312
403
|
const innerW = window.innerWidth || 1366;
|
|
313
|
-
getter(window, '
|
|
314
|
-
getter(window, '
|
|
404
|
+
if (!window.outerWidth || window.outerWidth < innerW) getter(window, 'outerWidth', innerW + 16, 'outerWidth');
|
|
405
|
+
if (!window.outerHeight || window.outerHeight < innerH) getter(window, 'outerHeight', innerH + 88, 'outerHeight');
|
|
315
406
|
} catch (_) {}
|
|
316
407
|
}
|
|
317
408
|
|
|
@@ -321,12 +412,15 @@ function buildInitScript(cfg) {
|
|
|
321
412
|
|
|
322
413
|
function resolveConfig(opts) {
|
|
323
414
|
opts = opts || {};
|
|
415
|
+
// Only carry explicit overrides. Defaults are resolved in-page from the real
|
|
416
|
+
// browser (see buildInitScript) so the profile cannot contradict the engine.
|
|
324
417
|
return {
|
|
325
|
-
renderer: opts.webglRenderer || opts.renderer
|
|
326
|
-
vendor: opts.webglVendor || opts.vendor
|
|
418
|
+
renderer: opts.webglRenderer || opts.renderer,
|
|
419
|
+
vendor: opts.webglVendor || opts.vendor,
|
|
327
420
|
hardwareConcurrency: opts.hardwareConcurrency,
|
|
328
421
|
deviceMemory: opts.deviceMemory,
|
|
329
422
|
platform: opts.platform,
|
|
423
|
+
languages: opts.languages,
|
|
330
424
|
};
|
|
331
425
|
}
|
|
332
426
|
|
|
@@ -352,10 +446,43 @@ async function installStealth(page, opts) {
|
|
|
352
446
|
opts = opts || {};
|
|
353
447
|
const cfg = resolveConfig(opts);
|
|
354
448
|
await page.evaluateOnNewDocument(buildInitScript, cfg);
|
|
449
|
+
await fixUserAgent(page, opts);
|
|
355
450
|
try { page[INSTALLED] = true; } catch (_) {}
|
|
356
451
|
return page;
|
|
357
452
|
}
|
|
358
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Headless Chrome announces itself as "HeadlessChrome/<ver>" in both the
|
|
456
|
+
* `User-Agent` request header and navigator.userAgent. The header is set by the
|
|
457
|
+
* network stack, so the in-page patches can't touch it; it has to go through
|
|
458
|
+
* the CDP UA override (page.setUserAgent).
|
|
459
|
+
*
|
|
460
|
+
* The UA is read from the running browser (browser.userAgent()) instead of
|
|
461
|
+
* being hardcoded, so the Chrome version always matches the real engine and
|
|
462
|
+
* never goes stale when the bundled Chrome is upgraded.
|
|
463
|
+
*
|
|
464
|
+
* opts.userAgent explicit UA, used as is
|
|
465
|
+
* opts.fixUserAgent set to false to skip
|
|
466
|
+
*
|
|
467
|
+
* No-op when the browser UA has no "HeadlessChrome" token (headful, connect()).
|
|
468
|
+
*/
|
|
469
|
+
async function fixUserAgent(page, opts) {
|
|
470
|
+
opts = opts || {};
|
|
471
|
+
if (opts.fixUserAgent === false || typeof page.setUserAgent !== 'function') return page;
|
|
472
|
+
try {
|
|
473
|
+
let ua = opts.userAgent;
|
|
474
|
+
if (!ua) {
|
|
475
|
+
const browser = typeof page.browser === 'function' ? page.browser() : null;
|
|
476
|
+
if (!browser || typeof browser.userAgent !== 'function') return page;
|
|
477
|
+
const current = await browser.userAgent();
|
|
478
|
+
if (!/HeadlessChrome/.test(current)) return page;
|
|
479
|
+
ua = current.replace('HeadlessChrome', 'Chrome');
|
|
480
|
+
}
|
|
481
|
+
await page.setUserAgent(ua);
|
|
482
|
+
} catch (_) {}
|
|
483
|
+
return page;
|
|
484
|
+
}
|
|
485
|
+
|
|
359
486
|
/**
|
|
360
487
|
* Also inject the bootstrap into documents that are ALREADY loaded (useful when
|
|
361
488
|
* you attach with connect() to a page that is already on the target URL).
|
|
@@ -468,6 +595,7 @@ module.exports = {
|
|
|
468
595
|
buildInitScript,
|
|
469
596
|
buildSource,
|
|
470
597
|
installStealth,
|
|
598
|
+
fixUserAgent,
|
|
471
599
|
injectNow,
|
|
472
600
|
installOopifHook,
|
|
473
601
|
newPage,
|