forked from tiliondev/fortress
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
148 lines (132 loc) · 6.64 KB
/
Copy pathindex.js
File metadata and controls
148 lines (132 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// tilion-fortress (Node) — install & drive the Fortress stealth Chromium engine.
// Ships the prebuilt binary only (no engine source). Detects platform, downloads the
// matching bundle from the GitHub Release, verifies SHA256, caches it, launches with CDP.
// macOS/Windows fall back to the Docker image until native binaries are published.
import { spawn, spawnSync } from "node:child_process";
import { createWriteStream, existsSync, chmodSync, mkdirSync, createReadStream } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import { createHash } from "node:crypto";
export const VERSION = "151.0.7908.0";
const REPO = "tiliondev/fortress";
const TAG = `v${VERSION}`;
const DOCKER_IMAGE = "tilion/fortress:latest";
const CACHE = process.env.FORTRESS_BROWSERS_PATH || join(homedir(), ".cache", "tilion-fortress");
const HOST = process.env.FORTRESS_DOWNLOAD_HOST || `https://github.com/${REPO}/releases/download/${TAG}`;
// platform key -> { asset, kind, launcher }
export const ASSETS = {
"linux-x64": { asset: "tilion-fortress-linux-x64.tar.gz", kind: "tar", launcher: "tilion-fortress/tilion" },
"win-x64": { asset: "tilion-fortress-win-x64.zip", kind: "zip", launcher: "tilion-fortress/tilion.cmd" },
"mac-arm64": { asset: "tilion-fortress-mac-arm64.tar.gz", kind: "tar", launcher: "tilion-fortress/tilion" },
"mac-x64": { asset: "tilion-fortress-mac-x64.tar.gz", kind: "tar", launcher: "tilion-fortress/tilion" },
};
export function resolvePlatform() {
const { platform, arch } = process;
if (platform === "linux" && arch === "x64") return "linux-x64";
if (platform === "win32" && arch === "x64") return "win-x64";
if (platform === "darwin") return arch === "arm64" ? "mac-arm64" : "mac-x64";
return null;
}
export function personaArgs(persona) {
if (!persona) return [];
const map = { platform: "--uxr-platform", timezone: "--uxr-timezone", languages: "--uxr-languages",
webglRenderer: "--uxr-webgl-renderer", webglVendor: "--uxr-webgl-vendor",
hwConcurrency: "--uxr-hw-concurrency", deviceMemory: "--uxr-device-memory",
screenWidth: "--uxr-screen-width", screenHeight: "--uxr-screen-height", canvasSeed: "--uxr-canvas-seed" };
return Object.entries(persona).map(([k, v]) => `${map[k] || `--uxr-${k}`}=${v}`);
}
export async function sha256(path) {
const h = createHash("sha256");
await pipeline(createReadStream(path), h);
return h.digest("hex");
}
export async function expectedSha(asset) {
try {
const r = await fetch(`${HOST}/SHA256SUMS`);
if (!r.ok) return null;
for (const line of (await r.text()).split("\n")) {
const p = line.trim().split(/\s+/);
if (p.length === 2 && p[1].replace(/^\*/, "") === asset) return p[0].toLowerCase();
}
} catch { /* none */ }
return null;
}
async function ensureNative(plat) {
const { asset, kind, launcher } = ASSETS[plat];
const root = join(CACHE, VERSION, plat);
const launcherPath = join(root, launcher);
if (existsSync(launcherPath)) return launcherPath;
mkdirSync(root, { recursive: true });
const archive = join(root, asset);
process.stderr.write(`[tilion-fortress] downloading ${HOST}/${asset} ...\n`);
const res = await fetch(`${HOST}/${asset}`);
if (!res.ok) throw new Error(`download failed: ${res.status}`);
await pipeline(res.body, createWriteStream(archive));
const exp = await expectedSha(asset);
if (exp) {
const act = await sha256(archive);
if (act !== exp) throw new Error(`SHA256 mismatch for ${asset}: expected ${exp}, got ${act}`);
process.stderr.write("[tilion-fortress] SHA256 verified\n");
} else {
process.stderr.write("[tilion-fortress] WARNING: no SHA256SUMS published; skipping verification\n");
}
if (kind === "tar") {
if (spawnSync("tar", ["xzf", archive, "-C", root], { stdio: "inherit" }).status !== 0)
throw new Error("tar extraction failed");
} else { // zip (Windows): use PowerShell Expand-Archive
if (spawnSync("powershell", ["-NoProfile", "-Command",
`Expand-Archive -Force -LiteralPath '${archive}' -DestinationPath '${root}'`],
{ stdio: "inherit" }).status !== 0) throw new Error("zip extraction failed");
}
if (!launcher.endsWith(".cmd") && existsSync(launcherPath)) chmodSync(launcherPath, 0o755);
if (!existsSync(launcherPath)) throw new Error(`launcher missing after extract: ${launcherPath}`);
return launcherPath;
}
async function assetExists(plat) {
try { return (await fetch(`${HOST}/${ASSETS[plat].asset}`, { method: "HEAD" })).ok; }
catch { return false; }
}
async function waitCdp(port, timeoutMs = 40000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try { const r = await fetch(`http://127.0.0.1:${port}/json/version`); if (r.ok) return (await r.json()).webSocketDebuggerUrl; }
catch {}
await new Promise((r) => setTimeout(r, 500));
}
throw new Error("Fortress CDP endpoint did not come up");
}
export class Fortress {
constructor({ port = 9222, persona = null, extraArgs = [], headless = true } = {}) {
Object.assign(this, { port, persona, extraArgs, headless, proc: null, dockerName: null, cdpUrl: null });
}
static async launch(opts) { return new Fortress(opts).start(); }
async start() {
const plat = resolvePlatform();
const native = plat && (plat === "linux-x64" || await assetExists(plat));
if (native) await this._startNative(plat); else this._startDocker();
this.cdpUrl = await waitCdp(this.port);
return this;
}
async _startNative(plat) {
const launcher = await ensureNative(plat);
const args = [];
if (this.headless) args.push("--headless=new", "--no-sandbox");
args.push(`--remote-debugging-port=${this.port}`, `--user-data-dir=${join(CACHE, "profile")}`,
...personaArgs(this.persona), ...this.extraArgs);
this.proc = spawn(launcher, args, { stdio: "ignore", shell: launcher.endsWith(".cmd") });
}
_startDocker() {
if (spawnSync("docker", ["--version"]).status !== 0)
throw new Error("No native binary for this platform yet and Docker not installed. Install Docker Desktop or use Linux x64.");
this.dockerName = `tilion-fortress-${process.pid}-${this.port}`;
const args = ["run", "-d", "--rm", "--name", this.dockerName, "-p", `${this.port}:9222`, DOCKER_IMAGE,
...personaArgs(this.persona), ...this.extraArgs];
if (spawnSync("docker", args, { stdio: "ignore" }).status !== 0) throw new Error("docker run failed");
}
async close() {
if (this.proc) { this.proc.kill(); this.proc = null; }
if (this.dockerName) { spawnSync("docker", ["rm", "-f", this.dockerName], { stdio: "ignore" }); this.dockerName = null; }
}
}
export default Fortress;