@webcrawlerapi/stealth 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +234 -2
- package/package.json +38 -4
- package/stealth.d.ts +55 -0
- package/stealth.js +477 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WebCrawlerAPI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,235 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @webcrawlerapi/stealth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A **zero-dependency** stealth layer for Puppeteer (or any CDP-driven browser).
|
|
4
|
+
It installs anti-detection patches into the page **before any page script runs**,
|
|
5
|
+
so fingerprints are already clean by the time the target's own JS looks at them.
|
|
6
|
+
|
|
7
|
+
Works with **both** ways of driving Chrome:
|
|
8
|
+
|
|
9
|
+
- `puppeteer.launch()` — you start the browser
|
|
10
|
+
- `puppeteer.connect()` — you attach to a browser that is already running
|
|
11
|
+
|
|
12
|
+
The two functions you normally need are:
|
|
13
|
+
|
|
14
|
+
- **`installStealth(page, opts)`** — patches a page
|
|
15
|
+
- **`installOopifHook(browser, opts)`** — patches sandboxed / out-of-process iframes
|
|
16
|
+
|
|
17
|
+
## What it patches
|
|
18
|
+
|
|
19
|
+
| Vector | Patch |
|
|
20
|
+
|--------|-------|
|
|
21
|
+
| `Function.prototype.toString` + anonymous injected globals | native-looking `toString` via a `WeakSet` cloak |
|
|
22
|
+
| `navigator.webdriver` | deleted + faked to `false` |
|
|
23
|
+
| WebDriver / Selenium / Playwright residue (`cdc_*`, `__webdriver_*`, `__selenium_*`, `__pw*`, `__playwright_*`) | removed |
|
|
24
|
+
| `navigator.languages / platform / hardwareConcurrency / deviceMemory / vendor / onLine` | normalized |
|
|
25
|
+
| `window.chrome` (`runtime`, `loadTimes`, `csi`, `app`) | faked when missing |
|
|
26
|
+
| `navigator.userAgentData` (`brands`, `getHighEntropyValues`) | faked |
|
|
27
|
+
| `navigator.plugins` / `mimeTypes` (when empty) | faked |
|
|
28
|
+
| `navigator.permissions.query` | notifications → `default` |
|
|
29
|
+
| WebGL `UNMASKED_VENDOR_WEBGL` / `UNMASKED_RENDERER_WEBGL` | spoofed (hides SwiftShader / software GL) |
|
|
30
|
+
| `matchMedia` pointer / hover | normalized |
|
|
31
|
+
| `window.outerWidth` / `outerHeight` | corrected to inner + chrome |
|
|
32
|
+
| `Error.prepareStackTrace` | installed if absent |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install (without npm)
|
|
37
|
+
|
|
38
|
+
There is nothing to fetch from the npm registry — the whole module is a single
|
|
39
|
+
dependency-free file. Use whichever fits:
|
|
40
|
+
|
|
41
|
+
### Option 1 — clone the repo
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
git clone https://github.com/WebCrawlerAPI/stealth.git
|
|
45
|
+
cp stealth/stealth.js /your/project/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Option 2 — single-file download
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
# install.sh ships with the repo
|
|
52
|
+
GH_TOKEN=ghp_xxx sh install.sh # writes ./stealth.js
|
|
53
|
+
|
|
54
|
+
# or, by hand (the repo is private → token required)
|
|
55
|
+
curl -fsSL \
|
|
56
|
+
-H "Authorization: Bearer $GH_TOKEN" \
|
|
57
|
+
-H "Accept: application/vnd.github.raw" \
|
|
58
|
+
"https://api.github.com/repos/WebCrawlerAPI/stealth/contents/stealth.js?ref=main" \
|
|
59
|
+
-o stealth.js
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const stealth = require('./stealth.js');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
No `npm install`, no transitive dependencies. (A `package.json` is included for
|
|
69
|
+
projects that *do* use npm, but it declares **no dependencies**.)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const stealth = require('./stealth.js');
|
|
77
|
+
|
|
78
|
+
const browser = await /* your browser */;
|
|
79
|
+
const page = await browser.newPage();
|
|
80
|
+
|
|
81
|
+
await stealth.installStealth(page, {}); // 1. register patches (BEFORE goto)
|
|
82
|
+
stealth.installOopifHook(browser, {}); // 2. patch sandboxed iframes too
|
|
83
|
+
|
|
84
|
+
await page.goto(url); // 3. navigate
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> Order matters: `installStealth` uses `page.evaluateOnNewDocument`, so it must
|
|
88
|
+
> run **before** the navigation you want patched.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Usage with `launch()`
|
|
93
|
+
|
|
94
|
+
If you start the browser yourself, use the bundled helper (it needs puppeteer —
|
|
95
|
+
pass it in or have it resolvable):
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
const puppeteer = require('puppeteer-core');
|
|
99
|
+
const stealth = require('./stealth.js');
|
|
100
|
+
|
|
101
|
+
const browser = await stealth.launch({
|
|
102
|
+
puppeteer, // or it is required lazily
|
|
103
|
+
executablePath: '/usr/bin/google-chrome',
|
|
104
|
+
headless: false,
|
|
105
|
+
userDataDir: '/tmp/my-profile', // use a FRESH profile
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const page = await stealth.newPage(browser, { origin: 'https://example.com' });
|
|
109
|
+
await page.goto('https://example.com');
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`newPage()` is shorthand for `browser.newPage()` + `installStealth()` +
|
|
113
|
+
`installOopifHook()`.
|
|
114
|
+
|
|
115
|
+
## Usage with `connect()`
|
|
116
|
+
|
|
117
|
+
This is the case when you attach to an already-running browser (e.g. one started
|
|
118
|
+
with `--remote-debugging-port=9222`, or a browser on a server / remote desktop):
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
const puppeteer = require('puppeteer-core');
|
|
122
|
+
const stealth = require('./stealth.js');
|
|
123
|
+
|
|
124
|
+
const browser = await puppeteer.connect({
|
|
125
|
+
browserURL: 'http://127.0.0.1:9222', // or browserWSEndpoint: 'ws://...'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const page = await browser.newPage();
|
|
129
|
+
await stealth.installStealth(page, {}); // MUST run before goto()
|
|
130
|
+
stealth.installOopifHook(browser, {});
|
|
131
|
+
|
|
132
|
+
await page.goto('https://example.com');
|
|
133
|
+
// ...do work...
|
|
134
|
+
await page.close();
|
|
135
|
+
browser.disconnect(); // do NOT close() a browser you didn't start
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### If the page is already loaded
|
|
139
|
+
|
|
140
|
+
`installStealth()` only affects **future** documents. When you attach to a page
|
|
141
|
+
that is already on the target URL, also patch the current document and reload:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
await stealth.installStealth(page, {}); // future navigations
|
|
145
|
+
await stealth.injectNow(page); // current document
|
|
146
|
+
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## API
|
|
152
|
+
|
|
153
|
+
### `await installStealth(page, opts?)`
|
|
154
|
+
Registers the bootstrap for every document the page loads from now on.
|
|
155
|
+
Must be called **before** the navigation you want patched.
|
|
156
|
+
Returns the page.
|
|
157
|
+
|
|
158
|
+
### `installOopifHook(browser, opts?)`
|
|
159
|
+
Hooks `targetcreated` and injects the bootstrap into out-of-process / sandboxed
|
|
160
|
+
iframes (which get their own CDP target and are **not** covered by
|
|
161
|
+
page-level `evaluateOnNewDocument`). Idempotent. Returns the browser.
|
|
162
|
+
|
|
163
|
+
### `await injectNow(page, opts?)`
|
|
164
|
+
Runs the bootstrap in the page's **current** document. Use after `connect()`
|
|
165
|
+
when the page is already loaded, then reload.
|
|
166
|
+
|
|
167
|
+
### `await newPage(browser, opts?)`
|
|
168
|
+
`browser.newPage()` + `installStealth` + `installOopifHook`.
|
|
169
|
+
|
|
170
|
+
### `buildSource(opts?)`
|
|
171
|
+
Returns the bootstrap as a **string** — handy for a raw CDP call:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
await session.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
175
|
+
source: stealth.buildSource({}),
|
|
176
|
+
runImmediately: true,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `await humanMouse(page, points)`
|
|
181
|
+
Tiny human-ish mouse helper: `[{ x, y, steps?, click?, delay? }, ...]`.
|
|
182
|
+
|
|
183
|
+
### `await launch(opts?)`
|
|
184
|
+
Optional launcher. Requires puppeteer via `opts.puppeteer` or resolved lazily
|
|
185
|
+
(`puppeteer-core`, then `puppeteer`). Never touched unless you call it.
|
|
186
|
+
|
|
187
|
+
## Options (`opts`)
|
|
188
|
+
|
|
189
|
+
| Key | Default | Used by |
|
|
190
|
+
|-----|---------|---------|
|
|
191
|
+
| `webglRenderer` / `renderer` | `ANGLE (Intel, ...)` | all |
|
|
192
|
+
| `webglVendor` / `vendor` | `Google Inc. (Intel)` | all |
|
|
193
|
+
| `hardwareConcurrency` | `8` | all |
|
|
194
|
+
| `deviceMemory` | `8` | all |
|
|
195
|
+
| `platform` | `Linux x86_64` | all |
|
|
196
|
+
| `origin` / `permissions` | — | `newPage` |
|
|
197
|
+
| `puppeteer` | lazily resolved | `launch` |
|
|
198
|
+
| `executablePath` | `$CHROME_PATH` | `launch` |
|
|
199
|
+
| `headless` | `false` | `launch` |
|
|
200
|
+
| `userDataDir` | `~/.cache/chrome-stealth-profile` | `launch` |
|
|
201
|
+
| `args` | `[]` | `launch` |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Verify it is working
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
await stealth.installStealth(page, {});
|
|
209
|
+
await page.goto('about:blank');
|
|
210
|
+
console.log(await page.evaluate(() => {
|
|
211
|
+
const c = document.createElement('canvas').getContext('webgl');
|
|
212
|
+
const e = c.getExtension('WEBGL_debug_renderer_info');
|
|
213
|
+
return {
|
|
214
|
+
webdriver: navigator.webdriver, // false
|
|
215
|
+
plugins: navigator.plugins.length, // > 0
|
|
216
|
+
gl: [c.getParameter(e.UNMASKED_VENDOR_WEBGL)], // not "SwiftShader"
|
|
217
|
+
};
|
|
218
|
+
}));
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Troubleshooting
|
|
222
|
+
|
|
223
|
+
- **Still detected after `connect()`** — you probably called `goto()` before
|
|
224
|
+
`installStealth()`. Install first, then navigate (or use `injectNow()` + reload).
|
|
225
|
+
- **Fresh vs reused profile** — a persistent profile can carry a *stale, already
|
|
226
|
+
flagged* cookie for the target. Test with a brand-new `userDataDir` to rule
|
|
227
|
+
that out.
|
|
228
|
+
- **Software WebGL** — servers with no GPU report SwiftShader. The WebGL patch
|
|
229
|
+
hides this in the page, but make sure `installStealth` actually ran.
|
|
230
|
+
- **Sandboxed iframes** — if the challenge runs in a `<iframe sandbox>`, make
|
|
231
|
+
sure `installOopifHook(browser)` was called.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webcrawlerapi/stealth",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency stealth layer for Puppeteer / CDP browsers (works with launch() and connect()).",
|
|
5
|
+
"main": "stealth.js",
|
|
6
|
+
"types": "stealth.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"stealth.js",
|
|
9
|
+
"stealth.d.ts",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"example:connect": "node examples/connect.js",
|
|
15
|
+
"example:launch": "node examples/launch.js"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=16"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"puppeteer": ">=19",
|
|
22
|
+
"puppeteer-core": ">=19"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"puppeteer": {
|
|
26
|
+
"optional": true
|
|
27
|
+
},
|
|
28
|
+
"puppeteer-core": {
|
|
29
|
+
"optional": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/WebCrawlerAPI/stealth.git"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/stealth.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface StealthOptions {
|
|
2
|
+
/** WebGL UNMASKED_RENDERER_WEBGL value to report. */
|
|
3
|
+
webglRenderer?: string;
|
|
4
|
+
/** WebGL UNMASKED_VENDOR_WEBGL value to report. */
|
|
5
|
+
webglVendor?: string;
|
|
6
|
+
/** navigator.hardwareConcurrency override. */
|
|
7
|
+
hardwareConcurrency?: number;
|
|
8
|
+
/** navigator.deviceMemory override. */
|
|
9
|
+
deviceMemory?: number;
|
|
10
|
+
/** navigator.platform override. */
|
|
11
|
+
platform?: string;
|
|
12
|
+
/** Launch only: injected puppeteer instance. */
|
|
13
|
+
puppeteer?: unknown;
|
|
14
|
+
/** Launch only: chrome executable. */
|
|
15
|
+
executablePath?: string;
|
|
16
|
+
/** Launch only. */
|
|
17
|
+
headless?: boolean;
|
|
18
|
+
/** Launch only: profile dir. */
|
|
19
|
+
userDataDir?: string;
|
|
20
|
+
/** Launch only: extra chrome args. */
|
|
21
|
+
args?: string[];
|
|
22
|
+
/** newPage only: origin for overridePermissions. */
|
|
23
|
+
origin?: string;
|
|
24
|
+
/** newPage only: permissions to grant. */
|
|
25
|
+
permissions?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Minimal structural types so the module has no puppeteer dependency. */
|
|
29
|
+
export interface PageLike {
|
|
30
|
+
evaluateOnNewDocument(fn: unknown, arg?: unknown): Promise<unknown>;
|
|
31
|
+
evaluate(fnOrSource: unknown): Promise<unknown>;
|
|
32
|
+
mouse: {
|
|
33
|
+
move(x: number, y: number, opts?: { steps?: number }): Promise<void>;
|
|
34
|
+
down(): Promise<void>;
|
|
35
|
+
up(): Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BrowserLike {
|
|
40
|
+
on(event: string, listener: (target: any) => void): unknown;
|
|
41
|
+
newPage(): Promise<PageLike>;
|
|
42
|
+
defaultBrowserContext?(): { overridePermissions(origin: string, permissions: string[]): Promise<void> };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export declare const VERSION: string;
|
|
46
|
+
export declare const UA: string;
|
|
47
|
+
export declare function buildInitScript(cfg?: StealthOptions): void;
|
|
48
|
+
export declare function buildSource(opts?: StealthOptions): string;
|
|
49
|
+
export declare function installStealth(page: PageLike, opts?: StealthOptions): Promise<PageLike>;
|
|
50
|
+
export declare function injectNow(page: PageLike, opts?: StealthOptions): Promise<PageLike>;
|
|
51
|
+
export declare function installOopifHook(browser: BrowserLike, opts?: StealthOptions): BrowserLike;
|
|
52
|
+
export declare function newPage(browser: BrowserLike, opts?: StealthOptions): Promise<PageLike>;
|
|
53
|
+
export declare function humanMouse(page: PageLike, points: Array<{ x: number; y: number; steps?: number; click?: boolean; delay?: number }>): Promise<void>;
|
|
54
|
+
export declare function getPuppeteer(injected?: unknown): any;
|
|
55
|
+
export declare function launch(opts?: StealthOptions): Promise<BrowserLike>;
|
package/stealth.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @webcrawlerapi/stealth
|
|
5
|
+
* Zero-dependency stealth layer for Puppeteer / any CDP-driven browser.
|
|
6
|
+
*
|
|
7
|
+
* Works with BOTH puppeteer.launch() and puppeteer.connect():
|
|
8
|
+
*
|
|
9
|
+
* const stealth = require('./stealth.js');
|
|
10
|
+
* const page = await browser.newPage(); // or reuse an existing page
|
|
11
|
+
* await stealth.installStealth(page, {}); // BEFORE any goto()
|
|
12
|
+
* stealth.installOopifHook(browser, {}); // patches sandboxed iframes too
|
|
13
|
+
* await page.goto(url);
|
|
14
|
+
*
|
|
15
|
+
* There are no runtime dependencies. Puppeteer is only required if you use
|
|
16
|
+
* the optional `launch()` helper, and even then it is looked up lazily or can
|
|
17
|
+
* be injected via `opts.puppeteer`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const UA =
|
|
21
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
|
|
22
|
+
'(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36';
|
|
23
|
+
|
|
24
|
+
const VERSION = '1.0.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)';
|
|
29
|
+
|
|
30
|
+
/* ------------------------------------------------------------------ *
|
|
31
|
+
* The in-page bootstrap. Runs inside every document before page JS. *
|
|
32
|
+
* Serialized by evaluateOnNewDocument() / addScriptToEvaluateOnNewDocument()
|
|
33
|
+
* so it must be fully self-contained (no closure over this module).
|
|
34
|
+
* ------------------------------------------------------------------ */
|
|
35
|
+
function buildInitScript(cfg) {
|
|
36
|
+
cfg = cfg || {};
|
|
37
|
+
const renderer = cfg.renderer || 'ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)';
|
|
38
|
+
const vendor = cfg.vendor || 'Google Inc. (Intel)';
|
|
39
|
+
|
|
40
|
+
const patched = new WeakSet();
|
|
41
|
+
|
|
42
|
+
try { Object.defineProperty(window, '__stealth_ts', { value: Date.now(), configurable: true }); } catch (_) {}
|
|
43
|
+
|
|
44
|
+
// --- native-looking Function.prototype.toString ---
|
|
45
|
+
const origToString = Function.prototype.toString;
|
|
46
|
+
const fakeToString = function toString() {
|
|
47
|
+
if (patched.has(this)) {
|
|
48
|
+
const name = (this && this.name) || '';
|
|
49
|
+
return 'function ' + name + '() { [native code] }';
|
|
50
|
+
}
|
|
51
|
+
return origToString.call(this);
|
|
52
|
+
};
|
|
53
|
+
patched.add(fakeToString);
|
|
54
|
+
try { Object.defineProperty(fakeToString, 'name', { value: 'toString' }); } catch (_) {}
|
|
55
|
+
try { Object.defineProperty(fakeToString, 'length', { value: 0 }); } catch (_) {}
|
|
56
|
+
Object.defineProperty(Function.prototype, 'toString', {
|
|
57
|
+
value: fakeToString,
|
|
58
|
+
writable: true,
|
|
59
|
+
configurable: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function cloak(fn, name) {
|
|
63
|
+
if (typeof fn !== 'function') return fn;
|
|
64
|
+
patched.add(fn);
|
|
65
|
+
try {
|
|
66
|
+
if (name) Object.defineProperty(fn, 'name', { value: name, configurable: true });
|
|
67
|
+
Object.defineProperty(fn, 'length', { value: fn.length, configurable: true });
|
|
68
|
+
} catch (_) {}
|
|
69
|
+
return fn;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getter(obj, prop, value, name) {
|
|
73
|
+
const get = cloak(function () { return value; }, 'get ' + (name || prop));
|
|
74
|
+
Object.defineProperty(obj, prop, {
|
|
75
|
+
get,
|
|
76
|
+
set: undefined,
|
|
77
|
+
enumerable: true,
|
|
78
|
+
configurable: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- navigator.webdriver ---
|
|
83
|
+
try { delete Navigator.prototype.webdriver; } catch (_) {}
|
|
84
|
+
try { getter(Navigator.prototype, 'webdriver', false, 'webdriver'); } catch (_) {}
|
|
85
|
+
|
|
86
|
+
// --- window.chrome ---
|
|
87
|
+
if (!window.chrome || typeof window.chrome !== 'object') window.chrome = {};
|
|
88
|
+
if (!window.chrome.runtime) {
|
|
89
|
+
Object.defineProperty(window.chrome, 'runtime', {
|
|
90
|
+
value: {
|
|
91
|
+
connect: cloak(function connect() {}, 'connect'),
|
|
92
|
+
sendMessage: cloak(function sendMessage() {}, 'sendMessage'),
|
|
93
|
+
id: undefined,
|
|
94
|
+
},
|
|
95
|
+
configurable: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (!window.chrome.loadTimes) {
|
|
99
|
+
Object.defineProperty(window.chrome, 'loadTimes', {
|
|
100
|
+
value: cloak(function loadTimes() {
|
|
101
|
+
return { requestTime: 0, startLoadTime: 0, commitLoadTime: 0, finishLoadTime: 0, navigationType: 'Other', wasNpnNegotiated: false, npnNegotiatedProtocol: 'unknown', wasAlternateProtocolAvailable: false, connectionInfo: 'unknown' };
|
|
102
|
+
}, 'loadTimes'),
|
|
103
|
+
configurable: true,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!window.chrome.csi) {
|
|
107
|
+
Object.defineProperty(window.chrome, 'csi', {
|
|
108
|
+
value: cloak(function csi() { return { onloadT: Date.now(), startE: Date.now(), pageT: 1, tran: 15 }; }, 'csi'),
|
|
109
|
+
configurable: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (!window.chrome.app) {
|
|
113
|
+
window.chrome.app = {
|
|
114
|
+
isInstalled: false,
|
|
115
|
+
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
|
|
116
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- navigator.languages / language ---
|
|
121
|
+
try { getter(Navigator.prototype, 'languages', Object.freeze(['en-US', 'en']), 'languages'); } catch (_) {}
|
|
122
|
+
try { getter(Navigator.prototype, 'language', 'en-US', 'language'); } catch (_) {}
|
|
123
|
+
|
|
124
|
+
// --- hardware / platform ---
|
|
125
|
+
try { getter(Navigator.prototype, 'hardwareConcurrency', cfg.hardwareConcurrency || 8, 'hardwareConcurrency'); } catch (_) {}
|
|
126
|
+
try { getter(Navigator.prototype, 'deviceMemory', cfg.deviceMemory || 8, 'deviceMemory'); } catch (_) {}
|
|
127
|
+
try { getter(Navigator.prototype, 'platform', cfg.platform || 'Linux x86_64', 'platform'); } catch (_) {}
|
|
128
|
+
try { getter(Navigator.prototype, 'maxTouchPoints', 0, 'maxTouchPoints'); } catch (_) {}
|
|
129
|
+
try { getter(Navigator.prototype, 'vendor', 'Google Inc.', 'vendor'); } catch (_) {}
|
|
130
|
+
try { getter(Navigator.prototype, 'onLine', true, 'onLine'); } catch (_) {}
|
|
131
|
+
|
|
132
|
+
// --- plugins / mimeTypes (only when empty) ---
|
|
133
|
+
try {
|
|
134
|
+
if (!navigator.plugins || navigator.plugins.length === 0) {
|
|
135
|
+
function mkMime(type, suffixes, description) {
|
|
136
|
+
return { type, suffixes, description, enabledPlugin: null };
|
|
137
|
+
}
|
|
138
|
+
const pluginDefs = [
|
|
139
|
+
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', mimes: [
|
|
140
|
+
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
|
141
|
+
{ type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
|
142
|
+
]},
|
|
143
|
+
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', mimes: [
|
|
144
|
+
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
|
145
|
+
{ type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
|
146
|
+
]},
|
|
147
|
+
];
|
|
148
|
+
const plugins = [];
|
|
149
|
+
const mimes = [];
|
|
150
|
+
for (const p of pluginDefs) {
|
|
151
|
+
const plugin = { name: p.name, filename: p.filename, description: p.description, length: p.mimes.length };
|
|
152
|
+
p.mimes.forEach((m, i) => {
|
|
153
|
+
const mm = mkMime(m.type, m.suffixes, m.description);
|
|
154
|
+
mm.enabledPlugin = plugin;
|
|
155
|
+
plugin[i] = mm;
|
|
156
|
+
if (!mimes.some((x) => x.type === m.type)) mimes.push(mm);
|
|
157
|
+
});
|
|
158
|
+
plugins.push(plugin);
|
|
159
|
+
}
|
|
160
|
+
plugins.item = cloak(function item(i) { return this[i] || null; }, 'item');
|
|
161
|
+
plugins.namedItem = cloak(function namedItem() { return null; }, 'namedItem');
|
|
162
|
+
plugins.refresh = cloak(function refresh() {}, 'refresh');
|
|
163
|
+
mimes.item = cloak(function item(i) { return this[i] || null; }, 'item');
|
|
164
|
+
mimes.namedItem = cloak(function namedItem() { return null; }, 'namedItem');
|
|
165
|
+
Object.defineProperty(plugins, 'length', { value: pluginDefs.length });
|
|
166
|
+
Object.defineProperty(mimes, 'length', { value: mimes.length });
|
|
167
|
+
try {
|
|
168
|
+
const pProto = Object.getPrototypeOf(navigator.plugins);
|
|
169
|
+
Object.defineProperty(pProto, 'item', { value: cloak(function item(i) { return this[i] || null; }, 'item'), writable: true, configurable: true });
|
|
170
|
+
Object.defineProperty(pProto, 'namedItem', { value: cloak(function namedItem() { return null; }, 'namedItem'), writable: true, configurable: true });
|
|
171
|
+
Object.defineProperty(pProto, 'refresh', { value: cloak(function refresh() {}, 'refresh'), writable: true, configurable: true });
|
|
172
|
+
const mProto = Object.getPrototypeOf(navigator.mimeTypes);
|
|
173
|
+
Object.defineProperty(mProto, 'item', { value: cloak(function item(i) { return this[i] || null; }, 'item'), writable: true, configurable: true });
|
|
174
|
+
Object.defineProperty(mProto, 'namedItem', { value: cloak(function namedItem() { return null; }, 'namedItem'), writable: true, configurable: true });
|
|
175
|
+
} catch (_) {}
|
|
176
|
+
getter(Navigator.prototype, 'plugins', plugins, 'plugins');
|
|
177
|
+
getter(Navigator.prototype, 'mimeTypes', mimes, 'mimeTypes');
|
|
178
|
+
}
|
|
179
|
+
} catch (_) {}
|
|
180
|
+
|
|
181
|
+
// --- permissions ---
|
|
182
|
+
try {
|
|
183
|
+
const origQuery = navigator.permissions.query.bind(navigator.permissions);
|
|
184
|
+
const query = cloak(function query(params) {
|
|
185
|
+
if (params && params.name === 'notifications') {
|
|
186
|
+
return Promise.resolve({ state: 'default', name: 'notifications', onchange: null });
|
|
187
|
+
}
|
|
188
|
+
return origQuery(params);
|
|
189
|
+
}, 'query');
|
|
190
|
+
try {
|
|
191
|
+
Object.defineProperty(Permissions.prototype, 'query', { value: query, writable: true, configurable: true });
|
|
192
|
+
} catch (_) {
|
|
193
|
+
navigator.permissions.query = query;
|
|
194
|
+
}
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
|
|
197
|
+
// --- userAgentData ---
|
|
198
|
+
try {
|
|
199
|
+
const brands = [
|
|
200
|
+
{ brand: 'Chromium', version: '151' },
|
|
201
|
+
{ brand: 'Not?A_Brand', version: '24' },
|
|
202
|
+
{ brand: 'Google Chrome', version: '151' },
|
|
203
|
+
];
|
|
204
|
+
const fullVersionList = brands.map((b) => ({ brand: b.brand, version: b.brand === 'Not?A_Brand' ? '24.0.0.0' : '151.0.7922.108' }));
|
|
205
|
+
const highEntropy = {
|
|
206
|
+
architecture: 'x86', bitness: '64', brands, fullVersionList, mobile: false, model: '',
|
|
207
|
+
platform: 'Linux', platformVersion: '6.8.0', uaFullVersion: '151.0.7922.108', wow64: false,
|
|
208
|
+
};
|
|
209
|
+
const data = {
|
|
210
|
+
brands, mobile: false, platform: 'Linux',
|
|
211
|
+
getHighEntropyValues: cloak(function getHighEntropyValues(hints) {
|
|
212
|
+
const out = { brands, mobile: false, platform: 'Linux' };
|
|
213
|
+
(hints || []).forEach((h) => { if (h in highEntropy) out[h] = highEntropy[h]; });
|
|
214
|
+
return Promise.resolve(out);
|
|
215
|
+
}, 'getHighEntropyValues'),
|
|
216
|
+
toJSON: cloak(function toJSON() { return { brands, mobile: false, platform: 'Linux' }; }, 'toJSON'),
|
|
217
|
+
};
|
|
218
|
+
if (navigator.userAgentData) {
|
|
219
|
+
try {
|
|
220
|
+
Object.defineProperty(Navigator.prototype, 'userAgentData', { get: cloak(function () { return data; }, 'get userAgentData'), configurable: true });
|
|
221
|
+
} catch (_) {}
|
|
222
|
+
} else {
|
|
223
|
+
getter(Navigator.prototype, 'userAgentData', data, 'userAgentData');
|
|
224
|
+
}
|
|
225
|
+
} catch (_) {}
|
|
226
|
+
|
|
227
|
+
// --- WebGL vendor / renderer ---
|
|
228
|
+
try {
|
|
229
|
+
for (const name of ['WebGLRenderingContext', 'WebGL2RenderingContext']) {
|
|
230
|
+
const Ctor = window[name];
|
|
231
|
+
if (!Ctor || !Ctor.prototype) continue;
|
|
232
|
+
const origGetParameter = Ctor.prototype.getParameter;
|
|
233
|
+
const origGetExtension = Ctor.prototype.getExtension;
|
|
234
|
+
const getParameter = cloak(function getParameter(p) {
|
|
235
|
+
if (p === 37445) return vendor;
|
|
236
|
+
if (p === 37446) return renderer;
|
|
237
|
+
return origGetParameter.call(this, p);
|
|
238
|
+
}, 'getParameter');
|
|
239
|
+
const getExtension = cloak(function getExtension(n) {
|
|
240
|
+
if (n === 'WEBGL_debug_renderer_info') {
|
|
241
|
+
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
242
|
+
}
|
|
243
|
+
return origGetExtension.call(this, n);
|
|
244
|
+
}, 'getExtension');
|
|
245
|
+
Object.defineProperty(Ctor.prototype, 'getParameter', { value: getParameter, writable: true, configurable: true });
|
|
246
|
+
Object.defineProperty(Ctor.prototype, 'getExtension', { value: getExtension, writable: true, configurable: true });
|
|
247
|
+
}
|
|
248
|
+
} catch (_) {}
|
|
249
|
+
|
|
250
|
+
// --- remove automation residue globals ---
|
|
251
|
+
try {
|
|
252
|
+
const keys = [
|
|
253
|
+
'cdc_adoQpoasnfa76pfcZLmcfl_Array', 'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
|
|
254
|
+
'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
|
|
255
|
+
'__webdriver_evaluate', '__selenium_evaluate', '__fxdriver_evaluate',
|
|
256
|
+
'__driver_evaluate', '__webdriver_script_function', '__webdriver_script_func',
|
|
257
|
+
'__webdriver_script_fn', '__webdriverFunc', '_WEBDRIVER_ELEM_CACHE',
|
|
258
|
+
'domAutomation', 'domAutomationController', '__lastWatirAlert',
|
|
259
|
+
'__lastWatirConfirm', '__lastWatirPrompt', '__playwright_builtins__',
|
|
260
|
+
'__pwInitScripts', '__playwright__binding__', '__pwWebSocketDispatch',
|
|
261
|
+
'__playwright__binding__controller__',
|
|
262
|
+
];
|
|
263
|
+
for (const k of keys) {
|
|
264
|
+
try {
|
|
265
|
+
if (k in window) delete window[k];
|
|
266
|
+
if (k in document) delete document[k];
|
|
267
|
+
} catch (_) {}
|
|
268
|
+
}
|
|
269
|
+
try { if (navigator.webdriver) delete navigator.webdriver; } catch (_) {}
|
|
270
|
+
} catch (_) {}
|
|
271
|
+
|
|
272
|
+
// --- Error.prepareStackTrace integrity ---
|
|
273
|
+
try {
|
|
274
|
+
if (typeof Error.prepareStackTrace !== 'function') {
|
|
275
|
+
Object.defineProperty(Error, 'prepareStackTrace', {
|
|
276
|
+
value: cloak(function prepareStackTrace(err, stack) {
|
|
277
|
+
return String(err) + '\n' + stack.map((s) => ' at ' + s.toString()).join('\n');
|
|
278
|
+
}, 'prepareStackTrace'),
|
|
279
|
+
writable: true,
|
|
280
|
+
configurable: true,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
} catch (_) {}
|
|
284
|
+
|
|
285
|
+
// --- matchMedia: pointer / hover ---
|
|
286
|
+
try {
|
|
287
|
+
const OVERRIDES = {
|
|
288
|
+
'any-pointer: fine': true, 'any-pointer: coarse': false, 'any-pointer: none': false,
|
|
289
|
+
'pointer: fine': true, 'pointer: coarse': false, 'pointer: none': false,
|
|
290
|
+
'any-hover: hover': true, 'any-hover: none': false,
|
|
291
|
+
'hover: hover': true, 'hover: none': false,
|
|
292
|
+
};
|
|
293
|
+
const norm = (q) => String(q).toLowerCase().replace(/\s+/g, ' ').trim().replace(/^\(/, '').replace(/\)$/, '').trim();
|
|
294
|
+
const orig = window.matchMedia.bind(window);
|
|
295
|
+
const matchMedia = cloak(function matchMedia(query) {
|
|
296
|
+
const mql = orig(query);
|
|
297
|
+
const key = norm(query);
|
|
298
|
+
if (Object.prototype.hasOwnProperty.call(OVERRIDES, key)) {
|
|
299
|
+
const val = OVERRIDES[key];
|
|
300
|
+
try {
|
|
301
|
+
Object.defineProperty(mql, 'matches', { get: cloak(function () { return val; }, 'get matches'), configurable: true });
|
|
302
|
+
} catch (_) {}
|
|
303
|
+
}
|
|
304
|
+
return mql;
|
|
305
|
+
}, 'matchMedia');
|
|
306
|
+
Object.defineProperty(window, 'matchMedia', { value: matchMedia, writable: true, configurable: true });
|
|
307
|
+
} catch (_) {}
|
|
308
|
+
|
|
309
|
+
// --- window geometry drift ---
|
|
310
|
+
try {
|
|
311
|
+
const innerH = window.innerHeight || 900;
|
|
312
|
+
const innerW = window.innerWidth || 1366;
|
|
313
|
+
getter(window, 'outerHeight', innerH + 20, 'outerHeight');
|
|
314
|
+
getter(window, 'outerWidth', innerW, 'outerWidth');
|
|
315
|
+
} catch (_) {}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ------------------------------------------------------------------ *
|
|
319
|
+
* Public API *
|
|
320
|
+
* ------------------------------------------------------------------ */
|
|
321
|
+
|
|
322
|
+
function resolveConfig(opts) {
|
|
323
|
+
opts = opts || {};
|
|
324
|
+
return {
|
|
325
|
+
renderer: opts.webglRenderer || opts.renderer || DEFAULT_RENDERER,
|
|
326
|
+
vendor: opts.webglVendor || opts.vendor || DEFAULT_VENDOR,
|
|
327
|
+
hardwareConcurrency: opts.hardwareConcurrency,
|
|
328
|
+
deviceMemory: opts.deviceMemory,
|
|
329
|
+
platform: opts.platform,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Serialized source string for CDP `Page.addScriptToEvaluateOnNewDocument`. */
|
|
334
|
+
function buildSource(opts) {
|
|
335
|
+
const cfg = resolveConfig(opts);
|
|
336
|
+
return '(' + buildInitScript.toString() + ')(' + JSON.stringify(cfg) + ');';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const INSTALLED = Symbol.for('@webcrawlerapi/stealth.installed');
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Install the stealth bootstrap into a page. Call BEFORE the first goto().
|
|
343
|
+
* Compatible with puppeteer.launch(), puppeteer.connect(), and any object
|
|
344
|
+
* exposing `evaluateOnNewDocument(fn, arg)`.
|
|
345
|
+
*
|
|
346
|
+
* @returns {Promise<any>} the same page
|
|
347
|
+
*/
|
|
348
|
+
async function installStealth(page, opts) {
|
|
349
|
+
if (!page || typeof page.evaluateOnNewDocument !== 'function') {
|
|
350
|
+
throw new TypeError('installStealth(page): page must expose evaluateOnNewDocument()');
|
|
351
|
+
}
|
|
352
|
+
opts = opts || {};
|
|
353
|
+
const cfg = resolveConfig(opts);
|
|
354
|
+
await page.evaluateOnNewDocument(buildInitScript, cfg);
|
|
355
|
+
try { page[INSTALLED] = true; } catch (_) {}
|
|
356
|
+
return page;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Also inject the bootstrap into documents that are ALREADY loaded (useful when
|
|
361
|
+
* you attach with connect() to a page that is already on the target URL).
|
|
362
|
+
* The patches will not re-run on navigation unless installStealth() is used too.
|
|
363
|
+
*/
|
|
364
|
+
async function injectNow(page, opts) {
|
|
365
|
+
if (!page || typeof page.evaluate !== 'function') {
|
|
366
|
+
throw new TypeError('injectNow(page): page must expose evaluate()');
|
|
367
|
+
}
|
|
368
|
+
await page.evaluate(buildSource(opts));
|
|
369
|
+
return page;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Patch out-of-process / sandboxed iframes (DataDome's challenge runs there).
|
|
374
|
+
* These get their own CDP target and are NOT covered by page-level
|
|
375
|
+
* evaluateOnNewDocument.
|
|
376
|
+
*/
|
|
377
|
+
function installOopifHook(browser, opts) {
|
|
378
|
+
if (!browser || typeof browser.on !== 'function') {
|
|
379
|
+
throw new TypeError('installOopifHook(browser): browser must expose on()');
|
|
380
|
+
}
|
|
381
|
+
opts = opts || {};
|
|
382
|
+
if (browser.__stealthOopifHooked) return browser;
|
|
383
|
+
browser.__stealthOopifHooked = true;
|
|
384
|
+
|
|
385
|
+
const source = buildSource(opts);
|
|
386
|
+
browser.on('targetcreated', async (target) => {
|
|
387
|
+
let type = 'unknown';
|
|
388
|
+
try { type = target.type(); } catch (_) {}
|
|
389
|
+
if (type === 'browser' || type === 'page' || type === 'service_worker') return;
|
|
390
|
+
let session;
|
|
391
|
+
try {
|
|
392
|
+
session = await target.createCDPSession();
|
|
393
|
+
await session.send('Page.enable').catch(() => {});
|
|
394
|
+
await session.send('Page.addScriptToEvaluateOnNewDocument', { source, runImmediately: true }).catch(() => {});
|
|
395
|
+
await session.send('Runtime.runIfWaitingForDebugger').catch(() => {});
|
|
396
|
+
} catch (_) {}
|
|
397
|
+
});
|
|
398
|
+
return browser;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Convenience: open a page and patch it in one call. */
|
|
402
|
+
async function newPage(browser, opts) {
|
|
403
|
+
if (!browser || typeof browser.newPage !== 'function') {
|
|
404
|
+
throw new TypeError('newPage(browser): browser must expose newPage()');
|
|
405
|
+
}
|
|
406
|
+
opts = opts || {};
|
|
407
|
+
const context = typeof browser.defaultBrowserContext === 'function' ? browser.defaultBrowserContext() : null;
|
|
408
|
+
if (context && opts.origin && typeof context.overridePermissions === 'function') {
|
|
409
|
+
await context.overridePermissions(opts.origin, opts.permissions || []).catch(() => {});
|
|
410
|
+
}
|
|
411
|
+
const page = await browser.newPage();
|
|
412
|
+
await installStealth(page, opts);
|
|
413
|
+
installOopifHook(browser, opts);
|
|
414
|
+
return page;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function humanMouse(page, points) {
|
|
418
|
+
for (let i = 0; i < points.length; i++) {
|
|
419
|
+
const p = points[i];
|
|
420
|
+
await page.mouse.move(p.x, p.y, { steps: p.steps || 12 });
|
|
421
|
+
if (p.click) { await page.mouse.down(); await page.mouse.up(); }
|
|
422
|
+
await new Promise((r) => setTimeout(r, p.delay == null ? 40 : p.delay));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Lazily resolve a puppeteer module (never required unless launch() is used). */
|
|
427
|
+
function getPuppeteer(injected) {
|
|
428
|
+
if (injected) return injected;
|
|
429
|
+
for (const name of ['puppeteer-core', 'puppeteer']) {
|
|
430
|
+
try { return require(name); } catch (_) {}
|
|
431
|
+
}
|
|
432
|
+
throw new Error('launch() needs puppeteer; pass opts.puppeteer or install puppeteer/puppeteer-core');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Optional launcher. Requires puppeteer only when actually called. */
|
|
436
|
+
async function launch(opts) {
|
|
437
|
+
opts = opts || {};
|
|
438
|
+
const puppeteer = getPuppeteer(opts.puppeteer);
|
|
439
|
+
const os = require('os');
|
|
440
|
+
const path = require('path');
|
|
441
|
+
const args = opts.args || [];
|
|
442
|
+
const browser = await puppeteer.launch({
|
|
443
|
+
executablePath: opts.executablePath || process.env.CHROME_PATH || undefined,
|
|
444
|
+
headless: opts.headless != null ? opts.headless : false,
|
|
445
|
+
userDataDir: opts.userDataDir || path.join(os.homedir(), '.cache', 'chrome-stealth-profile'),
|
|
446
|
+
defaultViewport: opts.viewport || { width: 1366, height: 900 },
|
|
447
|
+
protocolTimeout: opts.protocolTimeout || 180000,
|
|
448
|
+
ignoreDefaultArgs: ['--enable-automation', '--headless'],
|
|
449
|
+
args: [
|
|
450
|
+
'--no-sandbox',
|
|
451
|
+
'--disable-setuid-sandbox',
|
|
452
|
+
'--disable-blink-features=AutomationControlled',
|
|
453
|
+
'--disable-infobars',
|
|
454
|
+
'--disable-dev-shm-usage',
|
|
455
|
+
'--no-first-run',
|
|
456
|
+
'--no-default-browser-check',
|
|
457
|
+
'--window-size=1366,900',
|
|
458
|
+
'--lang=en-US',
|
|
459
|
+
...args,
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
return browser;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
VERSION,
|
|
467
|
+
UA,
|
|
468
|
+
buildInitScript,
|
|
469
|
+
buildSource,
|
|
470
|
+
installStealth,
|
|
471
|
+
injectNow,
|
|
472
|
+
installOopifHook,
|
|
473
|
+
newPage,
|
|
474
|
+
humanMouse,
|
|
475
|
+
getPuppeteer,
|
|
476
|
+
launch,
|
|
477
|
+
};
|