121 lines
4.8 KiB
TypeScript
121 lines
4.8 KiB
TypeScript
/*
|
|
* Single-file binary entry point (compiled with `bun build --compile`).
|
|
*
|
|
* Not part of upstream. Copied to the server clone root by scripts/patch.sh.
|
|
* Two archives are embedded into the binary by scripts/build.sh and extracted to
|
|
* a temp dir on first run:
|
|
* - ui.tar.gz the built web UI, served over HTTP
|
|
* - overrides.tar.gz the interception overrides tree (terminal shims, the JVM
|
|
* agent, frida, the browser webextension) that upstream
|
|
* expects to find on disk at APP_ROOT/overrides
|
|
*
|
|
* Usage: httptoolkit [--port 8080] [--server-port 45457] [--mockttp-port 45456] [--no-open]
|
|
*/
|
|
|
|
// Bun global is present at runtime in the compiled binary:
|
|
declare const Bun: { file(path: string): { arrayBuffer(): Promise<ArrayBuffer> } };
|
|
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import * as fsp from 'fs/promises';
|
|
import { x as extractTar } from 'tar';
|
|
|
|
// Embedded archives. These resolve to paths Bun can read from the compiled binary:
|
|
import uiArchive from './ui.tar.gz' with { type: 'file' };
|
|
import overridesArchive from './overrides.tar.gz' with { type: 'file' };
|
|
|
|
// Deliberately NOT imported from './src/constants': that module computes APP_ROOT
|
|
// at load time, and we have to set HTK_APP_ROOT before it is ever evaluated (see
|
|
// main()). package.json carries the version without pulling in any of that.
|
|
import pkg from './package.json';
|
|
|
|
const SERVER_VERSION: string = pkg.version;
|
|
|
|
function printHelp() {
|
|
console.log(`HTTP Toolkit (self-hosted single binary) v${SERVER_VERSION}
|
|
|
|
Usage: httptoolkit [options]
|
|
|
|
Options:
|
|
-p, --port <port> Port to serve the web UI on (default: 8080)
|
|
--server-port <port> HTK management API port (default: 45457)
|
|
--mockttp-port <port> Mockttp admin port (default: 45456)
|
|
--no-open Don't open a browser automatically
|
|
-h, --help Show this help
|
|
`);
|
|
}
|
|
|
|
function parseArgs(argv: string[]) {
|
|
const opts = { port: 8080, serverPort: 45457, mockttpPort: 45456, open: true };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
const eq = a.indexOf('=');
|
|
const inlineVal = eq !== -1 ? a.slice(eq + 1) : undefined;
|
|
const key = eq !== -1 ? a.slice(0, eq) : a;
|
|
const val = () => inlineVal ?? argv[++i];
|
|
|
|
switch (key) {
|
|
case '-p': case '--port': opts.port = parseInt(val(), 10); break;
|
|
case '--server-port': opts.serverPort = parseInt(val(), 10); break;
|
|
case '--mockttp-port': opts.mockttpPort = parseInt(val(), 10); break;
|
|
case '--no-open': opts.open = false; break;
|
|
case '-h': case '--help': printHelp(); process.exit(0);
|
|
default:
|
|
console.error(`Unknown option: ${a}`);
|
|
printHelp();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
if (!Number.isInteger(opts.port) || opts.port <= 0 || opts.port > 65535) {
|
|
console.error(`Invalid --port value`); process.exit(1);
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
// Extract one embedded tarball into `dir`, unless it's already there for this
|
|
// version. Modes are preserved, which matters: overrides/path holds executable
|
|
// shims that only work if they stay +x.
|
|
async function extractArchive(archivePath: string, dir: string): Promise<string> {
|
|
const marker = path.join(dir, '.extracted');
|
|
if (fs.existsSync(marker)) return dir; // already extracted for this version
|
|
|
|
await fsp.rm(dir, { recursive: true, force: true });
|
|
await fsp.mkdir(dir, { recursive: true });
|
|
|
|
const bytes = new Uint8Array(await Bun.file(archivePath).arrayBuffer());
|
|
const tmpArchive = path.join(dir, 'archive.tar.gz');
|
|
await fsp.writeFile(tmpArchive, bytes);
|
|
|
|
await extractTar({ file: tmpArchive, cwd: dir });
|
|
await fsp.unlink(tmpArchive).catch(() => {});
|
|
await fsp.writeFile(marker, '');
|
|
return dir;
|
|
}
|
|
|
|
async function main() {
|
|
const opts = parseArgs(process.argv.slice(2));
|
|
|
|
// One app root per version, laid out the way upstream expects to find it:
|
|
// <tmp>/httptoolkit-<version>/ui
|
|
// <tmp>/httptoolkit-<version>/overrides
|
|
const appRoot = path.join(os.tmpdir(), `httptoolkit-${SERVER_VERSION}`);
|
|
const uiDir = path.join(appRoot, 'ui');
|
|
|
|
await extractArchive(uiArchive, uiDir);
|
|
await extractArchive(overridesArchive, path.join(appRoot, 'overrides'));
|
|
|
|
// Must be set before ./src/constants is evaluated, since APP_ROOT (and hence
|
|
// OVERRIDES_DIR) is computed once at module load. That's why the app is pulled
|
|
// in with a dynamic import here rather than a static one at the top of the file.
|
|
process.env.HTK_APP_ROOT = appRoot;
|
|
|
|
const { startHtkApp } = await import('./src/htk-app');
|
|
await startHtkApp({ ...opts, uiDir });
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error('Failed to start HTTP Toolkit:', e);
|
|
process.exit(1);
|
|
});
|