/** * ======================================== * PATCHED VERSION - WebAI-2API * ======================================== * * 修改内容: * - 第 490 行: 修复 SOCKS5 代理 URL.origin 返回 "null" 的问题 * 原代码: server: proxyUrl.origin, * 修改为: server: proxyUrl.protocol + '//' + proxyUrl.host, * * 问题原因: * JavaScript 的 new URL('socks5://...').origin 对非标准协议返回 "null" 字符串, * 导致浏览器报错 NS_ERROR_UNKNOWN_PROXY_HOST * * 搜索 "PATCHED" 可定位所有修改位置 * ======================================== */ // from browserforge.fingerprints import Fingerprint, Screen // from screeninfo import get_monitors // from ua_parser import user_agent_parser import { readFileSync } from "node:fs"; import path from "node:path"; import { UAParser } from "ua-parser-js"; import { addDefaultAddons, confirmPaths, } from "./addons.js"; import { InvalidOS, InvalidPropertyType, NonFirefoxFingerprint, UnknownProperty, } from "./exceptions.js"; import { fromBrowserforge, generateFingerprint, SUPPORTED_OS, } from "./fingerprints.js"; import { publicIP, validIPv4, validIPv6 } from "./ip.js"; import { geoipAllowed, getGeolocation, handleLocales } from "./locale.js"; import FONTS from "./mappings/fonts.config.js"; import { getPath, installedVerStr, launchPath, OS_NAME } from "./pkgman.js"; import { LeakWarning } from "./warnings.js"; import { sampleWebGL } from "./webgl/sample.js"; // Camoufox preferences to cache previous pages and requests const CACHE_PREFS = { "browser.sessionhistory.max_entries": 10, "browser.sessionhistory.max_total_viewers": -1, "browser.cache.memory.enable": true, "browser.cache.disk_cache_ssl": true, "browser.cache.disk.smart_size.enabled": true, }; function getEnvVars(configMap, userAgentOS) { const envVars = {}; let updatedConfigData; try { updatedConfigData = new TextEncoder().encode(JSON.stringify(configMap)); } catch (e) { console.error(`Error updating config: ${e}`); process.exit(1); } const chunkSize = OS_NAME === "win" ? 2047 : 32767; const configStr = new TextDecoder().decode(updatedConfigData); for (let i = 0; i < configStr.length; i += chunkSize) { const chunk = configStr.slice(i, i + chunkSize); const envName = `CAMOU_CONFIG_${Math.floor(i / chunkSize) + 1}`; try { envVars[envName] = chunk; } catch (e) { console.error(`Error setting ${envName}: ${e}`); process.exit(1); } } if (OS_NAME === "lin") { const fontconfigPath = getPath(path.join("fontconfig", userAgentOS)); envVars.FONTCONFIG_PATH = fontconfigPath; } return envVars; } export function getAsBooleanFromENV(name, defaultValue) { const value = process.env[name]; if (value === "false" || value === "0") return false; if (value) return true; return !!defaultValue; } function loadProperties(filePath) { let propFile; filePath = filePath?.toString(); if (filePath) { propFile = path.join(path.dirname(filePath), "properties.json"); } else { propFile = getPath("properties.json"); } const propData = readFileSync(propFile).toString(); const propDict = JSON.parse(propData); return propDict.reduce((acc, prop) => { acc[prop.property] = prop.type; return acc; }, {}); } function validateConfig(configMap, path) { const propertyTypes = loadProperties(path); for (const [key, value] of Object.entries(configMap)) { const expectedType = propertyTypes[key]; if (!expectedType) { throw new UnknownProperty(`Unknown property ${key} in config`); } if (!validateType(value, expectedType)) { throw new InvalidPropertyType(`Invalid type for property ${key}. Expected ${expectedType}, got ${typeof value}`); } } } function validateType(value, expectedType) { switch (expectedType) { case "str": return typeof value === "string"; case "int": return Number.isInteger(value); case "uint": return Number.isInteger(value) && value >= 0; case "double": return typeof value === "number"; case "bool": return typeof value === "boolean"; case "array": return Array.isArray(value); case "dict": return (typeof value === "object" && value !== null && !Array.isArray(value)); default: return false; } } function getTargetOS(config) { if (config["navigator.userAgent"]) { return determineUAOS(config["navigator.userAgent"]); } return OS_NAME; } function determineUAOS(userAgent) { const parser = new UAParser(userAgent); const parsedUA = parser.getOS().name; if (!parsedUA) { throw new Error("Could not determine OS from user agent"); } if (parsedUA.startsWith("macOS")) { return "mac"; } if (parsedUA.startsWith("Windows")) { return "win"; } return "lin"; } function getScreenCons(headless) { if (headless === false) { return undefined; } // TODO - Implement getMonitors // try { // const monitors = getMonitors(); // if (!monitors.length) { // return undefined; // } // const monitor = monitors.reduce((prev, curr) => (prev.width * prev.height > curr.width * curr.height ? prev : curr)); // return { maxWidth: monitor.width, maxHeight: monitor.height }; // } catch { // return undefined; // } return undefined; } function updateFonts(config, targetOS) { const fonts = FONTS[targetOS]; if (config.fonts) { config.fonts = Array.from(new Set([...fonts, ...config.fonts])); } else { config.fonts = fonts; } } function checkCustomFingerprint(fingerprint) { const parser = new UAParser(fingerprint.navigator.userAgent); const browserName = parser.getBrowser().name || "Non-Firefox"; if (browserName !== "Firefox") { 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.`); } LeakWarning.warn("custom_fingerprint", false); } function validateOS(os) { if (!os) return undefined; if (Array.isArray(os)) { os.every(validateOS); return [...os]; } if (!SUPPORTED_OS.includes(os)) { throw new InvalidOS(`Camoufox does not support the OS: '${os}'`); } return [os]; } function _cleanLocals(data) { delete data.playwright; delete data.persistentContext; return data; } function mergeInto(target, source) { Object.entries(source).forEach(([key, value]) => { if (!(key in target)) { target[key] = value; } }); } function setInto(target, key, value) { if (!(key in target)) { target[key] = value; } } function isDomainSet(config, ...properties) { return properties.some((prop) => { if (prop.endsWith(".") || prop.endsWith(":")) { return Object.keys(config).some((key) => key.startsWith(prop)); } return prop in config; }); } function warnManualConfig(config) { if (isDomainSet(config, "navigator.language", "navigator.languages", "headers.Accept-Language", "locale:")) { LeakWarning.warn("locale", false); } if (isDomainSet(config, "geolocation:", "timezone")) { LeakWarning.warn("geolocation", false); } if (isDomainSet(config, "headers.User-Agent")) { LeakWarning.warn("header-ua", false); } if (isDomainSet(config, "navigator.")) { LeakWarning.warn("navigator", false); } if (isDomainSet(config, "screen.", "window.", "document.body.")) { LeakWarning.warn("viewport", false); } } async function _asyncAttachVD(browser, virtualDisplay) { if (!virtualDisplay) { return browser; } const originalClose = browser.close; browser.close = async (...args) => { await originalClose.apply(browser, ...args); if (virtualDisplay) { virtualDisplay.kill(); } }; browser._virtualDisplay = virtualDisplay; return browser; } export function syncAttachVD(browser, virtualDisplay) { /** * Attaches the virtual display to the sync browser cleanup */ if (!virtualDisplay) { // Skip if no virtual display is provided return browser; } const originalClose = browser.close; browser.close = (...args) => { originalClose.apply(browser, ...args); if (virtualDisplay) { virtualDisplay.kill(); } }; browser._virtualDisplay = virtualDisplay; return browser; } /** * Convert a Playwright proxy string to a URL object. * * Implementation from https://github.com/microsoft/playwright/blob/3873b72ac1441ca691f7594f0ed705bd84518f93/packages/playwright-core/src/server/browserContext.ts#L737-L747 */ function getProxyUrl(proxy) { if (!proxy) return null; if (typeof proxy === "string") { return new URL(proxy); } const { server, username, password } = proxy; let url; try { // new URL('127.0.0.1:8080') throws // new URL('localhost:8080') fails to parse host or protocol // In both of these cases, we need to try re-parse URL with `http://` prefix. url = new URL(server); if (!url.host || !url.protocol) url = new URL(`http://${server}`); } catch (_e) { url = new URL(`http://${server}`); } if (username) url.username = username; if (password) url.password = password; return url; } 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 }) { // Build the config if (!config) { config = {}; } // Set default values for optional arguments if (headless === undefined) { headless = false; } if (!addons) { addons = []; } if (!args) { args = []; } if (!firefox_user_prefs) { firefox_user_prefs = {}; } if (custom_fonts_only === undefined) { custom_fonts_only = false; } if (i_know_what_im_doing === undefined) { i_know_what_im_doing = false; } if (!env) { env = process.env; } if (typeof executable_path === "string") { // Convert executable path to a Path object executable_path = path.resolve(executable_path); } // Handle virtual display if (virtual_display) { env.DISPLAY = virtual_display; } // Warn the user for manual config settings if (!i_know_what_im_doing) { warnManualConfig(config); } const operatingSystems = validateOS(os); // webgl_config requires OS to be set if (!operatingSystems && webgl_config) { throw new Error("OS must be set when using webgl_config"); } // Add the default addons addDefaultAddons(addons, exclude_addons); // Confirm all addon paths are valid if (addons.length > 0) { confirmPaths(addons); config.addons = addons; } // Get the Firefox version let ff_version_str; if (ff_version) { ff_version_str = ff_version.toString(); LeakWarning.warn("ff_version", i_know_what_im_doing); } else { ff_version_str = installedVerStr().split(".", 1)[0]; } // Generate a fingerprint if (!fingerprint) { fingerprint = generateFingerprint(window, { screen: screen || getScreenCons(headless || "DISPLAY" in env), operatingSystems, }); } else { // Or use the one passed by the user if (!i_know_what_im_doing) { checkCustomFingerprint(fingerprint); } } // Inject the fingerprint into the config mergeInto(config, fromBrowserforge(fingerprint, ff_version_str)); const targetOS = getTargetOS(config); // Set a random window.history.length setInto(config, "window.history.length", Math.floor(Math.random() * 5) + 1); // Update fonts list if (fonts) { config.fonts = fonts; } if (custom_fonts_only) { firefox_user_prefs["gfx.bundled-fonts.activate"] = 0; if (fonts) { // The user has passed their own fonts, and OS fonts are disabled. LeakWarning.warn("custom_fonts_only"); } else { // OS fonts are disabled, and the user has not passed their own fonts either. throw new Error("No custom fonts were passed, but `custom_fonts_only` is enabled."); } } else { updateFonts(config, targetOS); } // Set a fixed font spacing seed setInto(config, "fonts:spacing_seed", Math.floor(Math.random() * 1_073_741_824)); // Handle proxy const proxyUrl = getProxyUrl(proxy); // Set geolocation if (geoip) { geoipAllowed(); // Find the user's IP address geoip = await publicIP(proxyUrl?.href); // Spoof WebRTC if not blocked if (!block_webrtc) { if (validIPv4(geoip)) { setInto(config, "webrtc:ipv4", geoip); firefox_user_prefs["network.dns.disableIPv6"] = true; } else if (validIPv6(geoip)) { setInto(config, "webrtc:ipv6", geoip); } } const geolocation = await getGeolocation(geoip); config = { ...config, ...geolocation.asConfig() }; } // Raise a warning when a proxy is being used without spoofing geolocation. // This is a very bad idea; the warning cannot be ignored with i_know_what_im_doing. if (proxyUrl && !proxyUrl.hostname.includes("localhost") && !isDomainSet(config, "geolocation:")) { LeakWarning.warn("proxy_without_geoip"); } // Set locale if (locale) { handleLocales(locale, config); } // Pass the humanize option if (humanize) { setInto(config, "humanize", true); if (typeof humanize === "number") { setInto(config, "humanize:maxTime", humanize); } } // Enable the main world context creation if (main_world_eval) { setInto(config, "allowMainWorld", true); } // Set Firefox user preferences if (block_images) { LeakWarning.warn("block_images", i_know_what_im_doing); firefox_user_prefs["permissions.default.image"] = 2; } if (block_webrtc) { firefox_user_prefs["media.peerconnection.enabled"] = false; } if (disable_coop) { LeakWarning.warn("disable_coop", i_know_what_im_doing); firefox_user_prefs["browser.tabs.remote.useCrossOriginOpenerPolicy"] = false; } // Allow allow_webgl parameter for backwards compatibility if (block_webgl || launch_options.allow_webgl === false) { firefox_user_prefs["webgl.disabled"] = true; LeakWarning.warn("block_webgl", i_know_what_im_doing); } else { // If the user has provided a specific WebGL vendor/renderer pair, use it let webgl_fp; if (webgl_config) { webgl_fp = await sampleWebGL(targetOS, ...webgl_config); } else { webgl_fp = await sampleWebGL(targetOS); } const { webGl2Enabled, ...webGlConfig } = webgl_fp; // Merge the WebGL fingerprint into the config mergeInto(config, webGlConfig); // Set the WebGL preferences mergeInto(firefox_user_prefs, { "webgl.enable-webgl2": webGl2Enabled, "webgl.force-enabled": true, }); } // Canvas anti-fingerprinting mergeInto(config, { "canvas:aaOffset": Math.floor(Math.random() * 101) - 50, // nosec "canvas:aaCapOffset": true, }); // Cache previous pages, requests, etc (uses more memory) if (enable_cache) { mergeInto(firefox_user_prefs, CACHE_PREFS); } // Print the config if debug is enabled if (debug) { console.debug("[DEBUG] Config:"); console.debug(config); } // Validate the config validateConfig(config, executable_path); //Prepare environment variables to pass to Camoufox const env_vars = { ...getEnvVars(config, targetOS), ...process.env, }; // Prepare the executable path if (executable_path) { executable_path = executable_path.toString(); } else { executable_path = launchPath(); } const out = { executablePath: executable_path, args: args, env: env_vars, firefoxUserPrefs: firefox_user_prefs, proxy: proxyUrl ? { server: proxyUrl.protocol + '//' + proxyUrl.host, // PATCHED: fix socks5 origin=null username: proxyUrl.username, password: proxyUrl.password, bypass: typeof proxy === "string" ? undefined : proxy?.bypass, } : undefined, headless: headless, ...launch_options, }; return out; }