pathes
Build self-hosted HTTP Toolkit / build (linux-x64, ubuntu-latest, true) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / build (macos-arm64, macos-latest, true) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / build (windows-x64, windows-latest, false) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / release (push) Canceled after 0s
Build self-hosted HTTP Toolkit / build (linux-x64, ubuntu-latest, true) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / build (macos-arm64, macos-latest, true) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / build (windows-x64, windows-latest, false) (push) Canceled after 0s
Build self-hosted HTTP Toolkit / release (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Single-file binary entry point (compiled with `bun build --compile`).
|
||||
*
|
||||
* Not part of upstream. Copied to the server submodule root by scripts/apply.sh.
|
||||
* The built UI is embedded into the binary as `ui.tar.gz` (created by
|
||||
* scripts/build.sh) and extracted to a temp dir on first run.
|
||||
*
|
||||
* 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';
|
||||
|
||||
import { startHtkApp } from './src/htk-app';
|
||||
import { SERVER_VERSION } from './src/constants';
|
||||
|
||||
// Embedded UI bundle. Resolves to a path Bun can read from the compiled binary:
|
||||
import uiArchive from './ui.tar.gz' with { type: 'file' };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function extractUi(): Promise<string> {
|
||||
const dir = path.join(os.tmpdir(), `httptoolkit-ui-${SERVER_VERSION}`);
|
||||
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(uiArchive).arrayBuffer());
|
||||
const tmpArchive = path.join(dir, 'ui.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));
|
||||
const uiDir = await extractUi();
|
||||
await startHtkApp({ ...opts, uiDir });
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('Failed to start HTTP Toolkit:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Fallback oclif command: `httptoolkit-server app`.
|
||||
*
|
||||
* Not part of upstream. Copied into the server submodule by scripts/apply.sh.
|
||||
* This lets the same feature run via the classic oclif-packed build (or `npm`),
|
||||
* as an alternative to the Bun single-file binary. Here the built UI is expected
|
||||
* to live in a sibling `ui/` directory next to the package root (APP_ROOT).
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import { APP_ROOT } from '../constants';
|
||||
import { startHtkApp } from '../htk-app';
|
||||
|
||||
export default class App extends Command {
|
||||
static description = 'run the HTK server and serve the bundled web UI locally';
|
||||
|
||||
static flags = {
|
||||
help: flags.help({ char: 'h' }),
|
||||
port: flags.integer({ char: 'p', description: 'port to serve the web UI on', default: 8080 }),
|
||||
'server-port': flags.integer({ description: 'HTK management API port', default: 45457 }),
|
||||
'mockttp-port': flags.integer({ description: 'Mockttp admin port', default: 45456 }),
|
||||
'no-open': flags.boolean({ description: "don't open a browser automatically", default: false })
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { flags: f } = this.parse(App);
|
||||
await startHtkApp({
|
||||
uiDir: path.join(APP_ROOT, 'ui'),
|
||||
port: f.port,
|
||||
serverPort: f['server-port'],
|
||||
mockttpPort: f['mockttp-port'],
|
||||
open: !f['no-open']
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Self-hosted HTTP Toolkit: shared core logic.
|
||||
*
|
||||
* This is NOT part of upstream httptoolkit-server. It's copied into the server
|
||||
* submodule by scripts/apply.sh. It starts the normal HTK backend (proxy admin
|
||||
* + management API) via runHTK(), then serves the pre-built web UI over HTTP on
|
||||
* a local port and (optionally) opens a browser.
|
||||
*
|
||||
* The UI connects back to 127.0.0.1:<serverPort>/<mockttpPort> at runtime (its
|
||||
* built-in defaults), so as long as those match, everything wires up with no
|
||||
* auth token required (we never set HTK_SERVER_TOKEN).
|
||||
*/
|
||||
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as fsp from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as net from 'net';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
import { runHTK } from './index';
|
||||
|
||||
const MIME: { [ext: string]: string } = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.wasm': 'application/wasm',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.map': 'application/json; charset=utf-8',
|
||||
'.txt': 'text/plain; charset=utf-8'
|
||||
};
|
||||
|
||||
const contentType = (file: string) =>
|
||||
MIME[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
||||
|
||||
function openBrowser(url: string) {
|
||||
const platform = process.platform;
|
||||
const [cmd, args] = platform === 'darwin'
|
||||
? ['open', [url]]
|
||||
: platform === 'win32'
|
||||
? ['cmd', ['/c', 'start', '', url]]
|
||||
: ['xdg-open', [url]];
|
||||
try {
|
||||
const child = spawn(cmd, args as string[], { stdio: 'ignore', detached: true });
|
||||
// Never let a missing opener crash the app - the URL is printed anyway.
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
} catch { /* ignore - user opens the printed URL manually */ }
|
||||
}
|
||||
|
||||
async function serveStaticUi(uiDir: string, port: number): Promise<http.Server> {
|
||||
const indexHtml = path.join(uiDir, 'index.html');
|
||||
const root = path.resolve(uiDir);
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
||||
let filePath = path.resolve(root, '.' + path.posix.normalize(urlPath));
|
||||
|
||||
// Prevent path traversal outside the UI directory:
|
||||
if (filePath !== root && !filePath.startsWith(root + path.sep)) {
|
||||
res.writeHead(403); res.end('Forbidden'); return;
|
||||
}
|
||||
|
||||
let stat = await fsp.stat(filePath).catch(() => null);
|
||||
if (stat && stat.isDirectory()) {
|
||||
filePath = path.join(filePath, 'index.html');
|
||||
stat = await fsp.stat(filePath).catch(() => null);
|
||||
}
|
||||
|
||||
// SPA fallback: unknown non-file routes serve index.html so the UI's
|
||||
// client-side router (reach/router) can handle them.
|
||||
if (!stat) {
|
||||
filePath = indexHtml;
|
||||
stat = await fsp.stat(filePath).catch(() => null);
|
||||
if (!stat) { res.writeHead(404); res.end('UI bundle not found'); return; }
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': contentType(filePath),
|
||||
'Content-Length': stat.size
|
||||
});
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
} catch {
|
||||
if (!res.headersSent) res.writeHead(500);
|
||||
res.end('Internal error');
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, '127.0.0.1', resolve);
|
||||
});
|
||||
return server;
|
||||
}
|
||||
|
||||
export interface HtkAppOptions {
|
||||
uiDir: string; // directory containing the built UI (index.html + assets)
|
||||
port: number; // port to serve the UI on
|
||||
serverPort: number; // HTK management API (UI default: 45457)
|
||||
mockttpPort: number; // Mockttp admin (UI default: 45456)
|
||||
open: boolean; // whether to auto-open a browser
|
||||
}
|
||||
|
||||
export async function startHtkApp(opts: HtkAppOptions) {
|
||||
// Match upstream start.ts: disable autoSelectFamily (unstable on Node 20+).
|
||||
if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
|
||||
|
||||
// Start the real HTK backend. No auth token => the locally-served UI can
|
||||
// talk to it without an ?authToken param.
|
||||
await runHTK({
|
||||
serverPort: opts.serverPort,
|
||||
mockttpPort: opts.mockttpPort
|
||||
});
|
||||
|
||||
await serveStaticUi(opts.uiDir, opts.port);
|
||||
|
||||
const url = `http://localhost:${opts.port}`;
|
||||
console.log(`\n HTTP Toolkit is running at ${url}`);
|
||||
console.log(` (management API: 127.0.0.1:${opts.serverPort}, mockttp: 127.0.0.1:${opts.mockttpPort})\n`);
|
||||
|
||||
if (opts.open) openBrowser(url);
|
||||
}
|
||||
Reference in New Issue
Block a user