@hieutran094/camoufox-js 0.6.2
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 +70 -0
- package/dist/__main__.d.ts +2 -0
- package/dist/__main__.js +137 -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/browserforge.yml +68 -0
- package/dist/data-files/fonts.json +11 -0
- package/dist/data-files/territoryInfo.xml +2024 -0
- package/dist/data-files/warnings.yml +51 -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 +84 -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 +252 -0
- package/dist/pkgman.d.ts +65 -0
- package/dist/pkgman.js +348 -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 +89 -0
- package/dist/utils.js +497 -0
- package/dist/virtdisplay.d.ts +16 -0
- package/dist/virtdisplay.js +103 -0
- package/dist/warnings.d.ts +4 -0
- package/dist/warnings.js +32 -0
- package/dist/webgl/sample.d.ts +19 -0
- package/dist/webgl/sample.js +81 -0
- package/package.json +60 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { CannotExecuteXvfb, CannotFindXvfb, VirtualDisplayNotSupported, } from './exceptions.js';
|
|
2
|
+
import { OS_NAME } from './pkgman.js';
|
|
3
|
+
import { execFileSync, spawn } from 'child_process';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
// import { globSync } from 'glob';
|
|
7
|
+
import { randomInt } from 'crypto';
|
|
8
|
+
// import { Lock } from 'async-mutex';
|
|
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", "0", "1x1x24",
|
|
20
|
+
"-ac",
|
|
21
|
+
"-nolisten", "tcp",
|
|
22
|
+
"-extension", "RENDER",
|
|
23
|
+
"+extension", "GLX",
|
|
24
|
+
"-extension", "COMPOSITE",
|
|
25
|
+
"-extension", "XVideo",
|
|
26
|
+
"-extension", "XVideo-MotionCompensation",
|
|
27
|
+
"-extension", "XINERAMA",
|
|
28
|
+
"-shmem",
|
|
29
|
+
"-fp", "built-ins",
|
|
30
|
+
"-nocursor",
|
|
31
|
+
"-br",
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
get xvfb_path() {
|
|
35
|
+
const path = execFileSync('which', ['Xvfb']).toString().trim();
|
|
36
|
+
if (!path) {
|
|
37
|
+
throw new CannotFindXvfb("Please install Xvfb to use headless mode.");
|
|
38
|
+
}
|
|
39
|
+
if (!existsSync(path) || !execFileSync('test', ['-x', path])) {
|
|
40
|
+
throw new CannotExecuteXvfb(`I do not have permission to execute Xvfb: ${path}`);
|
|
41
|
+
}
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
get xvfb_cmd() {
|
|
45
|
+
return [this.xvfb_path, `:${this.display}`, ...this.xvfb_args];
|
|
46
|
+
}
|
|
47
|
+
execute_xvfb() {
|
|
48
|
+
if (this.debug) {
|
|
49
|
+
console.log('Starting virtual display:', this.xvfb_cmd.join(' '));
|
|
50
|
+
}
|
|
51
|
+
this.proc = spawn(this.xvfb_cmd[0], this.xvfb_cmd.slice(1), {
|
|
52
|
+
stdio: this.debug ? 'inherit' : 'ignore',
|
|
53
|
+
detached: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
get() {
|
|
57
|
+
VirtualDisplay.assert_linux();
|
|
58
|
+
// this._lock.runExclusive(() => {
|
|
59
|
+
if (!this.proc) {
|
|
60
|
+
this.execute_xvfb();
|
|
61
|
+
}
|
|
62
|
+
else if (this.debug) {
|
|
63
|
+
console.log(`Using virtual display: ${this.display}`);
|
|
64
|
+
}
|
|
65
|
+
// });
|
|
66
|
+
return `:${this.display}`;
|
|
67
|
+
}
|
|
68
|
+
kill() {
|
|
69
|
+
// this._lock.runExclusive(() => {
|
|
70
|
+
if (this.proc && !this.proc.killed) {
|
|
71
|
+
if (this.debug) {
|
|
72
|
+
console.log('Terminating virtual display:', this.display);
|
|
73
|
+
}
|
|
74
|
+
this.proc.kill();
|
|
75
|
+
}
|
|
76
|
+
// });
|
|
77
|
+
}
|
|
78
|
+
static _get_lock_files() {
|
|
79
|
+
const tmpd = process.env.TMPDIR || tmpdir();
|
|
80
|
+
try {
|
|
81
|
+
return [];
|
|
82
|
+
// return globSync(join(tmpd, ".X*-lock")).filter(p => existsSync(p));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
static _free_display() {
|
|
89
|
+
const ls = VirtualDisplay._get_lock_files().map(x => parseInt(x.split("X")[1].split("-")[0]));
|
|
90
|
+
return ls.length ? Math.max(99, Math.max(...ls) + randomInt(3, 20)) : 99;
|
|
91
|
+
}
|
|
92
|
+
get display() {
|
|
93
|
+
if (this._display === null) {
|
|
94
|
+
this._display = VirtualDisplay._free_display();
|
|
95
|
+
}
|
|
96
|
+
return this._display;
|
|
97
|
+
}
|
|
98
|
+
static assert_linux() {
|
|
99
|
+
if (OS_NAME !== 'lin') {
|
|
100
|
+
throw new VirtualDisplayNotSupported("Virtual display is only supported on Linux.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/dist/warnings.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { loadYaml } from './pkgman.js';
|
|
3
|
+
const WARNINGS_DATA = loadYaml(join(import.meta.dirname, 'data-files', 'warnings.yml'));
|
|
4
|
+
export class LeakWarning extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'LeakWarning';
|
|
8
|
+
}
|
|
9
|
+
static warn(warningKey, iKnowWhatImDoing) {
|
|
10
|
+
let warning = WARNINGS_DATA[warningKey];
|
|
11
|
+
if (iKnowWhatImDoing) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (iKnowWhatImDoing !== undefined) {
|
|
15
|
+
warning += '\nIf this is intentional, pass `iKnowWhatImDoing=true`.';
|
|
16
|
+
}
|
|
17
|
+
const currentModule = import.meta.dirname;
|
|
18
|
+
const originalStackTrace = Error.prepareStackTrace;
|
|
19
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
20
|
+
const err = new Error();
|
|
21
|
+
const stack = err.stack;
|
|
22
|
+
Error.prepareStackTrace = originalStackTrace;
|
|
23
|
+
for (const frame of stack) {
|
|
24
|
+
const frameFileName = frame.getFileName();
|
|
25
|
+
if (frameFileName && !frameFileName.startsWith(currentModule)) {
|
|
26
|
+
console.warn(`${warning} at ${frameFileName}:${frame.getLineNumber()}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
console.warn(warning);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface WebGLData {
|
|
2
|
+
vendor: string;
|
|
3
|
+
renderer: string;
|
|
4
|
+
data: string;
|
|
5
|
+
win: number;
|
|
6
|
+
mac: number;
|
|
7
|
+
lin: number;
|
|
8
|
+
webGl2Enabled: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function sampleWebGL(os: 'win' | 'mac' | 'lin', vendor?: string, renderer?: string): Promise<WebGLData>;
|
|
11
|
+
interface VendorRenderer {
|
|
12
|
+
vendor: string;
|
|
13
|
+
renderer: string;
|
|
14
|
+
}
|
|
15
|
+
interface PossiblePairs {
|
|
16
|
+
[key: string]: Array<VendorRenderer>;
|
|
17
|
+
}
|
|
18
|
+
export declare function getPossiblePairs(): Promise<PossiblePairs>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { OS_ARCH_MATRIX } from '../pkgman.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
// Get database path relative to this file
|
|
5
|
+
const DB_PATH = path.join(import.meta.dirname, '..', 'data-files', 'webgl_data.db');
|
|
6
|
+
export async function sampleWebGL(os, vendor, renderer) {
|
|
7
|
+
if (!OS_ARCH_MATRIX[os]) {
|
|
8
|
+
throw new Error(`Invalid OS: ${os}. Must be one of: win, mac, lin`);
|
|
9
|
+
}
|
|
10
|
+
const db = new Database(DB_PATH);
|
|
11
|
+
let query = '';
|
|
12
|
+
let params = [];
|
|
13
|
+
if (vendor && renderer) {
|
|
14
|
+
query = `SELECT vendor, renderer, data, ${os} FROM webgl_fingerprints WHERE vendor = ? AND renderer = ?`;
|
|
15
|
+
params = [vendor, renderer];
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
query = `SELECT vendor, renderer, data, ${os} FROM webgl_fingerprints WHERE ${os} > 0`;
|
|
19
|
+
}
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
try {
|
|
22
|
+
const rows = db.prepare(query).all(...params);
|
|
23
|
+
if (rows.length === 0) {
|
|
24
|
+
reject(new Error(`No WebGL data found for OS: ${os}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (vendor && renderer) {
|
|
28
|
+
const result = rows[0];
|
|
29
|
+
if (result[os] <= 0) {
|
|
30
|
+
const pairs = db.prepare(`SELECT DISTINCT vendor, renderer FROM webgl_fingerprints WHERE ${os} > 0`).all();
|
|
31
|
+
reject(new Error(`Vendor "${vendor}" and renderer "${renderer}" combination not valid for ${os}. Possible pairs: ${pairs.map((pair) => `${pair.vendor}, ${pair.renderer}`).join(', ')}`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
resolve(JSON.parse(result.data));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const dataStrs = rows.map(row => row.data);
|
|
38
|
+
const probs = rows.map(row => row[os]);
|
|
39
|
+
const probsArray = probs.map(p => p / probs.reduce((a, b) => a + b, 0));
|
|
40
|
+
function weightedRandomChoice(weights) {
|
|
41
|
+
const sum = weights.reduce((acc, weight) => acc + weight, 0);
|
|
42
|
+
const threshold = Math.random() * sum;
|
|
43
|
+
let cumulativeSum = 0;
|
|
44
|
+
for (let i = 0; i < weights.length; i++) {
|
|
45
|
+
cumulativeSum += weights[i];
|
|
46
|
+
if (cumulativeSum >= threshold) {
|
|
47
|
+
return i;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return weights.length - 1; // Fallback in case of rounding errors
|
|
51
|
+
}
|
|
52
|
+
const idx = weightedRandomChoice(probsArray);
|
|
53
|
+
resolve(JSON.parse(dataStrs[idx]));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
reject(err);
|
|
58
|
+
}
|
|
59
|
+
}).finally(() => {
|
|
60
|
+
db.close();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function getPossiblePairs() {
|
|
64
|
+
const db = new Database(DB_PATH);
|
|
65
|
+
const result = {};
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
try {
|
|
68
|
+
const osTypes = Object.keys(OS_ARCH_MATRIX);
|
|
69
|
+
osTypes.forEach(os_type => {
|
|
70
|
+
const rows = db.prepare(`SELECT DISTINCT vendor, renderer FROM webgl_fingerprints WHERE ${os_type} > 0 ORDER BY ${os_type} DESC`).all();
|
|
71
|
+
result[os_type] = rows;
|
|
72
|
+
});
|
|
73
|
+
resolve(result);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
reject(err);
|
|
77
|
+
}
|
|
78
|
+
}).finally(() => {
|
|
79
|
+
db.close();
|
|
80
|
+
});
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hieutran094/camoufox-js",
|
|
3
|
+
"version": "0.6.2",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "rimraf dist && tsc && npm run copy-files",
|
|
8
|
+
"test": "vitest",
|
|
9
|
+
"copy-files": "cp -r src/data-files dist/data-files"
|
|
10
|
+
},
|
|
11
|
+
"bin": "dist/__main__.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/apify/camoufox-js"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/apify/camoufox-js/issues"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">= 20"
|
|
27
|
+
},
|
|
28
|
+
"license": "MPL-2.0",
|
|
29
|
+
"description": "Experimental JS port of Camoufox for Python.",
|
|
30
|
+
"packageManager": "yarn@4.9.4",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"adm-zip": "^0.5.16",
|
|
33
|
+
"better-sqlite3": "^12.2.0",
|
|
34
|
+
"commander": "^14.0.0",
|
|
35
|
+
"fingerprint-generator": "^2.1.66",
|
|
36
|
+
"impit": "^0.5.0",
|
|
37
|
+
"js-yaml": "^4.1.0",
|
|
38
|
+
"language-tags": "^2.0.1",
|
|
39
|
+
"maxmind": "^5.0.0",
|
|
40
|
+
"progress": "^2.0.3",
|
|
41
|
+
"ua-parser-js": "^2.0.2",
|
|
42
|
+
"xml2js": "^0.6.2"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/adm-zip": "^0.5.7",
|
|
46
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
47
|
+
"@types/js-yaml": "^4.0.9",
|
|
48
|
+
"@types/language-tags": "^1.0.4",
|
|
49
|
+
"@types/node": "^22.13.2",
|
|
50
|
+
"@types/progress": "^2.0.7",
|
|
51
|
+
"@types/xml2js": "^0.4.14",
|
|
52
|
+
"playwright-core": "^1.53.1",
|
|
53
|
+
"rimraf": "^6.0.1",
|
|
54
|
+
"typescript": "^5.8.3",
|
|
55
|
+
"vitest": "^3.1.1"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"playwright-core": "*"
|
|
59
|
+
}
|
|
60
|
+
}
|