arn-browser 0.0.1 → 0.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/dist/__main__.d.ts +2 -0
- package/dist/__main__.js +127 -0
- package/dist/__version__.d.ts +11 -0
- package/dist/__version__.js +16 -0
- package/dist/addons.d.ts +17 -0
- package/dist/addons.js +70 -0
- package/dist/data-files/territoryInfo.xml +2024 -0
- package/dist/data-files/webgl_data.db +0 -0
- package/dist/exceptions.d.ts +76 -0
- package/dist/exceptions.js +153 -0
- package/dist/fingerprints.d.ts +4 -0
- package/dist/fingerprints.js +82 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/ip.d.ts +25 -0
- package/dist/ip.js +90 -0
- package/dist/locale.d.ts +26 -0
- package/dist/locale.js +280 -0
- package/dist/mappings/browserforge.config.d.ts +47 -0
- package/dist/mappings/browserforge.config.js +72 -0
- package/dist/mappings/fonts.config.d.ts +6 -0
- package/dist/mappings/fonts.config.js +822 -0
- package/dist/mappings/warnings.config.d.ts +16 -0
- package/dist/mappings/warnings.config.js +28 -0
- package/dist/pkgman.d.ts +62 -0
- package/dist/pkgman.js +347 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +9 -0
- package/dist/sync_api.d.ts +7 -0
- package/dist/sync_api.js +27 -0
- package/dist/utils.d.ts +88 -0
- package/dist/utils.js +500 -0
- package/dist/virtdisplay.d.ts +20 -0
- package/dist/virtdisplay.js +123 -0
- package/dist/warnings.d.ts +4 -0
- package/dist/warnings.js +30 -0
- package/dist/webgl/db-compat.d.ts +9 -0
- package/dist/webgl/db-compat.js +44 -0
- package/dist/webgl/sample.d.ts +19 -0
- package/dist/webgl/sample.js +85 -0
- package/package.json +2 -2
package/dist/utils.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// from browserforge.fingerprints import Fingerprint, Screen
|
|
2
|
+
// from screeninfo import get_monitors
|
|
3
|
+
// from ua_parser import user_agent_parser
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { UAParser } from "ua-parser-js";
|
|
7
|
+
import { addDefaultAddons, confirmPaths, } from "./addons.js";
|
|
8
|
+
import { InvalidOS, InvalidPropertyType, NonFirefoxFingerprint, UnknownProperty, } from "./exceptions.js";
|
|
9
|
+
import { fromBrowserforge, generateFingerprint, SUPPORTED_OS, } from "./fingerprints.js";
|
|
10
|
+
import { publicIP, validIPv4, validIPv6 } from "./ip.js";
|
|
11
|
+
import { geoipAllowed, getGeolocation, handleLocales } from "./locale.js";
|
|
12
|
+
import FONTS from "./mappings/fonts.config.js";
|
|
13
|
+
import { getPath, installedVerStr, launchPath, OS_NAME } from "./pkgman.js";
|
|
14
|
+
import { LeakWarning } from "./warnings.js";
|
|
15
|
+
import { sampleWebGL } from "./webgl/sample.js";
|
|
16
|
+
// Camoufox preferences to cache previous pages and requests
|
|
17
|
+
const CACHE_PREFS = {
|
|
18
|
+
"browser.sessionhistory.max_entries": 10,
|
|
19
|
+
"browser.sessionhistory.max_total_viewers": -1,
|
|
20
|
+
"browser.cache.memory.enable": true,
|
|
21
|
+
"browser.cache.disk_cache_ssl": true,
|
|
22
|
+
"browser.cache.disk.smart_size.enabled": true,
|
|
23
|
+
};
|
|
24
|
+
function getEnvVars(configMap, userAgentOS) {
|
|
25
|
+
const envVars = {};
|
|
26
|
+
let updatedConfigData;
|
|
27
|
+
try {
|
|
28
|
+
updatedConfigData = new TextEncoder().encode(JSON.stringify(configMap));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.error(`Error updating config: ${e}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const chunkSize = OS_NAME === "win" ? 2047 : 32767;
|
|
35
|
+
const configStr = new TextDecoder().decode(updatedConfigData);
|
|
36
|
+
for (let i = 0; i < configStr.length; i += chunkSize) {
|
|
37
|
+
const chunk = configStr.slice(i, i + chunkSize);
|
|
38
|
+
const envName = `CAMOU_CONFIG_${Math.floor(i / chunkSize) + 1}`;
|
|
39
|
+
try {
|
|
40
|
+
envVars[envName] = chunk;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.error(`Error setting ${envName}: ${e}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (OS_NAME === "lin") {
|
|
48
|
+
const fontconfigPath = getPath(path.join("fontconfig", userAgentOS));
|
|
49
|
+
envVars.FONTCONFIG_PATH = fontconfigPath;
|
|
50
|
+
}
|
|
51
|
+
return envVars;
|
|
52
|
+
}
|
|
53
|
+
export function getAsBooleanFromENV(name, defaultValue) {
|
|
54
|
+
const value = process.env[name];
|
|
55
|
+
if (value === "false" || value === "0")
|
|
56
|
+
return false;
|
|
57
|
+
if (value)
|
|
58
|
+
return true;
|
|
59
|
+
return !!defaultValue;
|
|
60
|
+
}
|
|
61
|
+
function loadProperties(filePath) {
|
|
62
|
+
let propFile;
|
|
63
|
+
filePath = filePath?.toString();
|
|
64
|
+
if (filePath) {
|
|
65
|
+
propFile = path.join(path.dirname(filePath), "properties.json");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
propFile = getPath("properties.json");
|
|
69
|
+
}
|
|
70
|
+
const propData = readFileSync(propFile).toString();
|
|
71
|
+
const propDict = JSON.parse(propData);
|
|
72
|
+
return propDict.reduce((acc, prop) => {
|
|
73
|
+
acc[prop.property] = prop.type;
|
|
74
|
+
return acc;
|
|
75
|
+
}, {});
|
|
76
|
+
}
|
|
77
|
+
function validateConfig(configMap, path) {
|
|
78
|
+
const propertyTypes = loadProperties(path);
|
|
79
|
+
for (const [key, value] of Object.entries(configMap)) {
|
|
80
|
+
const expectedType = propertyTypes[key];
|
|
81
|
+
if (!expectedType) {
|
|
82
|
+
throw new UnknownProperty(`Unknown property ${key} in config`);
|
|
83
|
+
}
|
|
84
|
+
if (!validateType(value, expectedType)) {
|
|
85
|
+
throw new InvalidPropertyType(`Invalid type for property ${key}. Expected ${expectedType}, got ${typeof value}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function validateType(value, expectedType) {
|
|
90
|
+
switch (expectedType) {
|
|
91
|
+
case "str":
|
|
92
|
+
return typeof value === "string";
|
|
93
|
+
case "int":
|
|
94
|
+
return Number.isInteger(value);
|
|
95
|
+
case "uint":
|
|
96
|
+
return Number.isInteger(value) && value >= 0;
|
|
97
|
+
case "double":
|
|
98
|
+
return typeof value === "number";
|
|
99
|
+
case "bool":
|
|
100
|
+
return typeof value === "boolean";
|
|
101
|
+
case "array":
|
|
102
|
+
return Array.isArray(value);
|
|
103
|
+
case "dict":
|
|
104
|
+
return (typeof value === "object" && value !== null && !Array.isArray(value));
|
|
105
|
+
default:
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function getTargetOS(config) {
|
|
110
|
+
if (config["navigator.userAgent"]) {
|
|
111
|
+
return determineUAOS(config["navigator.userAgent"]);
|
|
112
|
+
}
|
|
113
|
+
return OS_NAME;
|
|
114
|
+
}
|
|
115
|
+
function determineUAOS(userAgent) {
|
|
116
|
+
const parser = new UAParser(userAgent);
|
|
117
|
+
const parsedUA = parser.getOS().name;
|
|
118
|
+
if (!parsedUA) {
|
|
119
|
+
throw new Error("Could not determine OS from user agent");
|
|
120
|
+
}
|
|
121
|
+
if (parsedUA.startsWith("macOS")) {
|
|
122
|
+
return "mac";
|
|
123
|
+
}
|
|
124
|
+
if (parsedUA.startsWith("Windows")) {
|
|
125
|
+
return "win";
|
|
126
|
+
}
|
|
127
|
+
return "lin";
|
|
128
|
+
}
|
|
129
|
+
function getScreenCons(headless) {
|
|
130
|
+
if (headless === false) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
// TODO - Implement getMonitors
|
|
134
|
+
// try {
|
|
135
|
+
// const monitors = getMonitors();
|
|
136
|
+
// if (!monitors.length) {
|
|
137
|
+
// return undefined;
|
|
138
|
+
// }
|
|
139
|
+
// const monitor = monitors.reduce((prev, curr) => (prev.width * prev.height > curr.width * curr.height ? prev : curr));
|
|
140
|
+
// return { maxWidth: monitor.width, maxHeight: monitor.height };
|
|
141
|
+
// } catch {
|
|
142
|
+
// return undefined;
|
|
143
|
+
// }
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
function updateFonts(config, targetOS) {
|
|
147
|
+
const fonts = FONTS[targetOS];
|
|
148
|
+
if (config.fonts) {
|
|
149
|
+
config.fonts = Array.from(new Set([...fonts, ...config.fonts]));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
config.fonts = fonts;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function checkCustomFingerprint(fingerprint) {
|
|
156
|
+
const parser = new UAParser(fingerprint.navigator.userAgent);
|
|
157
|
+
const browserName = parser.getBrowser().name || "Non-Firefox";
|
|
158
|
+
if (browserName !== "Firefox") {
|
|
159
|
+
throw new NonFirefoxFingerprint(`"${browserName}" fingerprints are not supported in Camoufox. Using fingerprints from a browser other than Firefox WILL lead to detection. If this is intentional, pass i_know_what_im_doing=True.`);
|
|
160
|
+
}
|
|
161
|
+
LeakWarning.warn("custom_fingerprint", false);
|
|
162
|
+
}
|
|
163
|
+
function validateOS(os) {
|
|
164
|
+
if (!os)
|
|
165
|
+
return undefined;
|
|
166
|
+
if (Array.isArray(os)) {
|
|
167
|
+
os.every(validateOS);
|
|
168
|
+
return [...os];
|
|
169
|
+
}
|
|
170
|
+
if (!SUPPORTED_OS.includes(os)) {
|
|
171
|
+
throw new InvalidOS(`Camoufox does not support the OS: '${os}'`);
|
|
172
|
+
}
|
|
173
|
+
return [os];
|
|
174
|
+
}
|
|
175
|
+
function _cleanLocals(data) {
|
|
176
|
+
delete data.playwright;
|
|
177
|
+
delete data.persistentContext;
|
|
178
|
+
return data;
|
|
179
|
+
}
|
|
180
|
+
function mergeInto(target, source) {
|
|
181
|
+
Object.entries(source).forEach(([key, value]) => {
|
|
182
|
+
if (!(key in target)) {
|
|
183
|
+
target[key] = value;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function setInto(target, key, value) {
|
|
188
|
+
if (!(key in target)) {
|
|
189
|
+
target[key] = value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function isDomainSet(config, ...properties) {
|
|
193
|
+
return properties.some((prop) => {
|
|
194
|
+
if (prop.endsWith(".") || prop.endsWith(":")) {
|
|
195
|
+
return Object.keys(config).some((key) => key.startsWith(prop));
|
|
196
|
+
}
|
|
197
|
+
return prop in config;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function warnManualConfig(config) {
|
|
201
|
+
if (isDomainSet(config, "navigator.language", "navigator.languages", "headers.Accept-Language", "locale:")) {
|
|
202
|
+
LeakWarning.warn("locale", false);
|
|
203
|
+
}
|
|
204
|
+
if (isDomainSet(config, "geolocation:", "timezone")) {
|
|
205
|
+
LeakWarning.warn("geolocation", false);
|
|
206
|
+
}
|
|
207
|
+
if (isDomainSet(config, "headers.User-Agent")) {
|
|
208
|
+
LeakWarning.warn("header-ua", false);
|
|
209
|
+
}
|
|
210
|
+
if (isDomainSet(config, "navigator.")) {
|
|
211
|
+
LeakWarning.warn("navigator", false);
|
|
212
|
+
}
|
|
213
|
+
if (isDomainSet(config, "screen.", "window.", "document.body.")) {
|
|
214
|
+
LeakWarning.warn("viewport", false);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function _asyncAttachVD(browser, virtualDisplay) {
|
|
218
|
+
if (!virtualDisplay) {
|
|
219
|
+
return browser;
|
|
220
|
+
}
|
|
221
|
+
const originalClose = browser.close;
|
|
222
|
+
browser.close = async (...args) => {
|
|
223
|
+
await originalClose.apply(browser, ...args);
|
|
224
|
+
if (virtualDisplay) {
|
|
225
|
+
virtualDisplay.kill();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
browser._virtualDisplay = virtualDisplay;
|
|
229
|
+
return browser;
|
|
230
|
+
}
|
|
231
|
+
export function syncAttachVD(browser, virtualDisplay) {
|
|
232
|
+
/**
|
|
233
|
+
* Attaches the virtual display to the sync browser cleanup
|
|
234
|
+
*/
|
|
235
|
+
if (!virtualDisplay) {
|
|
236
|
+
// Skip if no virtual display is provided
|
|
237
|
+
return browser;
|
|
238
|
+
}
|
|
239
|
+
const originalClose = browser.close;
|
|
240
|
+
browser.close = (...args) => {
|
|
241
|
+
originalClose.apply(browser, ...args);
|
|
242
|
+
if (virtualDisplay) {
|
|
243
|
+
virtualDisplay.kill();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
browser._virtualDisplay = virtualDisplay;
|
|
247
|
+
return browser;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Convert a Playwright proxy string to a URL object.
|
|
251
|
+
*
|
|
252
|
+
* Implementation from https://github.com/microsoft/playwright/blob/3873b72ac1441ca691f7594f0ed705bd84518f93/packages/playwright-core/src/server/browserContext.ts#L737-L747
|
|
253
|
+
*/
|
|
254
|
+
function getProxyUrl(proxy) {
|
|
255
|
+
if (!proxy)
|
|
256
|
+
return null;
|
|
257
|
+
if (typeof proxy === "string") {
|
|
258
|
+
return new URL(proxy);
|
|
259
|
+
}
|
|
260
|
+
const { server, username, password } = proxy;
|
|
261
|
+
let url;
|
|
262
|
+
try {
|
|
263
|
+
// new URL('127.0.0.1:8080') throws
|
|
264
|
+
// new URL('localhost:8080') fails to parse host or protocol
|
|
265
|
+
// In both of these cases, we need to try re-parse URL with `http://` prefix.
|
|
266
|
+
url = new URL(server);
|
|
267
|
+
if (!url.host || !url.protocol)
|
|
268
|
+
url = new URL(`http://${server}`);
|
|
269
|
+
}
|
|
270
|
+
catch (_e) {
|
|
271
|
+
url = new URL(`http://${server}`);
|
|
272
|
+
}
|
|
273
|
+
if (username)
|
|
274
|
+
url.username = username;
|
|
275
|
+
if (password)
|
|
276
|
+
url.password = password;
|
|
277
|
+
return url;
|
|
278
|
+
}
|
|
279
|
+
export async function launchOptions({ config, os, block_images, block_webrtc, block_webgl, disable_coop, webgl_config, geoip, humanize, locale, addons, fonts, custom_fonts_only, exclude_addons, screen, window, fingerprint, ff_version, headless, main_world_eval, executable_path, firefox_user_prefs, proxy, enable_cache, args, env, i_know_what_im_doing, debug, virtual_display, ...launch_options }) {
|
|
280
|
+
// Build the config
|
|
281
|
+
if (!config) {
|
|
282
|
+
config = {};
|
|
283
|
+
}
|
|
284
|
+
// Set default values for optional arguments
|
|
285
|
+
if (headless === undefined) {
|
|
286
|
+
headless = false;
|
|
287
|
+
}
|
|
288
|
+
if (!addons) {
|
|
289
|
+
addons = [];
|
|
290
|
+
}
|
|
291
|
+
if (!args) {
|
|
292
|
+
args = [];
|
|
293
|
+
}
|
|
294
|
+
if (!firefox_user_prefs) {
|
|
295
|
+
firefox_user_prefs = {};
|
|
296
|
+
}
|
|
297
|
+
if (custom_fonts_only === undefined) {
|
|
298
|
+
custom_fonts_only = false;
|
|
299
|
+
}
|
|
300
|
+
if (i_know_what_im_doing === undefined) {
|
|
301
|
+
i_know_what_im_doing = false;
|
|
302
|
+
}
|
|
303
|
+
if (!env) {
|
|
304
|
+
env = process.env;
|
|
305
|
+
}
|
|
306
|
+
if (typeof executable_path === "string") {
|
|
307
|
+
// Convert executable path to a Path object
|
|
308
|
+
executable_path = path.resolve(executable_path);
|
|
309
|
+
}
|
|
310
|
+
// Handle virtual display
|
|
311
|
+
if (virtual_display) {
|
|
312
|
+
env.DISPLAY = virtual_display;
|
|
313
|
+
}
|
|
314
|
+
// Warn the user for manual config settings
|
|
315
|
+
if (!i_know_what_im_doing) {
|
|
316
|
+
warnManualConfig(config);
|
|
317
|
+
}
|
|
318
|
+
const operatingSystems = validateOS(os);
|
|
319
|
+
// webgl_config requires OS to be set
|
|
320
|
+
if (!operatingSystems && webgl_config) {
|
|
321
|
+
throw new Error("OS must be set when using webgl_config");
|
|
322
|
+
}
|
|
323
|
+
// Add the default addons
|
|
324
|
+
addDefaultAddons(addons, exclude_addons);
|
|
325
|
+
// Confirm all addon paths are valid
|
|
326
|
+
if (addons.length > 0) {
|
|
327
|
+
confirmPaths(addons);
|
|
328
|
+
config.addons = addons;
|
|
329
|
+
}
|
|
330
|
+
// Get the Firefox version
|
|
331
|
+
let ff_version_str;
|
|
332
|
+
if (ff_version) {
|
|
333
|
+
ff_version_str = ff_version.toString();
|
|
334
|
+
LeakWarning.warn("ff_version", i_know_what_im_doing);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
ff_version_str = installedVerStr().split(".", 1)[0];
|
|
338
|
+
}
|
|
339
|
+
// Generate a fingerprint
|
|
340
|
+
if (!fingerprint) {
|
|
341
|
+
fingerprint = generateFingerprint(window, {
|
|
342
|
+
screen: screen || getScreenCons(headless || "DISPLAY" in env),
|
|
343
|
+
operatingSystems,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// Or use the one passed by the user
|
|
348
|
+
if (!i_know_what_im_doing) {
|
|
349
|
+
checkCustomFingerprint(fingerprint);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Inject the fingerprint into the config
|
|
353
|
+
mergeInto(config, fromBrowserforge(fingerprint, ff_version_str));
|
|
354
|
+
const targetOS = getTargetOS(config);
|
|
355
|
+
// Set a random window.history.length
|
|
356
|
+
setInto(config, "window.history.length", Math.floor(Math.random() * 5) + 1);
|
|
357
|
+
// Update fonts list
|
|
358
|
+
if (fonts) {
|
|
359
|
+
config.fonts = fonts;
|
|
360
|
+
}
|
|
361
|
+
if (custom_fonts_only) {
|
|
362
|
+
firefox_user_prefs["gfx.bundled-fonts.activate"] = 0;
|
|
363
|
+
if (fonts) {
|
|
364
|
+
// The user has passed their own fonts, and OS fonts are disabled.
|
|
365
|
+
LeakWarning.warn("custom_fonts_only");
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// OS fonts are disabled, and the user has not passed their own fonts either.
|
|
369
|
+
throw new Error("No custom fonts were passed, but `custom_fonts_only` is enabled.");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
updateFonts(config, targetOS);
|
|
374
|
+
}
|
|
375
|
+
// Set a fixed font spacing seed
|
|
376
|
+
setInto(config, "fonts:spacing_seed", Math.floor(Math.random() * 1_073_741_824));
|
|
377
|
+
// Handle proxy
|
|
378
|
+
const proxyUrl = getProxyUrl(proxy);
|
|
379
|
+
// Set geolocation
|
|
380
|
+
if (geoip) {
|
|
381
|
+
geoipAllowed();
|
|
382
|
+
// Find the user's IP address
|
|
383
|
+
geoip = await publicIP(proxyUrl?.href);
|
|
384
|
+
// Spoof WebRTC if not blocked
|
|
385
|
+
if (!block_webrtc) {
|
|
386
|
+
if (validIPv4(geoip)) {
|
|
387
|
+
setInto(config, "webrtc:ipv4", geoip);
|
|
388
|
+
firefox_user_prefs["network.dns.disableIPv6"] = true;
|
|
389
|
+
}
|
|
390
|
+
else if (validIPv6(geoip)) {
|
|
391
|
+
setInto(config, "webrtc:ipv6", geoip);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const geolocation = await getGeolocation(geoip);
|
|
395
|
+
config = { ...config, ...geolocation.asConfig() };
|
|
396
|
+
}
|
|
397
|
+
// Raise a warning when a proxy is being used without spoofing geolocation.
|
|
398
|
+
// This is a very bad idea; the warning cannot be ignored with i_know_what_im_doing.
|
|
399
|
+
if (proxyUrl &&
|
|
400
|
+
!proxyUrl.hostname.includes("localhost") &&
|
|
401
|
+
!isDomainSet(config, "geolocation:")) {
|
|
402
|
+
LeakWarning.warn("proxy_without_geoip");
|
|
403
|
+
}
|
|
404
|
+
// Set locale
|
|
405
|
+
if (locale) {
|
|
406
|
+
handleLocales(locale, config);
|
|
407
|
+
}
|
|
408
|
+
// Pass the humanize option
|
|
409
|
+
if (humanize) {
|
|
410
|
+
setInto(config, "humanize", true);
|
|
411
|
+
if (typeof humanize === "number") {
|
|
412
|
+
setInto(config, "humanize:maxTime", humanize);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Enable the main world context creation
|
|
416
|
+
if (main_world_eval) {
|
|
417
|
+
setInto(config, "allowMainWorld", true);
|
|
418
|
+
}
|
|
419
|
+
// Set Firefox user preferences
|
|
420
|
+
if (block_images) {
|
|
421
|
+
LeakWarning.warn("block_images", i_know_what_im_doing);
|
|
422
|
+
firefox_user_prefs["permissions.default.image"] = 2;
|
|
423
|
+
}
|
|
424
|
+
if (block_webrtc) {
|
|
425
|
+
firefox_user_prefs["media.peerconnection.enabled"] = false;
|
|
426
|
+
}
|
|
427
|
+
if (disable_coop) {
|
|
428
|
+
LeakWarning.warn("disable_coop", i_know_what_im_doing);
|
|
429
|
+
firefox_user_prefs["browser.tabs.remote.useCrossOriginOpenerPolicy"] =
|
|
430
|
+
false;
|
|
431
|
+
}
|
|
432
|
+
// Allow allow_webgl parameter for backwards compatibility
|
|
433
|
+
if (block_webgl || launch_options.allow_webgl === false) {
|
|
434
|
+
firefox_user_prefs["webgl.disabled"] = true;
|
|
435
|
+
LeakWarning.warn("block_webgl", i_know_what_im_doing);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
// If the user has provided a specific WebGL vendor/renderer pair, use it
|
|
439
|
+
let webgl_fp;
|
|
440
|
+
if (webgl_config) {
|
|
441
|
+
webgl_fp = await sampleWebGL(targetOS, ...webgl_config);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
webgl_fp = await sampleWebGL(targetOS);
|
|
445
|
+
}
|
|
446
|
+
const { webGl2Enabled, ...webGlConfig } = webgl_fp;
|
|
447
|
+
// Merge the WebGL fingerprint into the config
|
|
448
|
+
mergeInto(config, webGlConfig);
|
|
449
|
+
// Set the WebGL preferences
|
|
450
|
+
mergeInto(firefox_user_prefs, {
|
|
451
|
+
"webgl.enable-webgl2": webGl2Enabled,
|
|
452
|
+
"webgl.force-enabled": true,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
// Canvas anti-fingerprinting
|
|
456
|
+
mergeInto(config, {
|
|
457
|
+
"canvas:aaOffset": Math.floor(Math.random() * 101) - 50, // nosec
|
|
458
|
+
"canvas:aaCapOffset": true,
|
|
459
|
+
});
|
|
460
|
+
// Cache previous pages, requests, etc (uses more memory)
|
|
461
|
+
if (enable_cache) {
|
|
462
|
+
mergeInto(firefox_user_prefs, CACHE_PREFS);
|
|
463
|
+
}
|
|
464
|
+
// Print the config if debug is enabled
|
|
465
|
+
if (debug) {
|
|
466
|
+
console.debug("[DEBUG] Config:");
|
|
467
|
+
console.debug(config);
|
|
468
|
+
}
|
|
469
|
+
// Validate the config
|
|
470
|
+
validateConfig(config, executable_path);
|
|
471
|
+
//Prepare environment variables to pass to Camoufox
|
|
472
|
+
const env_vars = {
|
|
473
|
+
...getEnvVars(config, targetOS),
|
|
474
|
+
...process.env,
|
|
475
|
+
};
|
|
476
|
+
// Prepare the executable path
|
|
477
|
+
if (executable_path) {
|
|
478
|
+
executable_path = executable_path.toString();
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
executable_path = launchPath();
|
|
482
|
+
}
|
|
483
|
+
const out = {
|
|
484
|
+
executablePath: executable_path,
|
|
485
|
+
args: args,
|
|
486
|
+
env: env_vars,
|
|
487
|
+
firefoxUserPrefs: firefox_user_prefs,
|
|
488
|
+
proxy: proxyUrl
|
|
489
|
+
? {
|
|
490
|
+
server: proxyUrl.origin,
|
|
491
|
+
username: proxyUrl.username,
|
|
492
|
+
password: proxyUrl.password,
|
|
493
|
+
bypass: typeof proxy === "string" ? undefined : proxy?.bypass,
|
|
494
|
+
}
|
|
495
|
+
: undefined,
|
|
496
|
+
headless: headless,
|
|
497
|
+
...launch_options,
|
|
498
|
+
};
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class VirtualDisplay {
|
|
2
|
+
private debug;
|
|
3
|
+
private proc;
|
|
4
|
+
private _display;
|
|
5
|
+
constructor(debug?: boolean);
|
|
6
|
+
private get xvfb_args();
|
|
7
|
+
private get xvfb_path();
|
|
8
|
+
private get xvfb_cmd();
|
|
9
|
+
private execute_xvfb;
|
|
10
|
+
get(): string;
|
|
11
|
+
kill(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get list of lock files in /tmp
|
|
14
|
+
* @returns List of lock file paths
|
|
15
|
+
*/
|
|
16
|
+
static _get_lock_files(): string[];
|
|
17
|
+
private static _free_display;
|
|
18
|
+
private get display();
|
|
19
|
+
private static assert_linux;
|
|
20
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { randomInt } from "node:crypto";
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { globSync } from "glob";
|
|
7
|
+
import { CannotExecuteXvfb, CannotFindXvfb, VirtualDisplayNotSupported, } from "./exceptions.js";
|
|
8
|
+
import { OS_NAME } from "./pkgman.js";
|
|
9
|
+
export class VirtualDisplay {
|
|
10
|
+
debug;
|
|
11
|
+
proc = null;
|
|
12
|
+
_display = null;
|
|
13
|
+
// private _lock = new Lock();
|
|
14
|
+
constructor(debug = false) {
|
|
15
|
+
this.debug = debug;
|
|
16
|
+
}
|
|
17
|
+
get xvfb_args() {
|
|
18
|
+
return [
|
|
19
|
+
"-screen",
|
|
20
|
+
"0",
|
|
21
|
+
"1x1x24",
|
|
22
|
+
"-ac",
|
|
23
|
+
"-nolisten",
|
|
24
|
+
"tcp",
|
|
25
|
+
"-extension",
|
|
26
|
+
"RENDER",
|
|
27
|
+
"+extension",
|
|
28
|
+
"GLX",
|
|
29
|
+
"-extension",
|
|
30
|
+
"COMPOSITE",
|
|
31
|
+
"-extension",
|
|
32
|
+
"XVideo",
|
|
33
|
+
"-extension",
|
|
34
|
+
"XVideo-MotionCompensation",
|
|
35
|
+
"-extension",
|
|
36
|
+
"XINERAMA",
|
|
37
|
+
"-shmem",
|
|
38
|
+
"-fp",
|
|
39
|
+
"built-ins",
|
|
40
|
+
"-nocursor",
|
|
41
|
+
"-br",
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
get xvfb_path() {
|
|
45
|
+
const path = execFileSync("which", ["Xvfb"]).toString().trim();
|
|
46
|
+
if (!path) {
|
|
47
|
+
throw new CannotFindXvfb("Please install Xvfb to use headless mode.");
|
|
48
|
+
}
|
|
49
|
+
if (!existsSync(path) || !execFileSync("test", ["-x", path])) {
|
|
50
|
+
throw new CannotExecuteXvfb(`I do not have permission to execute Xvfb: ${path}`);
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
get xvfb_cmd() {
|
|
55
|
+
return [this.xvfb_path, `:${this.display}`, ...this.xvfb_args];
|
|
56
|
+
}
|
|
57
|
+
execute_xvfb() {
|
|
58
|
+
if (this.debug) {
|
|
59
|
+
console.log("Starting virtual display:", this.xvfb_cmd.join(" "));
|
|
60
|
+
}
|
|
61
|
+
this.proc = spawn(this.xvfb_cmd[0], this.xvfb_cmd.slice(1), {
|
|
62
|
+
stdio: this.debug ? "inherit" : "ignore",
|
|
63
|
+
detached: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
get() {
|
|
67
|
+
VirtualDisplay.assert_linux();
|
|
68
|
+
// this._lock.runExclusive(() => {
|
|
69
|
+
if (!this.proc) {
|
|
70
|
+
this.execute_xvfb();
|
|
71
|
+
}
|
|
72
|
+
else if (this.debug) {
|
|
73
|
+
console.log(`Using virtual display: ${this.display}`);
|
|
74
|
+
}
|
|
75
|
+
// });
|
|
76
|
+
return `:${this.display}`;
|
|
77
|
+
}
|
|
78
|
+
kill() {
|
|
79
|
+
// this._lock.runExclusive(() => {
|
|
80
|
+
if (this.proc && !this.proc.killed) {
|
|
81
|
+
if (this.debug) {
|
|
82
|
+
console.log("Terminating virtual display:", this.display);
|
|
83
|
+
}
|
|
84
|
+
this.proc.kill();
|
|
85
|
+
}
|
|
86
|
+
// });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get list of lock files in /tmp
|
|
90
|
+
* @returns List of lock file paths
|
|
91
|
+
*/
|
|
92
|
+
static _get_lock_files() {
|
|
93
|
+
const tmpd = process.env.TMPDIR || tmpdir();
|
|
94
|
+
try {
|
|
95
|
+
return globSync(path.join(tmpd, ".X*-lock")).filter((p) => {
|
|
96
|
+
try {
|
|
97
|
+
return statSync(p).isFile();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
static _free_display() {
|
|
109
|
+
const ls = VirtualDisplay._get_lock_files().map((x) => parseInt(x.split("X")[1].split("-")[0], 10));
|
|
110
|
+
return ls.length ? Math.max(99, Math.max(...ls) + randomInt(3, 20)) : 99;
|
|
111
|
+
}
|
|
112
|
+
get display() {
|
|
113
|
+
if (this._display === null) {
|
|
114
|
+
this._display = VirtualDisplay._free_display();
|
|
115
|
+
}
|
|
116
|
+
return this._display;
|
|
117
|
+
}
|
|
118
|
+
static assert_linux() {
|
|
119
|
+
if (OS_NAME !== "lin") {
|
|
120
|
+
throw new VirtualDisplayNotSupported("Virtual display is only supported on Linux.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/warnings.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import WARNINGS_DATA from "./mappings/warnings.config.js";
|
|
2
|
+
export class LeakWarning extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "LeakWarning";
|
|
6
|
+
}
|
|
7
|
+
static warn(warningKey, iKnowWhatImDoing) {
|
|
8
|
+
let warning = WARNINGS_DATA[warningKey];
|
|
9
|
+
if (iKnowWhatImDoing) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (iKnowWhatImDoing !== undefined) {
|
|
13
|
+
warning += "\nIf this is intentional, pass `iKnowWhatImDoing=true`.";
|
|
14
|
+
}
|
|
15
|
+
const currentModule = import.meta.dirname;
|
|
16
|
+
const originalStackTrace = Error.prepareStackTrace;
|
|
17
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
18
|
+
const err = new Error();
|
|
19
|
+
const stack = err.stack;
|
|
20
|
+
Error.prepareStackTrace = originalStackTrace;
|
|
21
|
+
for (const frame of stack) {
|
|
22
|
+
const frameFileName = frame.getFileName();
|
|
23
|
+
if (frameFileName && !frameFileName.startsWith(currentModule)) {
|
|
24
|
+
console.warn(`${warning} at ${frameFileName}:${frame.getLineNumber()}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
console.warn(warning);
|
|
29
|
+
}
|
|
30
|
+
}
|