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.
Files changed (42) hide show
  1. package/LICENSE.md +373 -0
  2. package/README.md +121 -0
  3. package/bin/headfox-js.mjs +17 -0
  4. package/dist/__main__.d.ts +2 -0
  5. package/dist/__main__.js +131 -0
  6. package/dist/__version__.d.ts +8 -0
  7. package/dist/__version__.js +10 -0
  8. package/dist/addons.d.ts +17 -0
  9. package/dist/addons.js +74 -0
  10. package/dist/data-files/territoryInfo.xml +2024 -0
  11. package/dist/data-files/webgl_data.db +0 -0
  12. package/dist/exceptions.d.ts +82 -0
  13. package/dist/exceptions.js +165 -0
  14. package/dist/fingerprints.d.ts +4 -0
  15. package/dist/fingerprints.js +82 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +4 -0
  18. package/dist/ip.d.ts +25 -0
  19. package/dist/ip.js +90 -0
  20. package/dist/locale.d.ts +26 -0
  21. package/dist/locale.js +285 -0
  22. package/dist/mappings/browserforge.config.d.ts +47 -0
  23. package/dist/mappings/browserforge.config.js +72 -0
  24. package/dist/mappings/fonts.config.d.ts +6 -0
  25. package/dist/mappings/fonts.config.js +822 -0
  26. package/dist/mappings/warnings.config.d.ts +16 -0
  27. package/dist/mappings/warnings.config.js +27 -0
  28. package/dist/pkgman.d.ts +67 -0
  29. package/dist/pkgman.js +421 -0
  30. package/dist/server.d.ts +7 -0
  31. package/dist/server.js +24 -0
  32. package/dist/sync_api.d.ts +10 -0
  33. package/dist/sync_api.js +35 -0
  34. package/dist/utils.d.ts +109 -0
  35. package/dist/utils.js +540 -0
  36. package/dist/virtdisplay.d.ts +20 -0
  37. package/dist/virtdisplay.js +123 -0
  38. package/dist/warnings.d.ts +4 -0
  39. package/dist/warnings.js +33 -0
  40. package/dist/webgl/sample.d.ts +19 -0
  41. package/dist/webgl/sample.js +121 -0
  42. package/package.json +94 -0
package/dist/locale.js ADDED
@@ -0,0 +1,285 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import tags from "language-tags";
5
+ import maxmind from "maxmind";
6
+ import xml2js from "xml2js";
7
+ import { InvalidLocale, MissingRelease, NotInstalledGeoIPExtra, UnknownIPLocation, UnknownLanguage, UnknownTerritory, } from "./exceptions.js";
8
+ import { validateIP } from "./ip.js";
9
+ import { GitHubDownloader, INSTALL_DIR, webdl } from "./pkgman.js";
10
+ import { getAsBooleanFromENV } from "./utils.js";
11
+ import { LeakWarning } from "./warnings.js";
12
+ const currentDir = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
13
+ export const ALLOW_GEOIP = true;
14
+ class Locale {
15
+ language;
16
+ region;
17
+ script;
18
+ constructor(language, region, script) {
19
+ this.language = language;
20
+ this.region = region;
21
+ this.script = script;
22
+ }
23
+ asString() {
24
+ if (this.region) {
25
+ return `${this.language}-${this.region}`;
26
+ }
27
+ return this.language;
28
+ }
29
+ asConfig() {
30
+ if (!this.region) {
31
+ throw new Error("Region is required for config");
32
+ }
33
+ const data = {
34
+ "locale:region": this.region,
35
+ "locale:language": this.language,
36
+ };
37
+ if (this.script) {
38
+ data["locale:script"] = this.script;
39
+ }
40
+ return data;
41
+ }
42
+ }
43
+ class Geolocation {
44
+ locale;
45
+ longitude;
46
+ latitude;
47
+ timezone;
48
+ accuracy;
49
+ constructor(locale, longitude, latitude, timezone, accuracy) {
50
+ this.locale = locale;
51
+ this.longitude = longitude;
52
+ this.latitude = latitude;
53
+ this.timezone = timezone;
54
+ this.accuracy = accuracy;
55
+ }
56
+ asConfig() {
57
+ const data = {
58
+ "geolocation:longitude": this.longitude,
59
+ "geolocation:latitude": this.latitude,
60
+ timezone: this.timezone,
61
+ ...this.locale.asConfig(),
62
+ };
63
+ if (this.accuracy !== undefined) {
64
+ data["geolocation:accuracy"] = this.accuracy;
65
+ }
66
+ return data;
67
+ }
68
+ }
69
+ function verifyLocale(loc) {
70
+ if (tags.check(loc)) {
71
+ return;
72
+ }
73
+ throw InvalidLocale.invalidInput(loc);
74
+ }
75
+ export function normalizeLocale(locale) {
76
+ verifyLocale(locale);
77
+ const parser = tags(locale);
78
+ if (!parser.region) {
79
+ throw InvalidLocale.invalidInput(locale);
80
+ }
81
+ return new Locale(parser.language()?.format() ?? "en", parser.region()?.format(), parser.language()?.script()?.format());
82
+ }
83
+ export function handleLocale(locale, ignoreRegion = false) {
84
+ if (locale.length > 3) {
85
+ return normalizeLocale(locale);
86
+ }
87
+ try {
88
+ return SELECTOR.fromRegion(locale);
89
+ }
90
+ catch (e) {
91
+ if (e instanceof UnknownTerritory) {
92
+ }
93
+ else {
94
+ throw e;
95
+ }
96
+ }
97
+ if (ignoreRegion) {
98
+ verifyLocale(locale);
99
+ return new Locale(locale);
100
+ }
101
+ try {
102
+ const language = SELECTOR.fromLanguage(locale);
103
+ LeakWarning.warn("no_region");
104
+ return language;
105
+ }
106
+ catch (e) {
107
+ if (e instanceof UnknownLanguage) {
108
+ }
109
+ else {
110
+ throw e;
111
+ }
112
+ }
113
+ throw InvalidLocale.invalidInput(locale);
114
+ }
115
+ export function handleLocales(locales, config) {
116
+ if (typeof locales === "string") {
117
+ locales = locales.split(",").map((loc) => loc.trim());
118
+ }
119
+ const intlLocale = handleLocale(locales[0]).asConfig();
120
+ for (const key in intlLocale) {
121
+ config[key] = intlLocale[key];
122
+ }
123
+ if (locales.length < 2) {
124
+ return;
125
+ }
126
+ config["locale:all"] = joinUnique(locales.map((locale) => handleLocale(locale, true).asString()));
127
+ }
128
+ function joinUnique(seq) {
129
+ const seen = new Set();
130
+ return seq.filter((x) => !seen.has(x) && seen.add(x)).join(", ");
131
+ }
132
+ const MMDB_FILE = path.join(INSTALL_DIR.toString(), "GeoLite2-City.mmdb");
133
+ const MMDB_REPO = "P3TERX/GeoLite.mmdb";
134
+ class MaxMindDownloader extends GitHubDownloader {
135
+ checkAsset(asset) {
136
+ if (asset.name.endsWith("-City.mmdb")) {
137
+ return asset.browser_download_url;
138
+ }
139
+ return null;
140
+ }
141
+ missingAssetError() {
142
+ throw new MissingRelease("Failed to find GeoIP database release asset");
143
+ }
144
+ }
145
+ export function geoipAllowed() {
146
+ if (!ALLOW_GEOIP) {
147
+ throw new NotInstalledGeoIPExtra("GeoIP support is unavailable in this build. Install the required MaxMind dependencies or disable the geoip option.");
148
+ }
149
+ }
150
+ export async function downloadMMDB() {
151
+ geoipAllowed();
152
+ if (getAsBooleanFromENV("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", false)) {
153
+ console.log("Skipping GeoIP database download due to PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD set!");
154
+ return;
155
+ }
156
+ const assetUrl = await new MaxMindDownloader(MMDB_REPO).getAsset();
157
+ const fileStream = fs.createWriteStream(MMDB_FILE);
158
+ await webdl(assetUrl, "Downloading GeoIP database", true, fileStream);
159
+ }
160
+ export function removeMMDB() {
161
+ if (!fs.existsSync(MMDB_FILE)) {
162
+ console.log("GeoIP database not found.");
163
+ return;
164
+ }
165
+ fs.unlinkSync(MMDB_FILE);
166
+ console.log("GeoIP database removed.");
167
+ }
168
+ export async function getGeolocation(ip) {
169
+ if (!fs.existsSync(MMDB_FILE)) {
170
+ await downloadMMDB();
171
+ }
172
+ validateIP(ip);
173
+ const reader = await maxmind.open(MMDB_FILE);
174
+ const resp = reader.get(ip);
175
+ if (!resp) {
176
+ throw new UnknownIPLocation(`Unknown IP location: ${ip}`);
177
+ }
178
+ const isoCode = resp.country?.iso_code?.toUpperCase();
179
+ const location = resp.location;
180
+ if (!location?.longitude ||
181
+ !location?.latitude ||
182
+ !location?.time_zone ||
183
+ !isoCode) {
184
+ throw new UnknownIPLocation(`Unknown IP location: ${ip}`);
185
+ }
186
+ const locale = SELECTOR.fromRegion(isoCode);
187
+ return new Geolocation(locale, location.longitude, location.latitude, location.time_zone);
188
+ }
189
+ async function getUnicodeInfo() {
190
+ const data = await fs.promises.readFile(path.join(currentDir, "data-files", "territoryInfo.xml"));
191
+ const parser = new xml2js.Parser();
192
+ return parser.parseStringPromise(data);
193
+ }
194
+ function asFloat(element, attr) {
195
+ return parseFloat(element[attr] || "0");
196
+ }
197
+ class StatisticalLocaleSelector {
198
+ root;
199
+ constructor() {
200
+ this.loadUnicodeInfo();
201
+ }
202
+ async loadUnicodeInfo() {
203
+ this.root = await getUnicodeInfo();
204
+ }
205
+ loadTerritoryData(isoCode) {
206
+ const territory = this.root.territoryInfo.territory.find((t) => t.$.type === isoCode);
207
+ if (!territory) {
208
+ throw new UnknownTerritory(`Unknown territory: ${isoCode}`);
209
+ }
210
+ const langPopulations = territory.languagePopulation;
211
+ if (!langPopulations) {
212
+ throw new Error(`No language data found for region: ${isoCode}`);
213
+ }
214
+ const languages = langPopulations.map((lang) => lang.$.type);
215
+ const percentages = langPopulations.map((lang) => asFloat(lang.$, "populationPercent"));
216
+ return this.normalizeProbabilities(languages, percentages);
217
+ }
218
+ loadLanguageData(language) {
219
+ const territories = this.root.territory.filter((t) => t.languagePopulation.some((lp) => lp.$.type === language));
220
+ if (!territories.length) {
221
+ throw new UnknownLanguage(`No region data found for language: ${language}`);
222
+ }
223
+ const regions = [];
224
+ const percentages = [];
225
+ for (const terr of territories) {
226
+ const region = terr.$.type;
227
+ const langPop = terr.languagePopulation.find((lp) => lp.$.type === language);
228
+ if (region && langPop) {
229
+ regions.push(region);
230
+ percentages.push(((asFloat(langPop.$, "populationPercent") *
231
+ asFloat(terr.$, "literacyPercent")) /
232
+ 10000) *
233
+ asFloat(terr.$, "population"));
234
+ }
235
+ }
236
+ if (!regions.length) {
237
+ throw new Error(`No valid region data found for language: ${language}`);
238
+ }
239
+ return this.normalizeProbabilities(regions, percentages);
240
+ }
241
+ normalizeProbabilities(languages, freq) {
242
+ const total = freq.reduce((a, b) => a + b, 0);
243
+ return [languages, freq.map((f) => f / total)];
244
+ }
245
+ weightedRandomChoice(items, weights) {
246
+ if (items.length === 0) {
247
+ throw new Error("items must not be empty");
248
+ }
249
+ if (items.length !== weights.length) {
250
+ throw new Error("items and weights must have the same length");
251
+ }
252
+ let total = 0;
253
+ for (const w of weights) {
254
+ if (w < 0) {
255
+ throw new Error("weights must be non-negative");
256
+ }
257
+ total += w;
258
+ }
259
+ // Fallback to uniform choice if all weights are zero
260
+ if (total === 0) {
261
+ return items[Math.floor(Math.random() * items.length)];
262
+ }
263
+ const r = Math.random() * total;
264
+ let acc = 0;
265
+ for (let i = 0; i < items.length; i++) {
266
+ acc += weights[i];
267
+ if (r < acc) {
268
+ return items[i];
269
+ }
270
+ }
271
+ // Numerical edge case
272
+ return items[items.length - 1];
273
+ }
274
+ fromRegion(region) {
275
+ const [languages, probabilities] = this.loadTerritoryData(region);
276
+ const language = this.weightedRandomChoice(languages, probabilities).replace("_", "-");
277
+ return normalizeLocale(`${language}-${region}`);
278
+ }
279
+ fromLanguage(language) {
280
+ const [regions, probabilities] = this.loadLanguageData(language);
281
+ const region = this.weightedRandomChoice(regions, probabilities);
282
+ return normalizeLocale(`${language}-${region}`);
283
+ }
284
+ }
285
+ const SELECTOR = new StatisticalLocaleSelector();
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Mappings of Browserforge fingerprints to Camoufox config properties.
3
+ */
4
+ declare const _default: {
5
+ navigator: {
6
+ userAgent: string;
7
+ doNotTrack: string;
8
+ appCodeName: string;
9
+ appName: string;
10
+ appVersion: string;
11
+ oscpu: string;
12
+ platform: string;
13
+ hardwareConcurrency: string;
14
+ product: string;
15
+ maxTouchPoints: string;
16
+ extraProperties: {
17
+ globalPrivacyControl: string;
18
+ };
19
+ };
20
+ screen: {
21
+ availLeft: string;
22
+ availTop: string;
23
+ availWidth: string;
24
+ availHeight: string;
25
+ height: string;
26
+ width: string;
27
+ colorDepth: string;
28
+ pixelDepth: string;
29
+ pageXOffset: string;
30
+ pageYOffset: string;
31
+ outerHeight: string;
32
+ outerWidth: string;
33
+ innerHeight: string;
34
+ innerWidth: string;
35
+ screenX: string;
36
+ screenY: string;
37
+ };
38
+ headers: {
39
+ "Accept-Encoding": string;
40
+ };
41
+ battery: {
42
+ charging: string;
43
+ chargingTime: string;
44
+ dischargingTime: string;
45
+ };
46
+ };
47
+ export default _default;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Mappings of Browserforge fingerprints to Camoufox config properties.
3
+ */
4
+ export default {
5
+ navigator: {
6
+ // Note: Browserforge tends to have outdated UAs.
7
+ // The version will be replaced in Camoufox.
8
+ userAgent: "navigator.userAgent",
9
+ // userAgentData not in Firefox
10
+ doNotTrack: "navigator.doNotTrack",
11
+ appCodeName: "navigator.appCodeName",
12
+ appName: "navigator.appName",
13
+ appVersion: "navigator.appVersion",
14
+ oscpu: "navigator.oscpu",
15
+ // webdriver is always True
16
+ // Locale is now implemented separately:
17
+ // language: "navigator.language",
18
+ // languages: "navigator.languages",
19
+ platform: "navigator.platform",
20
+ // deviceMemory not in Firefox
21
+ hardwareConcurrency: "navigator.hardwareConcurrency",
22
+ product: "navigator.product",
23
+ // Never override productSub #105
24
+ // productSub: "navigator.productSub",
25
+ // vendor is not necessary
26
+ // vendorSub is not necessary
27
+ maxTouchPoints: "navigator.maxTouchPoints",
28
+ extraProperties: {
29
+ // Note: Changing pdfViewerEnabled is not recommended. This will be kept to True.
30
+ globalPrivacyControl: "navigator.globalPrivacyControl",
31
+ },
32
+ },
33
+ screen: {
34
+ // hasHDR is not implemented in Camoufox
35
+ availLeft: "screen.availLeft",
36
+ availTop: "screen.availTop",
37
+ availWidth: "screen.availWidth",
38
+ availHeight: "screen.availHeight",
39
+ height: "screen.height",
40
+ width: "screen.width",
41
+ colorDepth: "screen.colorDepth",
42
+ pixelDepth: "screen.pixelDepth",
43
+ // devicePixelRatio is not recommended. Any value other than 1.0 is suspicious.
44
+ pageXOffset: "screen.pageXOffset",
45
+ pageYOffset: "screen.pageYOffset",
46
+ outerHeight: "window.outerHeight",
47
+ outerWidth: "window.outerWidth",
48
+ innerHeight: "window.innerHeight",
49
+ innerWidth: "window.innerWidth",
50
+ screenX: "window.screenX",
51
+ screenY: "window.screenY",
52
+ // Tends to generate out of bounds (network inconsistencies):
53
+ // clientWidth: "document.body.clientWidth",
54
+ // clientHeight: "document.body.clientHeight",
55
+ },
56
+ // videoCard: {
57
+ // renderer: "webgl:renderer",
58
+ // vendor: "webgl:vendor",
59
+ // },
60
+ headers: {
61
+ // headers.User-Agent is redundant with navigator.userAgent
62
+ // headers.Accept-Language is redundant with locale:*
63
+ "Accept-Encoding": "headers.Accept-Encoding",
64
+ },
65
+ battery: {
66
+ charging: "battery:charging",
67
+ chargingTime: "battery:chargingTime",
68
+ dischargingTime: "battery:dischargingTime",
69
+ },
70
+ // Unsupported: videoCodecs, audioCodecs, pluginsData, multimediaDevices
71
+ // Fonts are listed through the launcher.
72
+ };
@@ -0,0 +1,6 @@
1
+ declare const _default: {
2
+ win: string[];
3
+ mac: string[];
4
+ lin: string[];
5
+ };
6
+ export default _default;