real-browser-plus 1.1.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.
Potentially problematic release.
This version of real-browser-plus might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/index.d.mts +123 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +692 -0
- package/dist/index.mjs +645 -0
- package/package.json +62 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import puppeteerExtra from "puppeteer-extra";
|
|
3
|
+
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
4
|
+
|
|
5
|
+
// src/stealth/evasions.ts
|
|
6
|
+
async function applyStealthEvasions(page) {
|
|
7
|
+
await page.evaluateOnNewDocument(() => {
|
|
8
|
+
Object.defineProperty(Navigator.prototype, "webdriver", { get: () => void 0, configurable: true });
|
|
9
|
+
const w = window;
|
|
10
|
+
if (!w.chrome) {
|
|
11
|
+
w.chrome = {
|
|
12
|
+
runtime: {},
|
|
13
|
+
loadTimes: function() {
|
|
14
|
+
},
|
|
15
|
+
csi: function() {
|
|
16
|
+
},
|
|
17
|
+
app: { isInstalled: false }
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const originalQuery = window.navigator.permissions.query;
|
|
21
|
+
window.navigator.permissions.query = (parameters) => parameters.name === "notifications" ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters);
|
|
22
|
+
Object.defineProperty(navigator, "plugins", {
|
|
23
|
+
get: () => [
|
|
24
|
+
{ name: "PDF Viewer", filename: "internal-pdf-viewer" },
|
|
25
|
+
{ name: "Chrome PDF Viewer", filename: "internal-pdf-viewer" },
|
|
26
|
+
{ name: "Chromium PDF Viewer", filename: "internal-pdf-viewer" },
|
|
27
|
+
{ name: "Native Client", filename: "internal-nacl-plugin" }
|
|
28
|
+
]
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
const proto = HTMLIFrameElement.prototype;
|
|
32
|
+
const oIFCW = proto.__lookupGetter__?.call(HTMLIFrameElement.prototype, "contentWindow");
|
|
33
|
+
if (oIFCW) {
|
|
34
|
+
Object.defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
|
|
35
|
+
get: function() {
|
|
36
|
+
return oIFCW.call(this);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
const doc = document;
|
|
43
|
+
delete doc.$cdc_asdjflasutopfhvcZLmcfl_;
|
|
44
|
+
const win = window;
|
|
45
|
+
delete win.cdc_adoQpoasnfa76pfcZLmcfl_Array;
|
|
46
|
+
window.Function.prototype.toString = new Proxy(window.Function.prototype.toString, {
|
|
47
|
+
apply(target, thisArg, args) {
|
|
48
|
+
if (thisArg === window.navigator.permissions.query) return "function query() { [native code] }";
|
|
49
|
+
return Reflect.apply(target, thisArg, args);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
Object.defineProperty(Notification, "permission", { get: () => "default" });
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/stealth/index.ts
|
|
60
|
+
var STEALTH_LAUNCH_ARGS = [
|
|
61
|
+
"--disable-blink-features=AutomationControlled",
|
|
62
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
63
|
+
"--no-first-run",
|
|
64
|
+
"--no-default-browser-check",
|
|
65
|
+
"--disable-infobars",
|
|
66
|
+
"--window-position=0,0",
|
|
67
|
+
"--disable-dev-shm-usage"
|
|
68
|
+
];
|
|
69
|
+
async function applyAllStealth(page) {
|
|
70
|
+
await applyStealthEvasions(page);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/fingerprint/generator.ts
|
|
74
|
+
var CHROME_VERSIONS = ["128.0.6613.120", "129.0.6668.90", "130.0.6723.92", "131.0.6778.86", "132.0.6834.83"];
|
|
75
|
+
var WINDOWS_UA_TEMPLATE = (v) => `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${v} Safari/537.36`;
|
|
76
|
+
var MAC_UA_TEMPLATE = (v) => `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${v} Safari/537.36`;
|
|
77
|
+
var RESOLUTIONS = [
|
|
78
|
+
{ screen: { width: 1920, height: 1080 }, viewport: { width: 1920, height: 1040 } },
|
|
79
|
+
{ screen: { width: 1536, height: 864 }, viewport: { width: 1536, height: 824 } },
|
|
80
|
+
{ screen: { width: 1440, height: 900 }, viewport: { width: 1440, height: 860 } },
|
|
81
|
+
{ screen: { width: 1366, height: 768 }, viewport: { width: 1366, height: 728 } },
|
|
82
|
+
{ screen: { width: 1280, height: 800 }, viewport: { width: 1280, height: 760 } }
|
|
83
|
+
];
|
|
84
|
+
var GPU_PROFILES = [
|
|
85
|
+
{ vendor: "Google Inc. (NVIDIA)", renderer: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)" },
|
|
86
|
+
{ vendor: "Google Inc. (Intel)", renderer: "ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)" },
|
|
87
|
+
{ vendor: "Google Inc. (AMD)", renderer: "ANGLE (AMD, AMD Radeon RX 6600 Direct3D11 vs_5_0 ps_5_0, D3D11)" },
|
|
88
|
+
{ vendor: "Apple Inc.", renderer: "Apple M1" },
|
|
89
|
+
{ vendor: "Apple Inc.", renderer: "Apple M2" }
|
|
90
|
+
];
|
|
91
|
+
var TIMEZONES = ["Asia/Jakarta", "Asia/Singapore", "America/New_York", "Europe/London", "Asia/Tokyo"];
|
|
92
|
+
var LOCALES = ["en-US", "en-GB", "id-ID"];
|
|
93
|
+
function pick(arr) {
|
|
94
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
95
|
+
}
|
|
96
|
+
function generateFingerprint(overrides = {}) {
|
|
97
|
+
const isMac = Math.random() < 0.3;
|
|
98
|
+
const chromeVersion = pick(CHROME_VERSIONS);
|
|
99
|
+
const chromeMajorVersion = chromeVersion.split(".")[0];
|
|
100
|
+
const gpu = pick(GPU_PROFILES.filter((g) => isMac ? g.vendor === "Apple Inc." : g.vendor !== "Apple Inc."));
|
|
101
|
+
const resolution = pick(RESOLUTIONS);
|
|
102
|
+
const profile = {
|
|
103
|
+
userAgent: isMac ? MAC_UA_TEMPLATE(chromeVersion) : WINDOWS_UA_TEMPLATE(chromeVersion),
|
|
104
|
+
chromeVersion,
|
|
105
|
+
chromeMajorVersion,
|
|
106
|
+
platform: isMac ? "MacIntel" : "Win32",
|
|
107
|
+
viewport: { ...resolution.viewport, deviceScaleFactor: pick([1, 1.25, 1.5]) },
|
|
108
|
+
screen: resolution.screen,
|
|
109
|
+
hardwareConcurrency: pick([4, 8, 12, 16]),
|
|
110
|
+
deviceMemory: pick([4, 8, 16]),
|
|
111
|
+
timezone: pick(TIMEZONES),
|
|
112
|
+
locale: pick(LOCALES),
|
|
113
|
+
webglVendor: gpu.vendor,
|
|
114
|
+
webglRenderer: gpu.renderer,
|
|
115
|
+
languages: ["en-US", "en"],
|
|
116
|
+
...overrides
|
|
117
|
+
};
|
|
118
|
+
return profile;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/fingerprint/index.ts
|
|
122
|
+
async function applyFingerprint(page, profile) {
|
|
123
|
+
await page.setUserAgent(profile.userAgent);
|
|
124
|
+
await page.setViewport(profile.viewport);
|
|
125
|
+
const client = await page.target().createCDPSession();
|
|
126
|
+
await client.send("Emulation.setTimezoneOverride", { timezoneId: profile.timezone });
|
|
127
|
+
await client.send("Emulation.setLocaleOverride", { locale: profile.locale });
|
|
128
|
+
await client.send("Network.setUserAgentOverride", {
|
|
129
|
+
userAgent: profile.userAgent,
|
|
130
|
+
acceptLanguage: profile.languages.join(","),
|
|
131
|
+
platform: profile.platform,
|
|
132
|
+
userAgentMetadata: {
|
|
133
|
+
brands: [
|
|
134
|
+
{ brand: "Not/A)Brand", version: "99" },
|
|
135
|
+
{ brand: "Google Chrome", version: profile.chromeMajorVersion },
|
|
136
|
+
{ brand: "Chromium", version: profile.chromeMajorVersion }
|
|
137
|
+
],
|
|
138
|
+
fullVersionList: [
|
|
139
|
+
{ brand: "Not/A)Brand", version: "99.0.0.0" },
|
|
140
|
+
{ brand: "Google Chrome", version: profile.chromeVersion },
|
|
141
|
+
{ brand: "Chromium", version: profile.chromeVersion }
|
|
142
|
+
],
|
|
143
|
+
fullVersion: profile.chromeVersion,
|
|
144
|
+
platform: profile.platform === "MacIntel" ? "macOS" : "Windows",
|
|
145
|
+
platformVersion: profile.platform === "MacIntel" ? "13.0.0" : "10.0.0",
|
|
146
|
+
architecture: "x86",
|
|
147
|
+
model: "",
|
|
148
|
+
mobile: false
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
await page.setExtraHTTPHeaders({
|
|
152
|
+
"Accept-Language": `${profile.languages[0]};q=0.9,${profile.languages.slice(1).join(";q=0.8,")}`.replace(/;q=0\.8,$/, "")
|
|
153
|
+
});
|
|
154
|
+
await page.evaluateOnNewDocument((p) => {
|
|
155
|
+
Object.defineProperty(navigator, "platform", { get: () => p.platform });
|
|
156
|
+
Object.defineProperty(navigator, "hardwareConcurrency", { get: () => p.hardwareConcurrency });
|
|
157
|
+
Object.defineProperty(navigator, "deviceMemory", { get: () => p.deviceMemory });
|
|
158
|
+
Object.defineProperty(navigator, "languages", { get: () => p.languages });
|
|
159
|
+
Object.defineProperty(screen, "width", { get: () => p.screen.width });
|
|
160
|
+
Object.defineProperty(screen, "height", { get: () => p.screen.height });
|
|
161
|
+
Object.defineProperty(screen, "availWidth", { get: () => p.screen.width });
|
|
162
|
+
Object.defineProperty(screen, "availHeight", { get: () => p.screen.height - 40 });
|
|
163
|
+
Object.defineProperty(window, "outerWidth", { get: () => p.screen.width });
|
|
164
|
+
Object.defineProperty(window, "outerHeight", { get: () => p.screen.height });
|
|
165
|
+
Object.defineProperty(window, "devicePixelRatio", { get: () => p.viewport.deviceScaleFactor });
|
|
166
|
+
const patchGetParameter = (proto) => {
|
|
167
|
+
const original = proto.getParameter;
|
|
168
|
+
proto.getParameter = function(parameter) {
|
|
169
|
+
const UNMASKED_VENDOR_WEBGL = 37445;
|
|
170
|
+
const UNMASKED_RENDERER_WEBGL = 37446;
|
|
171
|
+
if (parameter === UNMASKED_VENDOR_WEBGL) return p.webglVendor;
|
|
172
|
+
if (parameter === UNMASKED_RENDERER_WEBGL) return p.webglRenderer;
|
|
173
|
+
return original.call(this, parameter);
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
patchGetParameter(WebGLRenderingContext.prototype);
|
|
177
|
+
if (typeof WebGL2RenderingContext !== "undefined") {
|
|
178
|
+
patchGetParameter(WebGL2RenderingContext.prototype);
|
|
179
|
+
}
|
|
180
|
+
const toDataURLProto = HTMLCanvasElement.prototype.toDataURL;
|
|
181
|
+
HTMLCanvasElement.prototype.toDataURL = function(...args) {
|
|
182
|
+
const ctx = this.getContext("2d");
|
|
183
|
+
if (ctx) {
|
|
184
|
+
const imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
185
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
186
|
+
imageData.data[i] = imageData.data[i] ^ 1;
|
|
187
|
+
}
|
|
188
|
+
ctx.putImageData(imageData, 0, 0);
|
|
189
|
+
}
|
|
190
|
+
return toDataURLProto.apply(this, args);
|
|
191
|
+
};
|
|
192
|
+
const origCreateAnalyser = AudioContext.prototype.createAnalyser;
|
|
193
|
+
AudioContext.prototype.createAnalyser = function() {
|
|
194
|
+
const analyser = origCreateAnalyser.call(this);
|
|
195
|
+
const origGetFloatFrequencyData = analyser.getFloatFrequencyData.bind(analyser);
|
|
196
|
+
analyser.getFloatFrequencyData = ((array) => {
|
|
197
|
+
origGetFloatFrequencyData(array);
|
|
198
|
+
const typedArray = array;
|
|
199
|
+
for (let i = 0; i < typedArray.length; i++) typedArray[i] += Math.random() * 1e-4;
|
|
200
|
+
});
|
|
201
|
+
return analyser;
|
|
202
|
+
};
|
|
203
|
+
const nav = navigator;
|
|
204
|
+
if (nav.getBattery) {
|
|
205
|
+
nav.getBattery = () => Promise.resolve({
|
|
206
|
+
charging: true,
|
|
207
|
+
chargingTime: 0,
|
|
208
|
+
dischargingTime: Infinity,
|
|
209
|
+
level: 1,
|
|
210
|
+
addEventListener: () => {
|
|
211
|
+
},
|
|
212
|
+
removeEventListener: () => {
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const navConn = navigator;
|
|
217
|
+
if (navConn.connection) {
|
|
218
|
+
Object.defineProperty(navConn, "connection", {
|
|
219
|
+
get: () => ({
|
|
220
|
+
effectiveType: "4g",
|
|
221
|
+
rtt: 50,
|
|
222
|
+
downlink: 10,
|
|
223
|
+
saveData: false,
|
|
224
|
+
addEventListener: () => {
|
|
225
|
+
},
|
|
226
|
+
removeEventListener: () => {
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}, profile);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/cursor/ghostCursor.ts
|
|
235
|
+
function bezierPath(start, end, steps) {
|
|
236
|
+
const c1 = {
|
|
237
|
+
x: start.x + (end.x - start.x) * (0.3 + Math.random() * 0.2) + (Math.random() - 0.5) * 100,
|
|
238
|
+
y: start.y + (end.y - start.y) * (0.1 + Math.random() * 0.3) + (Math.random() - 0.5) * 100
|
|
239
|
+
};
|
|
240
|
+
const c2 = {
|
|
241
|
+
x: start.x + (end.x - start.x) * (0.6 + Math.random() * 0.2) + (Math.random() - 0.5) * 100,
|
|
242
|
+
y: start.y + (end.y - start.y) * (0.7 + Math.random() * 0.2) + (Math.random() - 0.5) * 100
|
|
243
|
+
};
|
|
244
|
+
const points = [];
|
|
245
|
+
for (let i = 0; i <= steps; i++) {
|
|
246
|
+
const t = i / steps;
|
|
247
|
+
const x = Math.pow(1 - t, 3) * start.x + 3 * Math.pow(1 - t, 2) * t * c1.x + 3 * (1 - t) * Math.pow(t, 2) * c2.x + Math.pow(t, 3) * end.x;
|
|
248
|
+
const y = Math.pow(1 - t, 3) * start.y + 3 * Math.pow(1 - t, 2) * t * c1.y + 3 * (1 - t) * Math.pow(t, 2) * c2.y + Math.pow(t, 3) * end.y;
|
|
249
|
+
points.push({ x, y });
|
|
250
|
+
}
|
|
251
|
+
return points;
|
|
252
|
+
}
|
|
253
|
+
function easeInOutQuad(t) {
|
|
254
|
+
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
255
|
+
}
|
|
256
|
+
var lastPosition = { x: 100, y: 100 };
|
|
257
|
+
async function moveMouseHuman(page, target) {
|
|
258
|
+
const steps = 20 + Math.floor(Math.random() * 15);
|
|
259
|
+
const path = bezierPath(lastPosition, target, steps);
|
|
260
|
+
for (let i = 0; i < path.length; i++) {
|
|
261
|
+
const eased = easeInOutQuad(i / path.length);
|
|
262
|
+
const idx = Math.min(path.length - 1, Math.floor(eased * path.length));
|
|
263
|
+
const p = path[idx];
|
|
264
|
+
await page.mouse.move(p.x, p.y);
|
|
265
|
+
await new Promise((r) => setTimeout(r, 3 + Math.random() * 10));
|
|
266
|
+
}
|
|
267
|
+
lastPosition = target;
|
|
268
|
+
}
|
|
269
|
+
async function ghostClick(page, selector) {
|
|
270
|
+
const handle = await page.waitForSelector(selector, { timeout: 15e3 });
|
|
271
|
+
if (!handle) throw new Error(`real-browser-plus: element not found for selector "${selector}"`);
|
|
272
|
+
const box = await handle.boundingBox();
|
|
273
|
+
if (!box) throw new Error(`real-browser-plus: element "${selector}" has no bounding box (is it visible?)`);
|
|
274
|
+
const target = {
|
|
275
|
+
x: box.x + box.width * (0.3 + Math.random() * 0.4),
|
|
276
|
+
y: box.y + box.height * (0.3 + Math.random() * 0.4)
|
|
277
|
+
};
|
|
278
|
+
await moveMouseHuman(page, target);
|
|
279
|
+
await new Promise((r) => setTimeout(r, 50 + Math.random() * 150));
|
|
280
|
+
await page.mouse.down();
|
|
281
|
+
await new Promise((r) => setTimeout(r, 30 + Math.random() * 90));
|
|
282
|
+
await page.mouse.up();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/humanize/typing.ts
|
|
286
|
+
var NEARBY_KEYS = {
|
|
287
|
+
a: ["s", "q", "z"],
|
|
288
|
+
s: ["a", "d", "w"],
|
|
289
|
+
e: ["w", "r", "d"],
|
|
290
|
+
i: ["u", "o", "k"],
|
|
291
|
+
o: ["i", "p", "l"],
|
|
292
|
+
n: ["b", "m", "h"]
|
|
293
|
+
};
|
|
294
|
+
function randomDelay(min, max) {
|
|
295
|
+
return min + Math.random() * (max - min);
|
|
296
|
+
}
|
|
297
|
+
async function humanType(page, selector, text) {
|
|
298
|
+
await ghostClick(page, selector);
|
|
299
|
+
await new Promise((r) => setTimeout(r, randomDelay(200, 600)));
|
|
300
|
+
for (const char of text) {
|
|
301
|
+
const lower = char.toLowerCase();
|
|
302
|
+
const nearby = NEARBY_KEYS[lower];
|
|
303
|
+
const shouldTypo = Math.random() < 0.04 && nearby;
|
|
304
|
+
if (shouldTypo) {
|
|
305
|
+
const wrongChar = nearby[Math.floor(Math.random() * nearby.length)];
|
|
306
|
+
await page.keyboard.type(wrongChar, { delay: randomDelay(60, 180) });
|
|
307
|
+
await new Promise((r) => setTimeout(r, randomDelay(150, 350)));
|
|
308
|
+
await page.keyboard.press("Backspace");
|
|
309
|
+
await new Promise((r) => setTimeout(r, randomDelay(80, 200)));
|
|
310
|
+
}
|
|
311
|
+
await page.keyboard.type(char, { delay: randomDelay(60, 180) });
|
|
312
|
+
if (Math.random() < 0.06) {
|
|
313
|
+
await new Promise((r) => setTimeout(r, randomDelay(300, 900)));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/humanize/scroll.ts
|
|
319
|
+
async function humanScroll(page, distance) {
|
|
320
|
+
const direction = distance > 0 ? 1 : -1;
|
|
321
|
+
let remaining = Math.abs(distance);
|
|
322
|
+
while (remaining > 0) {
|
|
323
|
+
const step = Math.min(remaining, 60 + Math.random() * 120);
|
|
324
|
+
await page.mouse.wheel({ deltaY: direction * step });
|
|
325
|
+
remaining -= step;
|
|
326
|
+
const delay = Math.random() < 0.15 ? 400 + Math.random() * 800 : 30 + Math.random() * 90;
|
|
327
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/captcha/turnstile.ts
|
|
332
|
+
var TURNSTILE_IFRAME_SELECTOR = 'iframe[src*="challenges.cloudflare.com"]';
|
|
333
|
+
var TURNSTILE_TIMEOUT_MS = 2e4;
|
|
334
|
+
async function solveTurnstile(page) {
|
|
335
|
+
let frameHandle;
|
|
336
|
+
try {
|
|
337
|
+
frameHandle = await page.waitForSelector(TURNSTILE_IFRAME_SELECTOR, { timeout: 4e3 });
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
if (!frameHandle) return false;
|
|
342
|
+
const frame = await frameHandle.contentFrame();
|
|
343
|
+
if (!frame) return false;
|
|
344
|
+
const checkbox = await frame.waitForSelector('input[type="checkbox"], #challenge-stage', { timeout: TURNSTILE_TIMEOUT_MS }).catch(() => null);
|
|
345
|
+
if (!checkbox) return false;
|
|
346
|
+
const box = await checkbox.boundingBox();
|
|
347
|
+
if (!box) return false;
|
|
348
|
+
const frameElement = frameHandle.asElement();
|
|
349
|
+
const parentFrameBox = frameElement ? await frameElement.boundingBox() : null;
|
|
350
|
+
const offsetX = parentFrameBox?.x ?? 0;
|
|
351
|
+
const offsetY = parentFrameBox?.y ?? 0;
|
|
352
|
+
await moveMouseHuman(page, {
|
|
353
|
+
x: offsetX + box.x + box.width / 2,
|
|
354
|
+
y: offsetY + box.y + box.height / 2
|
|
355
|
+
});
|
|
356
|
+
await new Promise((r) => setTimeout(r, 150 + Math.random() * 250));
|
|
357
|
+
await page.mouse.down();
|
|
358
|
+
await new Promise((r) => setTimeout(r, 40 + Math.random() * 80));
|
|
359
|
+
await page.mouse.up();
|
|
360
|
+
await page.waitForSelector(TURNSTILE_IFRAME_SELECTOR, { hidden: true, timeout: TURNSTILE_TIMEOUT_MS }).catch(() => null);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
function watchForTurnstile(page) {
|
|
364
|
+
page.on("framenavigated", async () => {
|
|
365
|
+
try {
|
|
366
|
+
await solveTurnstile(page);
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/proxy/index.ts
|
|
373
|
+
function resolveProxy(proxy) {
|
|
374
|
+
if (!proxy) return void 0;
|
|
375
|
+
if (Array.isArray(proxy)) return proxy[Math.floor(Math.random() * proxy.length)];
|
|
376
|
+
return proxy;
|
|
377
|
+
}
|
|
378
|
+
function proxyLaunchArg(proxy) {
|
|
379
|
+
const server = proxy.server.startsWith("http") ? proxy.server : `http://${proxy.server}`;
|
|
380
|
+
return `--proxy-server=${server}`;
|
|
381
|
+
}
|
|
382
|
+
async function applyProxyAuth(page, proxy) {
|
|
383
|
+
if (!proxy.username || !proxy.password) return;
|
|
384
|
+
await page.authenticate({ username: proxy.username, password: proxy.password });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/webrtc/index.ts
|
|
388
|
+
async function blockWebRTCLeak(page) {
|
|
389
|
+
await page.evaluateOnNewDocument(() => {
|
|
390
|
+
const OriginalRTCPeerConnection = window.RTCPeerConnection;
|
|
391
|
+
if (!OriginalRTCPeerConnection) return;
|
|
392
|
+
class PatchedRTCPeerConnection extends OriginalRTCPeerConnection {
|
|
393
|
+
constructor(config) {
|
|
394
|
+
const sanitizedConfig = { ...config, iceServers: [] };
|
|
395
|
+
super(sanitizedConfig);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
Object.defineProperty(window, "RTCPeerConnection", {
|
|
399
|
+
value: PatchedRTCPeerConnection,
|
|
400
|
+
writable: false,
|
|
401
|
+
configurable: true
|
|
402
|
+
});
|
|
403
|
+
Object.defineProperty(window, "webkitRTCPeerConnection", {
|
|
404
|
+
value: PatchedRTCPeerConnection,
|
|
405
|
+
writable: false,
|
|
406
|
+
configurable: true
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/session/index.ts
|
|
412
|
+
async function saveSession(page) {
|
|
413
|
+
const cookies = await page.cookies();
|
|
414
|
+
const storageSnapshot = await page.evaluate(() => {
|
|
415
|
+
const local = {};
|
|
416
|
+
const session = {};
|
|
417
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
418
|
+
const key = window.localStorage.key(i);
|
|
419
|
+
if (key !== null) local[key] = window.localStorage.getItem(key) ?? "";
|
|
420
|
+
}
|
|
421
|
+
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
422
|
+
const key = window.sessionStorage.key(i);
|
|
423
|
+
if (key !== null) session[key] = window.sessionStorage.getItem(key) ?? "";
|
|
424
|
+
}
|
|
425
|
+
return { local, session, origin: window.location.origin };
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
cookies,
|
|
429
|
+
localStorage: storageSnapshot.local,
|
|
430
|
+
sessionStorage: storageSnapshot.session,
|
|
431
|
+
origin: storageSnapshot.origin
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async function restoreSession(page, session) {
|
|
435
|
+
if (session.cookies.length > 0) {
|
|
436
|
+
await page.setCookie(...session.cookies);
|
|
437
|
+
}
|
|
438
|
+
if (session.origin) {
|
|
439
|
+
await page.evaluateOnNewDocument(
|
|
440
|
+
(data) => {
|
|
441
|
+
for (const [key, value] of Object.entries(data.localStorage)) {
|
|
442
|
+
window.localStorage.setItem(key, value);
|
|
443
|
+
}
|
|
444
|
+
for (const [key, value] of Object.entries(data.sessionStorage)) {
|
|
445
|
+
window.sessionStorage.setItem(key, value);
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
{ localStorage: session.localStorage, sessionStorage: session.sessionStorage }
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/retry/index.ts
|
|
454
|
+
async function withRetry(fn, options = {}) {
|
|
455
|
+
const { retries = 3, minDelayMs = 300, maxDelayMs = 3e3, onRetry } = options;
|
|
456
|
+
let lastError;
|
|
457
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
458
|
+
try {
|
|
459
|
+
return await fn();
|
|
460
|
+
} catch (error) {
|
|
461
|
+
lastError = error;
|
|
462
|
+
if (attempt === retries) break;
|
|
463
|
+
onRetry?.(attempt + 1, error);
|
|
464
|
+
const exponential = Math.min(maxDelayMs, minDelayMs * Math.pow(2, attempt));
|
|
465
|
+
const jitter = exponential * (0.5 + Math.random() * 0.5);
|
|
466
|
+
await new Promise((r) => setTimeout(r, jitter));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
throw lastError;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/logger/index.ts
|
|
473
|
+
var LEVEL_WEIGHT = {
|
|
474
|
+
silent: 0,
|
|
475
|
+
error: 1,
|
|
476
|
+
warn: 2,
|
|
477
|
+
info: 3,
|
|
478
|
+
debug: 4
|
|
479
|
+
};
|
|
480
|
+
var Logger = class {
|
|
481
|
+
constructor(level = "warn") {
|
|
482
|
+
this.level = level;
|
|
483
|
+
}
|
|
484
|
+
setLevel(level) {
|
|
485
|
+
this.level = level;
|
|
486
|
+
}
|
|
487
|
+
shouldLog(target) {
|
|
488
|
+
return LEVEL_WEIGHT[this.level] >= LEVEL_WEIGHT[target];
|
|
489
|
+
}
|
|
490
|
+
error(...args) {
|
|
491
|
+
if (this.shouldLog("error")) console.error("[real-browser-plus]", ...args);
|
|
492
|
+
}
|
|
493
|
+
warn(...args) {
|
|
494
|
+
if (this.shouldLog("warn")) console.warn("[real-browser-plus]", ...args);
|
|
495
|
+
}
|
|
496
|
+
info(...args) {
|
|
497
|
+
if (this.shouldLog("info")) console.info("[real-browser-plus]", ...args);
|
|
498
|
+
}
|
|
499
|
+
debug(...args) {
|
|
500
|
+
if (this.shouldLog("debug")) console.debug("[real-browser-plus]", ...args);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
var logger = new Logger();
|
|
504
|
+
|
|
505
|
+
// src/plugins/index.ts
|
|
506
|
+
var PluginManager = class {
|
|
507
|
+
constructor() {
|
|
508
|
+
this.plugins = [];
|
|
509
|
+
}
|
|
510
|
+
register(plugin) {
|
|
511
|
+
this.plugins.push(plugin);
|
|
512
|
+
}
|
|
513
|
+
async runBrowserLaunched(browser) {
|
|
514
|
+
for (const plugin of this.plugins) {
|
|
515
|
+
await plugin.onBrowserLaunched?.(browser);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async runPageCreated(page) {
|
|
519
|
+
for (const plugin of this.plugins) {
|
|
520
|
+
await plugin.onPageCreated?.(page);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async runBeforeClose(browser) {
|
|
524
|
+
for (const plugin of this.plugins) {
|
|
525
|
+
await plugin.onBeforeClose?.(browser);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/index.ts
|
|
531
|
+
puppeteerExtra.use(StealthPlugin());
|
|
532
|
+
async function connect(options = {}) {
|
|
533
|
+
const {
|
|
534
|
+
headless = false,
|
|
535
|
+
args = [],
|
|
536
|
+
executablePath,
|
|
537
|
+
proxy,
|
|
538
|
+
turnstile = true,
|
|
539
|
+
fingerprint: fingerprintOverrides,
|
|
540
|
+
disableFingerprinting = false,
|
|
541
|
+
ghostCursor = true,
|
|
542
|
+
blockWebRTCLeak: shouldBlockWebRTC = true,
|
|
543
|
+
connectToExisting,
|
|
544
|
+
customConfig = {},
|
|
545
|
+
ignoreAllFlags = false,
|
|
546
|
+
plugins = [],
|
|
547
|
+
logLevel = "warn",
|
|
548
|
+
session
|
|
549
|
+
} = options;
|
|
550
|
+
logger.setLevel(logLevel);
|
|
551
|
+
const pluginManager = new PluginManager();
|
|
552
|
+
for (const plugin of plugins) pluginManager.register(plugin);
|
|
553
|
+
const chosenProxy = resolveProxy(proxy);
|
|
554
|
+
const launchArgs = ignoreAllFlags ? args : [...STEALTH_LAUNCH_ARGS, ...args, ...chosenProxy ? [proxyLaunchArg(chosenProxy)] : []];
|
|
555
|
+
let browser;
|
|
556
|
+
if (connectToExisting) {
|
|
557
|
+
browser = await puppeteerExtra.connect(connectToExisting);
|
|
558
|
+
logger.info("Attached to existing browser instance");
|
|
559
|
+
} else {
|
|
560
|
+
browser = await puppeteerExtra.launch({
|
|
561
|
+
headless,
|
|
562
|
+
args: launchArgs,
|
|
563
|
+
executablePath,
|
|
564
|
+
defaultViewport: null,
|
|
565
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
566
|
+
...customConfig
|
|
567
|
+
});
|
|
568
|
+
logger.info("Launched new browser instance", { headless, proxy: Boolean(chosenProxy) });
|
|
569
|
+
}
|
|
570
|
+
await pluginManager.runBrowserLaunched(browser);
|
|
571
|
+
const page = await browser.newPage();
|
|
572
|
+
if (chosenProxy) {
|
|
573
|
+
await applyProxyAuth(page, chosenProxy);
|
|
574
|
+
logger.debug("Proxy authentication applied");
|
|
575
|
+
}
|
|
576
|
+
if (!ignoreAllFlags) {
|
|
577
|
+
await applyAllStealth(page);
|
|
578
|
+
}
|
|
579
|
+
if (shouldBlockWebRTC && !ignoreAllFlags) {
|
|
580
|
+
await blockWebRTCLeak(page);
|
|
581
|
+
}
|
|
582
|
+
const fingerprint = generateFingerprint(fingerprintOverrides);
|
|
583
|
+
if (!disableFingerprinting && !ignoreAllFlags) {
|
|
584
|
+
await applyFingerprint(page, fingerprint);
|
|
585
|
+
logger.debug("Fingerprint applied", fingerprint);
|
|
586
|
+
}
|
|
587
|
+
if (session) {
|
|
588
|
+
await restoreSession(page, session);
|
|
589
|
+
logger.debug("Session restored", { origin: session.origin });
|
|
590
|
+
}
|
|
591
|
+
if (turnstile) {
|
|
592
|
+
watchForTurnstile(page);
|
|
593
|
+
solveTurnstile(page).catch((error) => logger.debug("Initial turnstile check skipped", error));
|
|
594
|
+
}
|
|
595
|
+
await pluginManager.runPageCreated(page);
|
|
596
|
+
const humanClick = async (targetPage, selector) => {
|
|
597
|
+
if (ghostCursor) {
|
|
598
|
+
await ghostClick(targetPage, selector);
|
|
599
|
+
} else {
|
|
600
|
+
await targetPage.click(selector);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const use = async (plugin) => {
|
|
604
|
+
pluginManager.register(plugin);
|
|
605
|
+
await plugin.onPageCreated?.(page);
|
|
606
|
+
};
|
|
607
|
+
browser.on("disconnected", () => {
|
|
608
|
+
logger.info("Browser disconnected");
|
|
609
|
+
});
|
|
610
|
+
const originalClose = browser.close.bind(browser);
|
|
611
|
+
browser.close = (async () => {
|
|
612
|
+
await pluginManager.runBeforeClose(browser);
|
|
613
|
+
return originalClose();
|
|
614
|
+
});
|
|
615
|
+
return {
|
|
616
|
+
browser,
|
|
617
|
+
page,
|
|
618
|
+
fingerprint,
|
|
619
|
+
humanClick,
|
|
620
|
+
humanType,
|
|
621
|
+
humanScroll,
|
|
622
|
+
solveTurnstile,
|
|
623
|
+
saveSession,
|
|
624
|
+
restoreSession,
|
|
625
|
+
withRetry,
|
|
626
|
+
use
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
var index_default = { connect };
|
|
630
|
+
export {
|
|
631
|
+
Logger,
|
|
632
|
+
blockWebRTCLeak,
|
|
633
|
+
connect,
|
|
634
|
+
index_default as default,
|
|
635
|
+
generateFingerprint,
|
|
636
|
+
ghostClick,
|
|
637
|
+
humanScroll,
|
|
638
|
+
humanType,
|
|
639
|
+
logger,
|
|
640
|
+
moveMouseHuman,
|
|
641
|
+
restoreSession,
|
|
642
|
+
saveSession,
|
|
643
|
+
solveTurnstile,
|
|
644
|
+
withRetry
|
|
645
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "real-browser-plus",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A modernized, more powerful successor to puppeteer-real-browser — stealth browser automation with fingerprint spoofing, ghost cursor, human-like typing, auto Turnstile handling, and proxy rotation.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "node test/basic.test.js",
|
|
23
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"puppeteer",
|
|
27
|
+
"stealth",
|
|
28
|
+
"anti-detect",
|
|
29
|
+
"cloudflare-bypass",
|
|
30
|
+
"turnstile",
|
|
31
|
+
"fingerprint",
|
|
32
|
+
"ghost-cursor",
|
|
33
|
+
"browser-automation",
|
|
34
|
+
"scraping"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/fidzzcodex-me/real-browser-plus.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/fidzzcodex-me/real-browser-plus/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/fidzzcodex-me/real-browser-plus#readme",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"puppeteer": "^23.9.0",
|
|
51
|
+
"puppeteer-extra": "^3.3.6",
|
|
52
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"typescript": "^5.6.3",
|
|
56
|
+
"tsup": "^8.3.5",
|
|
57
|
+
"@types/node": "^22.9.0"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|