pithos/bin/pithos-web.js
Local Dev 6b8eeaf82b Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.

One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00

78 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
// Web console: serves the Pithos UI in a browser.
//
// pithos-web loopback only, prints a sign-in link
// pithos-web --listen 0.0.0.0:8700 remote access; requires PITHOS_PASSWORD
// (or --password-file) and a TLS proxy in front
//
// Flags: --listen host:port, --config <s3d.yml>, --s3d <binary>, --bin-dir <dir>,
// --password-file <file>, --allow-host <name[:port]> (repeatable)
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createPithos } from '../core/server.js';
const HERE = path.dirname(fileURLToPath(import.meta.url));
function parseArgs(argv) {
const out = { allowHosts: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const next = () => { if (i + 1 >= argv.length) fail(`${a} needs a value`); return argv[++i]; };
switch (a) {
case '--listen': out.listen = next(); break;
case '--config': out.config = next(); break;
case '--s3d': out.s3d = next(); break;
case '--bin-dir': out.binDir = next(); break;
case '--password-file': out.passwordFile = next(); break;
case '--allow-host': out.allowHosts.push(next().toLowerCase()); break;
case '-h': case '--help':
console.log(fs.readFileSync(fileURLToPath(import.meta.url), 'utf8').split('\n').slice(1, 11).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'));
process.exit(0);
default: fail(`unknown flag ${a}`);
}
}
return out;
}
function fail(msg) { console.error(`pithos-web: ${msg}`); process.exit(2); }
const args = parseArgs(process.argv.slice(2));
const [host, portStr] = splitHostPort(args.listen || '127.0.0.1:8700');
const loopback = ['127.0.0.1', 'localhost', '::1'].includes(host);
let password = process.env.PITHOS_PASSWORD || '';
if (args.passwordFile) password = fs.readFileSync(args.passwordFile, 'utf8').trim();
if (!loopback && password.length < 12) fail('listening beyond loopback needs PITHOS_PASSWORD or --password-file (12+ characters)');
const pithos = await createPithos({
host,
port: Number(portStr),
configFile: args.config,
binary: args.s3d,
binDir: args.binDir || HERE,
password: password || undefined,
allowedHosts: args.allowHosts,
hostName: 'web',
});
console.log(`Pithos web console on ${pithos.url}`);
if (loopback) console.log(`Sign in: ${pithos.authUrl}`);
else console.log('Sign in with the configured password. Put a TLS reverse proxy in front of this port.');
console.log(`s3d config: ${pithos.daemon.configFile}`);
console.log(`s3d binary: ${pithos.daemon.binary || '(not found - install it from the console)'}`);
const shutdown = async () => {
console.log('stopping...');
await pithos.close();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
function splitHostPort(s) {
const m = s.match(/^\[?([^\]]+?)\]?:(\d+)$/);
if (!m) fail(`--listen wants host:port, got ${s}`);
return [m[1], m[2]];
}