camoufox
Version:
JavaScript port of Camoufox - a tool for Firefox anti-fingerprinting and browser automation.
324 lines (323 loc) • 11.2 kB
JavaScript
import { CONSTRAINTS } from './__version__.js';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { execSync } from 'child_process';
import { CamoufoxNotInstalled, FileNotFoundError, MissingRelease, UnsupportedArchitecture, UnsupportedOS, UnsupportedVersion, } from './exceptions.js';
import AdmZip from 'adm-zip';
import * as yaml from 'js-yaml';
import ProgressBar from 'progress';
const ARCH_MAP = {
'x64': 'x86_64',
'ia32': 'i686',
'arm64': 'arm64',
'arm': 'arm64',
};
const OS_MAP = {
'darwin': 'mac',
'linux': 'lin',
'win32': 'win',
};
if (!(process.platform in OS_MAP)) {
throw new UnsupportedOS(`OS ${process.platform} is not supported`);
}
export const OS_NAME = OS_MAP[process.platform];
export const INSTALL_DIR = userCacheDir('camoufox');
export const LOCAL_DATA = path.join(import.meta?.dirname ?? __dirname, '../data-files');
export const PACKAGE_DATA = path.join(process.cwd(), 'data-files');
export const OS_ARCH_MATRIX = {
'win': ['x86_64', 'i686'],
'mac': ['x86_64', 'arm64'],
'lin': ['x86_64', 'arm64', 'i686'],
};
const LAUNCH_FILE = {
'win': 'camoufox.exe',
'mac': '../MacOS/camoufox',
'lin': 'camoufox-bin',
};
class Version {
release;
version;
sorted_rel;
constructor(release, version) {
this.release = release;
this.version = version;
this.sorted_rel = this.buildSortedRel();
}
buildSortedRel() {
const parts = this.release.split('.').map(x => (isNaN(Number(x)) ? x.charCodeAt(0) - 1024 : Number(x)));
while (parts.length < 5) {
parts.push(0);
}
return parts;
}
get fullString() {
return `${this.version}-${this.release}`;
}
equals(other) {
return this.sorted_rel.join('.') === other.sorted_rel.join('.');
}
lessThan(other) {
for (let i = 0; i < this.sorted_rel.length; i++) {
if (this.sorted_rel[i] < other.sorted_rel[i])
return true;
if (this.sorted_rel[i] > other.sorted_rel[i])
return false;
}
return false;
}
isSupported() {
return VERSION_MIN.lessThan(this) && this.lessThan(VERSION_MAX);
}
static fromPath(filePath = INSTALL_DIR) {
const versionPath = path.join(filePath.toString(), 'version.json');
if (!fs.existsSync(versionPath)) {
throw new FileNotFoundError(`Version information not found at ${versionPath}. Please run \`camoufox fetch\` to install.`);
}
const versionData = JSON.parse(fs.readFileSync(versionPath, 'utf-8'));
return new Version(versionData.release, versionData.version);
}
static isSupportedPath(path) {
return Version.fromPath(path).isSupported();
}
static buildMinMax() {
return [new Version(CONSTRAINTS.MIN_VERSION), new Version(CONSTRAINTS.MAX_VERSION)];
}
}
const [VERSION_MIN, VERSION_MAX] = Version.buildMinMax();
export class GitHubDownloader {
githubRepo;
apiUrl;
constructor(githubRepo) {
this.githubRepo = githubRepo;
this.apiUrl = `https://api.github.com/repos/${githubRepo}/releases`;
}
checkAsset(asset) {
return asset.browser_download_url;
}
missingAssetError() {
throw new MissingRelease(`Could not find a release asset in ${this.githubRepo}.`);
}
async getAsset() {
const resp = await fetch(this.apiUrl);
if (!resp.ok) {
throw new Error(`Failed to fetch releases from ${this.apiUrl}`);
}
const releases = await resp.json();
for (const release of releases) {
for (const asset of release.assets) {
const data = this.checkAsset(asset);
if (data) {
return data;
}
}
}
this.missingAssetError();
}
}
export class CamoufoxFetcher extends GitHubDownloader {
arch;
_version_obj;
pattern;
_url;
installDir;
constructor(installDir = INSTALL_DIR) {
super("daijro/camoufox");
this.arch = CamoufoxFetcher.getPlatformArch();
this.pattern = new RegExp(`camoufox-(.+)-(.+)-${OS_NAME}\\.${this.arch}\\.zip`);
this.installDir = installDir;
}
async init() {
await this.fetchLatest();
}
checkAsset(asset) {
const match = asset.name.match(this.pattern);
if (!match)
return null;
const version = new Version(match[2], match[1]);
if (!version.isSupported())
return null;
return [version, asset.browser_download_url];
}
missingAssetError() {
throw new MissingRelease(`No matching release found for ${OS_NAME} ${this.arch} in the supported range: (${CONSTRAINTS.asRange()}). Please update the library.`);
}
static getPlatformArch() {
const platArch = os.arch().toLowerCase();
if (!(platArch in ARCH_MAP)) {
throw new UnsupportedArchitecture(`Architecture ${platArch} is not supported`);
}
const arch = ARCH_MAP[platArch];
if (!OS_ARCH_MATRIX[OS_NAME].includes(arch)) {
throw new UnsupportedArchitecture(`Architecture ${arch} is not supported for ${OS_NAME}`);
}
return arch;
}
async fetchLatest() {
if (this._version_obj)
return;
const releaseData = await this.getAsset();
this._version_obj = releaseData[0];
this._url = releaseData[1];
}
static async downloadFile(url) {
const response = await fetch(url);
return Buffer.from(await response.arrayBuffer());
}
async extractZip(zipFile) {
const zip = AdmZip(zipFile);
zip.extractAllTo(this.installDir.toString(), true);
}
static cleanup(installDir = INSTALL_DIR) {
if (fs.existsSync(installDir)) {
fs.rmSync(installDir, { recursive: true });
return true;
}
return false;
}
setVersion() {
fs.writeFileSync(path.join(this.installDir.toString(), 'version.json'), JSON.stringify({ version: this.version, release: this.release }));
}
async install() {
await this.init();
await CamoufoxFetcher.cleanup(this.installDir);
try {
fs.mkdirSync(this.installDir, { recursive: true });
const zipFile = await webdl(this.url, 'Downloading Camoufox...', true);
await this.extractZip(zipFile);
this.setVersion();
if (OS_NAME !== 'win') {
execSync(`chmod -R 755 ${this.installDir}`);
}
console.log('Camoufox successfully installed.');
}
catch (e) {
console.error(`Error installing Camoufox: ${e}`);
await CamoufoxFetcher.cleanup(this.installDir);
throw e;
}
}
get url() {
if (!this._url) {
throw new Error("Url is not available. Make sure to run fetchLatest first.");
}
return this._url;
}
get version() {
if (!this._version_obj || !this._version_obj.version) {
throw new Error("Version is not available. Make sure to run fetchLatest first.");
}
return this._version_obj.version;
}
get release() {
if (!this._version_obj) {
throw new Error("Release information is not available. Make sure to run the installation first.");
}
return this._version_obj.release;
}
get verstr() {
if (!this._version_obj) {
throw new Error("Version is not available. Make sure to run the installation first.");
}
return this._version_obj.fullString;
}
}
function userCacheDir(appName) {
if (OS_NAME === 'win') {
return path.join(os.homedir(), 'AppData', 'Local', appName, appName, 'Cache');
}
else if (OS_NAME === 'mac') {
return path.join(os.homedir(), 'Library', 'Caches', appName);
}
else {
return path.join(os.homedir(), '.cache', appName);
}
}
export function installedVerStr() {
return Version.fromPath().fullString;
}
export function camoufoxPath(installDir = INSTALL_DIR, downloadIfMissing = true) {
// Ensure the directory exists and is not empty
if (!fs.existsSync(installDir) || fs.readdirSync(installDir).length === 0) {
if (!downloadIfMissing) {
throw new Error(`Camoufox executable not found at ${installDir}`);
}
}
else if (fs.existsSync(installDir) && Version.isSupportedPath(installDir)) {
return installDir;
}
else {
if (!downloadIfMissing) {
throw new UnsupportedVersion("Camoufox executable is outdated.");
}
}
// Install and recheck
const fetcher = new CamoufoxFetcher();
fetcher.install().then(() => camoufoxPath(installDir, downloadIfMissing));
return installDir;
}
export function getPath(file, installDir = INSTALL_DIR) {
if (OS_NAME === 'mac') {
return path.resolve(camoufoxPath(installDir).toString(), 'Camoufox.app', 'Contents', 'Resources', file);
}
return path.join(camoufoxPath(installDir).toString(), file);
}
export function getLaunchPath(installDir = INSTALL_DIR) {
return getPath(LAUNCH_FILE[OS_NAME], installDir);
}
export function launchPath(installDir = INSTALL_DIR) {
const launchPath = getPath(LAUNCH_FILE[OS_NAME], installDir);
if (!fs.existsSync(launchPath)) {
throw new CamoufoxNotInstalled(`Camoufox is not installed at ${launchPath}. Please run \`camoufox fetch\` to install.`);
}
return launchPath;
}
export async function webdl(url, desc = '', bar = true, buffer = null) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download file from ${url}`);
}
const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
const progressBar = bar ? new ProgressBar(`${desc} [:bar] :percent :etas`, {
total: totalSize,
width: 40,
}) : null;
const chunks = [];
for await (const chunk of response.body) {
if (buffer) {
buffer.write(chunk);
}
else {
chunks.push(chunk);
}
if (progressBar) {
progressBar.tick(chunk.length, "X");
}
}
const fileBuffer = Buffer.concat(chunks);
return fileBuffer;
}
export async function unzip(zipFile, extractPath, desc, bar = true) {
const zip = AdmZip(zipFile);
const zipEntries = zip.getEntries();
if (bar) {
console.log(desc || 'Extracting files...');
}
for (const entry of zipEntries) {
if (bar) {
console.log(`Extracting ${entry.entryName}`);
}
zip.extractEntryTo(entry, extractPath, false, true);
}
if (bar) {
console.log('Extraction complete.');
}
}
export function loadYaml(file) {
let filePath = file;
if (!path.isAbsolute(file)) {
filePath = path.join(LOCAL_DATA.toString(), file);
}
const fileContents = fs.readFileSync(filePath, 'utf8');
return yaml.load(fileContents);
}