camoufox
Version:
JavaScript port of Camoufox - a tool for Firefox anti-fingerprinting and browser automation.
256 lines (255 loc) • 8.97 kB
JavaScript
import { GitHubDownloader, webdl, INSTALL_DIR, LOCAL_DATA } from './pkgman.js';
import { LeakWarning } from './warnings.js';
import { InvalidLocale, MissingRelease, NotInstalledGeoIPExtra, UnknownIPLocation, UnknownLanguage, UnknownTerritory, } from './exceptions.js';
import { validateIP } from './ip.js';
import tags from 'language-tags';
import * as fs from 'fs';
import * as path from 'path';
import maxmind from 'maxmind';
import xml2js from 'xml2js';
import { getAsBooleanFromENV } from './utils.js';
export const ALLOW_GEOIP = true;
class Locale {
language;
region;
script;
constructor(language, region, script) {
this.language = language;
this.region = region;
this.script = script;
}
asString() {
if (this.region) {
return `${this.language}-${this.region}`;
}
return this.language;
}
asConfig() {
if (!this.region) {
throw new Error("Region is required for config");
}
const data = {
'locale:region': this.region,
'locale:language': this.language,
};
if (this.script) {
data['locale:script'] = this.script;
}
return data;
}
}
class Geolocation {
locale;
longitude;
latitude;
timezone;
accuracy;
constructor(locale, longitude, latitude, timezone, accuracy) {
this.locale = locale;
this.longitude = longitude;
this.latitude = latitude;
this.timezone = timezone;
this.accuracy = accuracy;
}
asConfig() {
const data = {
'geolocation:longitude': this.longitude,
'geolocation:latitude': this.latitude,
'timezone': this.timezone,
...this.locale.asConfig(),
};
if (this.accuracy !== undefined) {
data['geolocation:accuracy'] = this.accuracy;
}
return data;
}
}
function verifyLocale(loc) {
if (tags.check(loc)) {
return;
}
throw InvalidLocale.invalidInput(loc);
}
export function normalizeLocale(locale) {
verifyLocale(locale);
const parser = tags(locale);
if (!parser.region) {
throw InvalidLocale.invalidInput(locale);
}
const record = parser.language()?.data['record'];
return new Locale(record['Subtag'], parser.region()?.data['record']['Subtag'], record['Suppress-Script']);
}
export function handleLocale(locale, ignoreRegion = false) {
if (locale.length > 3) {
return normalizeLocale(locale);
}
try {
return SELECTOR.fromRegion(locale);
}
catch (e) {
if (e instanceof UnknownTerritory) {
}
else {
throw e;
}
}
if (ignoreRegion) {
verifyLocale(locale);
return new Locale(locale);
}
try {
const language = SELECTOR.fromLanguage(locale);
LeakWarning.warn('no_region');
return language;
}
catch (e) {
if (e instanceof UnknownLanguage) {
}
else {
throw e;
}
}
throw InvalidLocale.invalidInput(locale);
}
export function handleLocales(locales, config) {
if (typeof locales === 'string') {
locales = locales.split(',').map(loc => loc.trim());
}
const intlLocale = handleLocale(locales[0]);
config = { ...config, ...intlLocale.asConfig() };
if (locales.length < 2) {
return;
}
config['locale:all'] = joinUnique(locales.map(locale => handleLocale(locale, true).asString()));
}
function joinUnique(seq) {
const seen = new Set();
return seq.filter(x => !seen.has(x) && seen.add(x)).join(', ');
}
const MMDB_FILE = path.join(INSTALL_DIR.toString(), 'GeoLite2-City.mmdb');
const MMDB_REPO = "P3TERX/GeoLite.mmdb";
class MaxMindDownloader extends GitHubDownloader {
checkAsset(asset) {
if (asset['name'].endsWith('-City.mmdb')) {
return asset['browser_download_url'];
}
return null;
}
missingAssetError() {
throw new MissingRelease('Failed to find GeoIP database release asset');
}
}
export function geoipAllowed() {
if (!ALLOW_GEOIP) {
throw new NotInstalledGeoIPExtra('Please install the geoip extra to use this feature: pip install camoufox[geoip]');
}
}
export async function downloadMMDB(mmdb_file = MMDB_FILE) {
geoipAllowed();
if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD', false)) {
console.log("Skipping GeoIP database download due to PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD set!");
return;
}
const assetUrl = await (new MaxMindDownloader(MMDB_REPO).getAsset());
const dir = path.dirname(mmdb_file);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const fileStream = fs.createWriteStream(mmdb_file);
await webdl(assetUrl, 'Downloading GeoIP database', true, fileStream);
}
export function removeMMDB(mmdb_file = MMDB_FILE) {
if (!fs.existsSync(mmdb_file)) {
console.log("GeoIP database not found.");
return;
}
fs.unlinkSync(mmdb_file);
console.log("GeoIP database removed.");
}
export async function getGeolocation(ip, mmdb_file = MMDB_FILE) {
if (!fs.existsSync(mmdb_file)) {
await downloadMMDB(mmdb_file);
}
validateIP(ip);
const reader = await maxmind.open(mmdb_file);
const resp = reader.get(ip);
const isoCode = resp.country?.iso_code.toUpperCase();
const location = resp.location;
if (!location?.longitude || !location?.latitude || !location?.time_zone || !isoCode) {
throw new UnknownIPLocation(`Unknown IP location: ${ip}`);
}
const locale = SELECTOR.fromRegion(isoCode);
return new Geolocation(locale, location.longitude, location.latitude, location.time_zone);
}
async function getUnicodeInfo() {
const data = await fs.promises.readFile(path.join(LOCAL_DATA.toString(), 'territoryInfo.xml'));
const parser = new xml2js.Parser();
return parser.parseStringPromise(data);
}
function asFloat(element, attr) {
return parseFloat(element[attr] || '0');
}
class StatisticalLocaleSelector {
root;
constructor() {
this.loadUnicodeInfo();
}
async loadUnicodeInfo() {
this.root = await getUnicodeInfo();
}
loadTerritoryData(isoCode) {
const territory = this.root.territoryInfo.territory.find((t) => t.$.type === isoCode);
if (!territory) {
throw new UnknownTerritory(`Unknown territory: ${isoCode}`);
}
const langPopulations = territory.languagePopulation;
if (!langPopulations) {
throw new Error(`No language data found for region: ${isoCode}`);
}
const languages = langPopulations.map((lang) => lang.$.type);
const percentages = langPopulations.map((lang) => asFloat(lang.$, 'populationPercent'));
return this.normalizeProbabilities(languages, percentages);
}
loadLanguageData(language) {
const territories = this.root.territory.filter((t) => t.languagePopulation.some((lp) => lp.$.type === language));
if (!territories.length) {
throw new UnknownLanguage(`No region data found for language: ${language}`);
}
const regions = [];
const percentages = [];
for (const terr of territories) {
const region = terr.$.type;
const langPop = terr.languagePopulation.find((lp) => lp.$.type === language);
if (region && langPop) {
regions.push(region);
percentages.push(asFloat(langPop.$, 'populationPercent') *
asFloat(terr.$, 'literacyPercent') / 10000 *
asFloat(terr.$, 'population'));
}
}
if (!regions.length) {
throw new Error(`No valid region data found for language: ${language}`);
}
return this.normalizeProbabilities(regions, percentages);
}
normalizeProbabilities(languages, freq) {
const total = freq.reduce((a, b) => a + b, 0);
return [languages, freq.map(f => f / total)];
}
weightedRandomChoice(items, weights) {
const cumulativeWeights = weights.map((sum => (value) => sum += value)(0));
const random = Math.random() * cumulativeWeights[cumulativeWeights.length - 1];
return items[cumulativeWeights.findIndex(weight => weight > random)];
}
fromRegion(region) {
const [languages, probabilities] = this.loadTerritoryData(region);
const language = this.weightedRandomChoice(languages, probabilities).replace('_', '-');
return normalizeLocale(`${language}-${region}`);
}
fromLanguage(language) {
const [regions, probabilities] = this.loadLanguageData(language);
const region = this.weightedRandomChoice(regions, probabilities);
return normalizeLocale(`${language}-${region}`);
}
}
const SELECTOR = new StatisticalLocaleSelector();