headfox-js 0.1.1
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.md +373 -0
- package/README.md +121 -0
- package/bin/headfox-js.mjs +17 -0
- package/dist/__main__.d.ts +2 -0
- package/dist/__main__.js +131 -0
- package/dist/__version__.d.ts +8 -0
- package/dist/__version__.js +10 -0
- package/dist/addons.d.ts +17 -0
- package/dist/addons.js +74 -0
- package/dist/data-files/territoryInfo.xml +2024 -0
- package/dist/data-files/webgl_data.db +0 -0
- package/dist/exceptions.d.ts +82 -0
- package/dist/exceptions.js +165 -0
- package/dist/fingerprints.d.ts +4 -0
- package/dist/fingerprints.js +82 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -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 +285 -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 +27 -0
- package/dist/pkgman.d.ts +67 -0
- package/dist/pkgman.js +421 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +24 -0
- package/dist/sync_api.d.ts +10 -0
- package/dist/sync_api.js +35 -0
- package/dist/utils.d.ts +109 -0
- package/dist/utils.js +540 -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 +33 -0
- package/dist/webgl/sample.d.ts +19 -0
- package/dist/webgl/sample.js +121 -0
- package/package.json +94 -0
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type PathLike } from "node:fs";
|
|
2
|
+
import type { Fingerprint, FingerprintGeneratorOptions } from "fingerprint-generator";
|
|
3
|
+
import type { LaunchOptions as PlaywrightLaunchOptions } from "playwright-core";
|
|
4
|
+
import { type DefaultAddons } from "./addons.js";
|
|
5
|
+
import { SUPPORTED_OS } from "./fingerprints.js";
|
|
6
|
+
import type { VirtualDisplay } from "./virtdisplay.js";
|
|
7
|
+
type Screen = FingerprintGeneratorOptions["screen"];
|
|
8
|
+
export declare function getAsBooleanFromENV(name: string, defaultValue?: boolean | undefined): boolean;
|
|
9
|
+
interface WebGlLaunchConfig {
|
|
10
|
+
config: Record<string, any>;
|
|
11
|
+
firefoxUserPrefs: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveWebGlLaunchConfig({ targetOS, block_webgl, allow_webgl, webgl_config, i_know_what_im_doing, }: {
|
|
14
|
+
targetOS: "mac" | "win" | "lin";
|
|
15
|
+
block_webgl?: boolean;
|
|
16
|
+
allow_webgl?: boolean;
|
|
17
|
+
webgl_config?: [string, string];
|
|
18
|
+
i_know_what_im_doing?: boolean;
|
|
19
|
+
}): Promise<WebGlLaunchConfig>;
|
|
20
|
+
export declare function syncAttachVD(browser: any, virtualDisplay?: VirtualDisplay | null): any;
|
|
21
|
+
export interface LaunchOptions {
|
|
22
|
+
/** Operating system to use for the fingerprint generation.
|
|
23
|
+
* Can be "windows", "macos", "linux", or a list to randomly choose from.
|
|
24
|
+
* Default: ["windows", "macos", "linux"]
|
|
25
|
+
*/
|
|
26
|
+
os?: (typeof SUPPORTED_OS)[number] | (typeof SUPPORTED_OS)[number][];
|
|
27
|
+
/** Whether to block all images. */
|
|
28
|
+
block_images?: boolean;
|
|
29
|
+
/** Whether to block WebRTC entirely. */
|
|
30
|
+
block_webrtc?: boolean;
|
|
31
|
+
/** Whether to block WebGL. To prevent leaks, only use this for special cases. */
|
|
32
|
+
block_webgl?: boolean;
|
|
33
|
+
/** Disables the Cross-Origin-Opener-Policy, allowing elements in cross-origin iframes to be clicked. */
|
|
34
|
+
disable_coop?: boolean;
|
|
35
|
+
/** Calculate longitude, latitude, timezone, country, & locale based on the IP address.
|
|
36
|
+
* Pass the target IP address to use, or `true` to find the IP address automatically.
|
|
37
|
+
*/
|
|
38
|
+
geoip?: string | boolean;
|
|
39
|
+
/** Humanize the cursor movement.
|
|
40
|
+
* Takes either `true`, or the MAX duration in seconds of the cursor movement.
|
|
41
|
+
* The cursor typically takes up to 1.5 seconds to move across the window.
|
|
42
|
+
*/
|
|
43
|
+
humanize?: boolean | number;
|
|
44
|
+
/** Locale(s) to use. The first listed locale will be used for the Intl API. */
|
|
45
|
+
locale?: string | string[];
|
|
46
|
+
/** List of Firefox addons to use. */
|
|
47
|
+
addons?: string[];
|
|
48
|
+
/** Fonts to load into the browser (in addition to the default fonts for the target `os`).
|
|
49
|
+
* Takes a list of font family names that are installed on the system.
|
|
50
|
+
*/
|
|
51
|
+
fonts?: string[];
|
|
52
|
+
/** If enabled, OS-specific system fonts will not be passed to the browser. */
|
|
53
|
+
custom_fonts_only?: boolean;
|
|
54
|
+
/** Default addons to exclude. Passed as a list of `DefaultAddons` enums. */
|
|
55
|
+
exclude_addons?: (keyof typeof DefaultAddons)[];
|
|
56
|
+
/** Constrains the screen dimensions of the generated fingerprint. */
|
|
57
|
+
screen?: Screen;
|
|
58
|
+
/** Set a fixed window size instead of generating a random one. */
|
|
59
|
+
window?: [number, number];
|
|
60
|
+
/** Use a custom BrowserForge fingerprint. If not provided, a random fingerprint will be generated
|
|
61
|
+
* based on the provided `os` & `screen` constraints.
|
|
62
|
+
*/
|
|
63
|
+
fingerprint?: Fingerprint;
|
|
64
|
+
/** Firefox version to use. Defaults to the current Headfox version.
|
|
65
|
+
* To prevent leaks, only use this for special cases.
|
|
66
|
+
*/
|
|
67
|
+
ff_version?: number;
|
|
68
|
+
/** Whether to run the browser in headless mode. Defaults to `false`.
|
|
69
|
+
* Can be `true`, `false`, or `"virtual"` to use a virtual display.
|
|
70
|
+
*/
|
|
71
|
+
headless?: boolean | "virtual";
|
|
72
|
+
/** Whether to enable running scripts in the main world.
|
|
73
|
+
* To use this, prepend "mw:" to the script: `page.evaluate("mw:" + script)`.
|
|
74
|
+
*/
|
|
75
|
+
main_world_eval?: boolean;
|
|
76
|
+
/** Custom browser executable path. */
|
|
77
|
+
executable_path?: string | PathLike;
|
|
78
|
+
/** Firefox user preferences to set. */
|
|
79
|
+
firefox_user_prefs?: Record<string, any>;
|
|
80
|
+
/** Proxy to use for the browser.
|
|
81
|
+
* Note: If `geoip` is `true`, a request will be sent through this proxy to find the target IP.
|
|
82
|
+
*/
|
|
83
|
+
proxy?: string | PlaywrightLaunchOptions["proxy"];
|
|
84
|
+
/** Cache previous pages, requests, etc. (uses more memory). */
|
|
85
|
+
enable_cache?: boolean;
|
|
86
|
+
/** Arguments to pass to the browser. */
|
|
87
|
+
args?: string[];
|
|
88
|
+
/** Environment variables to set. */
|
|
89
|
+
env?: Record<string, string | number | boolean>;
|
|
90
|
+
/** Prints the config being sent to Headfox. */
|
|
91
|
+
debug?: boolean;
|
|
92
|
+
/** Virtual display number. Example: `":99"`. This is handled by Headfox and the legacy AsyncCamoufox flow. */
|
|
93
|
+
virtual_display?: string;
|
|
94
|
+
/** Use a specific WebGL vendor/renderer pair. Passed as a tuple of `[vendor, renderer]`. */
|
|
95
|
+
webgl_config?: [string, string];
|
|
96
|
+
/** Additional Firefox launch options. */
|
|
97
|
+
[key: string]: any;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Prepare launch options for Playwright's Firefox browser.
|
|
101
|
+
*
|
|
102
|
+
* Note: This function only accepts `boolean` for the `headless` parameter.
|
|
103
|
+
* Callers must normalize `"virtual"` to `boolean` before calling this function.
|
|
104
|
+
* The virtual display setup is handled separately in the calling function.
|
|
105
|
+
*/
|
|
106
|
+
export declare 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 }: Omit<LaunchOptions, "headless"> & {
|
|
107
|
+
headless?: boolean;
|
|
108
|
+
}): Promise<Record<string, any>>;
|
|
109
|
+
export {};
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
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, WebGLFingerprintUnavailable, } 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 { ensureBrowserInstalled, getPath, installedVerStr, launchPath, OS_NAME, } from "./pkgman.js";
|
|
14
|
+
import { LeakWarning } from "./warnings.js";
|
|
15
|
+
import { sampleWebGL } from "./webgl/sample.js";
|
|
16
|
+
// Headfox 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
|
+
// The Python engine still reads the legacy CAMOU_CONFIG_* names.
|
|
39
|
+
const envName = `CAMOU_CONFIG_${Math.floor(i / chunkSize) + 1}`;
|
|
40
|
+
try {
|
|
41
|
+
envVars[envName] = chunk;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
console.error(`Error setting ${envName}: ${e}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (OS_NAME === "lin") {
|
|
49
|
+
const fontconfigPath = getPath(path.join("fontconfig", userAgentOS));
|
|
50
|
+
envVars.FONTCONFIG_PATH = fontconfigPath;
|
|
51
|
+
}
|
|
52
|
+
return envVars;
|
|
53
|
+
}
|
|
54
|
+
export function getAsBooleanFromENV(name, defaultValue) {
|
|
55
|
+
const value = process.env[name];
|
|
56
|
+
if (value === "false" || value === "0")
|
|
57
|
+
return false;
|
|
58
|
+
if (value)
|
|
59
|
+
return true;
|
|
60
|
+
return !!defaultValue;
|
|
61
|
+
}
|
|
62
|
+
function loadProperties(filePath) {
|
|
63
|
+
let propFile;
|
|
64
|
+
filePath = filePath?.toString();
|
|
65
|
+
if (filePath) {
|
|
66
|
+
propFile = path.join(path.dirname(filePath), "properties.json");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
propFile = getPath("properties.json");
|
|
70
|
+
}
|
|
71
|
+
const propData = readFileSync(propFile).toString();
|
|
72
|
+
const propDict = JSON.parse(propData);
|
|
73
|
+
return propDict.reduce((acc, prop) => {
|
|
74
|
+
acc[prop.property] = prop.type;
|
|
75
|
+
return acc;
|
|
76
|
+
}, {});
|
|
77
|
+
}
|
|
78
|
+
function validateConfig(configMap, path) {
|
|
79
|
+
const propertyTypes = loadProperties(path);
|
|
80
|
+
for (const [key, value] of Object.entries(configMap)) {
|
|
81
|
+
const expectedType = propertyTypes[key];
|
|
82
|
+
if (!expectedType) {
|
|
83
|
+
throw new UnknownProperty(`Unknown property ${key} in config`);
|
|
84
|
+
}
|
|
85
|
+
if (!validateType(value, expectedType)) {
|
|
86
|
+
throw new InvalidPropertyType(`Invalid type for property ${key}. Expected ${expectedType}, got ${typeof value}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function validateType(value, expectedType) {
|
|
91
|
+
switch (expectedType) {
|
|
92
|
+
case "str":
|
|
93
|
+
return typeof value === "string";
|
|
94
|
+
case "int":
|
|
95
|
+
return Number.isInteger(value);
|
|
96
|
+
case "uint":
|
|
97
|
+
return Number.isInteger(value) && value >= 0;
|
|
98
|
+
case "double":
|
|
99
|
+
return typeof value === "number";
|
|
100
|
+
case "bool":
|
|
101
|
+
return typeof value === "boolean";
|
|
102
|
+
case "array":
|
|
103
|
+
return Array.isArray(value);
|
|
104
|
+
case "dict":
|
|
105
|
+
return (typeof value === "object" && value !== null && !Array.isArray(value));
|
|
106
|
+
default:
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function getTargetOS(config) {
|
|
111
|
+
if (config["navigator.userAgent"]) {
|
|
112
|
+
return determineUAOS(config["navigator.userAgent"]);
|
|
113
|
+
}
|
|
114
|
+
return OS_NAME;
|
|
115
|
+
}
|
|
116
|
+
function determineUAOS(userAgent) {
|
|
117
|
+
const parser = new UAParser(userAgent);
|
|
118
|
+
const parsedUA = parser.getOS().name;
|
|
119
|
+
if (!parsedUA) {
|
|
120
|
+
throw new Error("Could not determine OS from user agent");
|
|
121
|
+
}
|
|
122
|
+
if (parsedUA.startsWith("macOS")) {
|
|
123
|
+
return "mac";
|
|
124
|
+
}
|
|
125
|
+
if (parsedUA.startsWith("Windows")) {
|
|
126
|
+
return "win";
|
|
127
|
+
}
|
|
128
|
+
return "lin";
|
|
129
|
+
}
|
|
130
|
+
function getScreenCons(headless) {
|
|
131
|
+
if (headless === false) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
// TODO - Implement getMonitors
|
|
135
|
+
// try {
|
|
136
|
+
// const monitors = getMonitors();
|
|
137
|
+
// if (!monitors.length) {
|
|
138
|
+
// return undefined;
|
|
139
|
+
// }
|
|
140
|
+
// const monitor = monitors.reduce((prev, curr) => (prev.width * prev.height > curr.width * curr.height ? prev : curr));
|
|
141
|
+
// return { maxWidth: monitor.width, maxHeight: monitor.height };
|
|
142
|
+
// } catch {
|
|
143
|
+
// return undefined;
|
|
144
|
+
// }
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
export async function resolveWebGlLaunchConfig({ targetOS, block_webgl, allow_webgl, webgl_config, i_know_what_im_doing, }) {
|
|
148
|
+
const firefoxUserPrefs = {};
|
|
149
|
+
if (block_webgl || allow_webgl === false) {
|
|
150
|
+
firefoxUserPrefs["webgl.disabled"] = true;
|
|
151
|
+
LeakWarning.warn("block_webgl", i_know_what_im_doing);
|
|
152
|
+
return {
|
|
153
|
+
config: {},
|
|
154
|
+
firefoxUserPrefs,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const webglFingerprint = webgl_config
|
|
159
|
+
? await sampleWebGL(targetOS, ...webgl_config)
|
|
160
|
+
: await sampleWebGL(targetOS);
|
|
161
|
+
const { webGl2Enabled, ...webGlConfig } = webglFingerprint;
|
|
162
|
+
return {
|
|
163
|
+
config: webGlConfig,
|
|
164
|
+
firefoxUserPrefs: {
|
|
165
|
+
"webgl.enable-webgl2": webGl2Enabled,
|
|
166
|
+
"webgl.force-enabled": true,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error instanceof WebGLFingerprintUnavailable)) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
firefoxUserPrefs["webgl.disabled"] = true;
|
|
175
|
+
console.warn("headfox-js(warn): WebGL fingerprint sampling is unavailable. Continuing with WebGL disabled for this launch.");
|
|
176
|
+
return {
|
|
177
|
+
config: {},
|
|
178
|
+
firefoxUserPrefs,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function updateFonts(config, targetOS) {
|
|
183
|
+
const fonts = FONTS[targetOS];
|
|
184
|
+
if (config.fonts) {
|
|
185
|
+
config.fonts = Array.from(new Set([...fonts, ...config.fonts]));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
config.fonts = fonts;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function checkCustomFingerprint(fingerprint) {
|
|
192
|
+
const parser = new UAParser(fingerprint.navigator.userAgent);
|
|
193
|
+
const browserName = parser.getBrowser().name || "Non-Firefox";
|
|
194
|
+
if (browserName !== "Firefox") {
|
|
195
|
+
throw new NonFirefoxFingerprint(`"${browserName}" fingerprints are not supported in Headfox. Using fingerprints from a browser other than Firefox will lead to detection. If this is intentional, pass i_know_what_im_doing=true.`);
|
|
196
|
+
}
|
|
197
|
+
LeakWarning.warn("custom_fingerprint", false);
|
|
198
|
+
}
|
|
199
|
+
function validateOS(os) {
|
|
200
|
+
if (!os)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (Array.isArray(os)) {
|
|
203
|
+
os.every(validateOS);
|
|
204
|
+
return [...os];
|
|
205
|
+
}
|
|
206
|
+
if (!SUPPORTED_OS.includes(os)) {
|
|
207
|
+
throw new InvalidOS(`Headfox does not support the OS: '${os}'`);
|
|
208
|
+
}
|
|
209
|
+
return [os];
|
|
210
|
+
}
|
|
211
|
+
function _cleanLocals(data) {
|
|
212
|
+
delete data.playwright;
|
|
213
|
+
delete data.persistentContext;
|
|
214
|
+
return data;
|
|
215
|
+
}
|
|
216
|
+
function mergeInto(target, source) {
|
|
217
|
+
Object.entries(source).forEach(([key, value]) => {
|
|
218
|
+
if (!(key in target)) {
|
|
219
|
+
target[key] = value;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function setInto(target, key, value) {
|
|
224
|
+
if (!(key in target)) {
|
|
225
|
+
target[key] = value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function isDomainSet(config, ...properties) {
|
|
229
|
+
return properties.some((prop) => {
|
|
230
|
+
if (prop.endsWith(".") || prop.endsWith(":")) {
|
|
231
|
+
return Object.keys(config).some((key) => key.startsWith(prop));
|
|
232
|
+
}
|
|
233
|
+
return prop in config;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function warnManualConfig(config) {
|
|
237
|
+
if (isDomainSet(config, "navigator.language", "navigator.languages", "headers.Accept-Language", "locale:")) {
|
|
238
|
+
LeakWarning.warn("locale", false);
|
|
239
|
+
}
|
|
240
|
+
if (isDomainSet(config, "geolocation:", "timezone")) {
|
|
241
|
+
LeakWarning.warn("geolocation", false);
|
|
242
|
+
}
|
|
243
|
+
if (isDomainSet(config, "headers.User-Agent")) {
|
|
244
|
+
LeakWarning.warn("header-ua", false);
|
|
245
|
+
}
|
|
246
|
+
if (isDomainSet(config, "navigator.")) {
|
|
247
|
+
LeakWarning.warn("navigator", false);
|
|
248
|
+
}
|
|
249
|
+
if (isDomainSet(config, "screen.", "window.", "document.body.")) {
|
|
250
|
+
LeakWarning.warn("viewport", false);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function _asyncAttachVD(browser, virtualDisplay) {
|
|
254
|
+
if (!virtualDisplay) {
|
|
255
|
+
return browser;
|
|
256
|
+
}
|
|
257
|
+
const originalClose = browser.close;
|
|
258
|
+
browser.close = async (...args) => {
|
|
259
|
+
await originalClose.apply(browser, ...args);
|
|
260
|
+
if (virtualDisplay) {
|
|
261
|
+
virtualDisplay.kill();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
browser._virtualDisplay = virtualDisplay;
|
|
265
|
+
return browser;
|
|
266
|
+
}
|
|
267
|
+
export function syncAttachVD(browser, virtualDisplay) {
|
|
268
|
+
/**
|
|
269
|
+
* Attaches the virtual display to the sync browser cleanup
|
|
270
|
+
*/
|
|
271
|
+
if (!virtualDisplay) {
|
|
272
|
+
// Skip if no virtual display is provided
|
|
273
|
+
return browser;
|
|
274
|
+
}
|
|
275
|
+
const originalClose = browser.close;
|
|
276
|
+
browser.close = (...args) => {
|
|
277
|
+
originalClose.apply(browser, ...args);
|
|
278
|
+
if (virtualDisplay) {
|
|
279
|
+
virtualDisplay.kill();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
browser._virtualDisplay = virtualDisplay;
|
|
283
|
+
return browser;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Convert a Playwright proxy string to a URL object.
|
|
287
|
+
*
|
|
288
|
+
* Implementation from https://github.com/microsoft/playwright/blob/3873b72ac1441ca691f7594f0ed705bd84518f93/packages/playwright-core/src/server/browserContext.ts#L737-L747
|
|
289
|
+
*/
|
|
290
|
+
function getProxyUrl(proxy) {
|
|
291
|
+
if (!proxy)
|
|
292
|
+
return null;
|
|
293
|
+
if (typeof proxy === "string") {
|
|
294
|
+
return new URL(proxy);
|
|
295
|
+
}
|
|
296
|
+
const { server, username, password } = proxy;
|
|
297
|
+
let url;
|
|
298
|
+
try {
|
|
299
|
+
// new URL('127.0.0.1:8080') throws
|
|
300
|
+
// new URL('localhost:8080') fails to parse host or protocol
|
|
301
|
+
// In both of these cases, we need to try re-parse URL with `http://` prefix.
|
|
302
|
+
url = new URL(server);
|
|
303
|
+
if (!url.host || !url.protocol)
|
|
304
|
+
url = new URL(`http://${server}`);
|
|
305
|
+
}
|
|
306
|
+
catch (_e) {
|
|
307
|
+
url = new URL(`http://${server}`);
|
|
308
|
+
}
|
|
309
|
+
if (username)
|
|
310
|
+
url.username = username;
|
|
311
|
+
if (password)
|
|
312
|
+
url.password = password;
|
|
313
|
+
return url;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Prepare launch options for Playwright's Firefox browser.
|
|
317
|
+
*
|
|
318
|
+
* Note: This function only accepts `boolean` for the `headless` parameter.
|
|
319
|
+
* Callers must normalize `"virtual"` to `boolean` before calling this function.
|
|
320
|
+
* The virtual display setup is handled separately in the calling function.
|
|
321
|
+
*/
|
|
322
|
+
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 }) {
|
|
323
|
+
// Build the config
|
|
324
|
+
if (!config) {
|
|
325
|
+
config = {};
|
|
326
|
+
}
|
|
327
|
+
// Set default values for optional arguments
|
|
328
|
+
const headlessBoolean = headless ?? false;
|
|
329
|
+
if (!addons) {
|
|
330
|
+
addons = [];
|
|
331
|
+
}
|
|
332
|
+
if (!args) {
|
|
333
|
+
args = [];
|
|
334
|
+
}
|
|
335
|
+
if (!firefox_user_prefs) {
|
|
336
|
+
firefox_user_prefs = {};
|
|
337
|
+
}
|
|
338
|
+
if (custom_fonts_only === undefined) {
|
|
339
|
+
custom_fonts_only = false;
|
|
340
|
+
}
|
|
341
|
+
if (i_know_what_im_doing === undefined) {
|
|
342
|
+
i_know_what_im_doing = false;
|
|
343
|
+
}
|
|
344
|
+
if (!env) {
|
|
345
|
+
env = process.env;
|
|
346
|
+
}
|
|
347
|
+
if (typeof executable_path === "string") {
|
|
348
|
+
// Convert executable path to a Path object
|
|
349
|
+
executable_path = path.resolve(executable_path);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
await ensureBrowserInstalled();
|
|
353
|
+
}
|
|
354
|
+
// Handle virtual display
|
|
355
|
+
if (virtual_display) {
|
|
356
|
+
env.DISPLAY = virtual_display;
|
|
357
|
+
}
|
|
358
|
+
// Warn the user for manual config settings
|
|
359
|
+
if (!i_know_what_im_doing) {
|
|
360
|
+
warnManualConfig(config);
|
|
361
|
+
}
|
|
362
|
+
const operatingSystems = validateOS(os);
|
|
363
|
+
// webgl_config requires OS to be set
|
|
364
|
+
if (!operatingSystems && webgl_config) {
|
|
365
|
+
throw new Error("OS must be set when using webgl_config");
|
|
366
|
+
}
|
|
367
|
+
// Add the default addons
|
|
368
|
+
await addDefaultAddons(addons, exclude_addons);
|
|
369
|
+
// Confirm all addon paths are valid
|
|
370
|
+
if (addons.length > 0) {
|
|
371
|
+
confirmPaths(addons);
|
|
372
|
+
config.addons = addons;
|
|
373
|
+
}
|
|
374
|
+
// Get the Firefox version
|
|
375
|
+
let ff_version_str;
|
|
376
|
+
if (ff_version) {
|
|
377
|
+
ff_version_str = ff_version.toString();
|
|
378
|
+
LeakWarning.warn("ff_version", i_know_what_im_doing);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
try {
|
|
382
|
+
ff_version_str = installedVerStr().split(".", 1)[0];
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (executable_path) {
|
|
386
|
+
throw new Error("Unable to infer the Firefox version for a custom executable path. Pass `ff_version` explicitly or install the managed browser with `headfox-js fetch`.", { cause: error });
|
|
387
|
+
}
|
|
388
|
+
await ensureBrowserInstalled();
|
|
389
|
+
ff_version_str = installedVerStr().split(".", 1)[0];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Generate a fingerprint
|
|
393
|
+
if (!fingerprint) {
|
|
394
|
+
fingerprint = generateFingerprint(window, {
|
|
395
|
+
screen: screen || getScreenCons(headlessBoolean || "DISPLAY" in env),
|
|
396
|
+
operatingSystems,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// Or use the one passed by the user
|
|
401
|
+
if (!i_know_what_im_doing) {
|
|
402
|
+
checkCustomFingerprint(fingerprint);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Inject the fingerprint into the config
|
|
406
|
+
mergeInto(config, fromBrowserforge(fingerprint, ff_version_str));
|
|
407
|
+
const targetOS = getTargetOS(config);
|
|
408
|
+
// Set a random window.history.length
|
|
409
|
+
setInto(config, "window.history.length", Math.floor(Math.random() * 5) + 1);
|
|
410
|
+
// Update fonts list
|
|
411
|
+
if (fonts) {
|
|
412
|
+
config.fonts = fonts;
|
|
413
|
+
}
|
|
414
|
+
if (custom_fonts_only) {
|
|
415
|
+
firefox_user_prefs["gfx.bundled-fonts.activate"] = 0;
|
|
416
|
+
if (fonts) {
|
|
417
|
+
// The user has passed their own fonts, and OS fonts are disabled.
|
|
418
|
+
LeakWarning.warn("custom_fonts_only");
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// OS fonts are disabled, and the user has not passed their own fonts either.
|
|
422
|
+
throw new Error("No custom fonts were passed, but `custom_fonts_only` is enabled.");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
updateFonts(config, targetOS);
|
|
427
|
+
}
|
|
428
|
+
// Set a fixed font spacing seed
|
|
429
|
+
setInto(config, "fonts:spacing_seed", Math.floor(Math.random() * 1_073_741_824));
|
|
430
|
+
// Handle proxy
|
|
431
|
+
const proxyUrl = getProxyUrl(proxy);
|
|
432
|
+
// Set geolocation
|
|
433
|
+
if (geoip) {
|
|
434
|
+
geoipAllowed();
|
|
435
|
+
// Find the user's IP address
|
|
436
|
+
geoip = await publicIP(proxyUrl?.href);
|
|
437
|
+
// Spoof WebRTC if not blocked
|
|
438
|
+
if (!block_webrtc) {
|
|
439
|
+
if (validIPv4(geoip)) {
|
|
440
|
+
setInto(config, "webrtc:ipv4", geoip);
|
|
441
|
+
firefox_user_prefs["network.dns.disableIPv6"] = true;
|
|
442
|
+
}
|
|
443
|
+
else if (validIPv6(geoip)) {
|
|
444
|
+
setInto(config, "webrtc:ipv6", geoip);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const geolocation = await getGeolocation(geoip);
|
|
448
|
+
config = { ...config, ...geolocation.asConfig() };
|
|
449
|
+
}
|
|
450
|
+
// Raise a warning when a proxy is being used without spoofing geolocation.
|
|
451
|
+
// This is a very bad idea; the warning cannot be ignored with i_know_what_im_doing.
|
|
452
|
+
if (proxyUrl &&
|
|
453
|
+
!proxyUrl.hostname.includes("localhost") &&
|
|
454
|
+
!isDomainSet(config, "geolocation:")) {
|
|
455
|
+
LeakWarning.warn("proxy_without_geoip");
|
|
456
|
+
}
|
|
457
|
+
// Set locale
|
|
458
|
+
if (locale) {
|
|
459
|
+
handleLocales(locale, config);
|
|
460
|
+
}
|
|
461
|
+
// Pass the humanize option
|
|
462
|
+
if (humanize) {
|
|
463
|
+
setInto(config, "humanize", true);
|
|
464
|
+
if (typeof humanize === "number") {
|
|
465
|
+
setInto(config, "humanize:maxTime", humanize);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Enable the main world context creation
|
|
469
|
+
if (main_world_eval) {
|
|
470
|
+
setInto(config, "allowMainWorld", true);
|
|
471
|
+
}
|
|
472
|
+
// Set Firefox user preferences
|
|
473
|
+
if (block_images) {
|
|
474
|
+
LeakWarning.warn("block_images", i_know_what_im_doing);
|
|
475
|
+
firefox_user_prefs["permissions.default.image"] = 2;
|
|
476
|
+
}
|
|
477
|
+
if (block_webrtc) {
|
|
478
|
+
firefox_user_prefs["media.peerconnection.enabled"] = false;
|
|
479
|
+
}
|
|
480
|
+
if (disable_coop) {
|
|
481
|
+
LeakWarning.warn("disable_coop", i_know_what_im_doing);
|
|
482
|
+
firefox_user_prefs["browser.tabs.remote.useCrossOriginOpenerPolicy"] =
|
|
483
|
+
false;
|
|
484
|
+
}
|
|
485
|
+
const webglLaunchConfig = await resolveWebGlLaunchConfig({
|
|
486
|
+
targetOS,
|
|
487
|
+
block_webgl,
|
|
488
|
+
allow_webgl: launch_options.allow_webgl,
|
|
489
|
+
webgl_config,
|
|
490
|
+
i_know_what_im_doing,
|
|
491
|
+
});
|
|
492
|
+
mergeInto(config, webglLaunchConfig.config);
|
|
493
|
+
mergeInto(firefox_user_prefs, webglLaunchConfig.firefoxUserPrefs);
|
|
494
|
+
// Canvas anti-fingerprinting
|
|
495
|
+
mergeInto(config, {
|
|
496
|
+
"canvas:aaOffset": Math.floor(Math.random() * 101) - 50, // nosec
|
|
497
|
+
"canvas:aaCapOffset": true,
|
|
498
|
+
});
|
|
499
|
+
// Cache previous pages, requests, etc (uses more memory)
|
|
500
|
+
if (enable_cache) {
|
|
501
|
+
mergeInto(firefox_user_prefs, CACHE_PREFS);
|
|
502
|
+
}
|
|
503
|
+
// Print the config if debug is enabled
|
|
504
|
+
if (debug) {
|
|
505
|
+
console.debug("[DEBUG] Config:");
|
|
506
|
+
console.debug(config);
|
|
507
|
+
}
|
|
508
|
+
// Validate the config
|
|
509
|
+
validateConfig(config, executable_path);
|
|
510
|
+
// Prepare environment variables to pass to Headfox.
|
|
511
|
+
const env_vars = {
|
|
512
|
+
...getEnvVars(config, targetOS),
|
|
513
|
+
...process.env,
|
|
514
|
+
...env,
|
|
515
|
+
};
|
|
516
|
+
// Prepare the executable path
|
|
517
|
+
if (executable_path) {
|
|
518
|
+
executable_path = executable_path.toString();
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
executable_path = launchPath();
|
|
522
|
+
}
|
|
523
|
+
const out = {
|
|
524
|
+
executablePath: executable_path,
|
|
525
|
+
args: args,
|
|
526
|
+
env: env_vars,
|
|
527
|
+
firefoxUserPrefs: firefox_user_prefs,
|
|
528
|
+
proxy: proxyUrl
|
|
529
|
+
? {
|
|
530
|
+
server: proxyUrl.origin,
|
|
531
|
+
username: proxyUrl.username,
|
|
532
|
+
password: proxyUrl.password,
|
|
533
|
+
bypass: typeof proxy === "string" ? undefined : proxy?.bypass,
|
|
534
|
+
}
|
|
535
|
+
: undefined,
|
|
536
|
+
headless: headlessBoolean,
|
|
537
|
+
...launch_options,
|
|
538
|
+
};
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
@@ -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
|
+
}
|