Source-candidate: poe d9a564e6d20f5f0fd75ce45b12cbbf8bb4c9d568\nSource-candidate: magica38ba5762b06716db1dee00d0afec162656488f5c\nValidated321whole-file phase plus whole bus1349/model-proxy100/webui490; final gate tooling checks pending, not landed.
100 lines
5.8 KiB
JavaScript
100 lines
5.8 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import net from 'node:net';
|
|
import { randomBytes } from 'node:crypto';
|
|
import { PIN, refuse, safePath, regularBytes, sha256, validManagementKey, proxyTiming } from './common.mjs';
|
|
export function installPaths(dataRoot, pin = PIN) {
|
|
const dir = path.join(dataRoot, 'opt', 'cliproxyapi', pin.version);
|
|
return { dir, binary: path.join(dir, 'cli-proxy-api'), panel: path.join(dir, 'static', 'management.html') };
|
|
}
|
|
export function validateProxy(value, dataRoot, pin = PIN) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
|| Object.keys(value).some(k => !['binary', 'port', 'rotateGraceMinutes', 'firstOutputSeconds'].includes(k))
|
|
|| !Number.isInteger(value.port) || value.port < 1024 || value.port > 65535 || value.port === 8317
|
|
|| value.binary !== installPaths(dataRoot, pin).binary) refuse('proxy-config-invalid');
|
|
return { port: value.port, binary: value.binary, ...proxyTiming(value) };
|
|
}
|
|
export function loadProxy(configDir, dataRoot) {
|
|
const file = path.join(configDir, 'proxy.json');
|
|
try { fs.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return undefined; refuse('proxy-config-invalid'); }
|
|
try { return validateProxy(JSON.parse(regularBytes(file, 'proxy-config-invalid')), dataRoot); }
|
|
catch (error) {
|
|
if (['proxy-rotate-grace-invalid', 'proxy-first-output-invalid'].includes(error.code)) throw error;
|
|
refuse('proxy-config-invalid');
|
|
}
|
|
}
|
|
export function verifyInstall(dataRoot, pin = PIN) {
|
|
const paths = installPaths(dataRoot, pin);
|
|
for (const [file, hash] of [[paths.binary, pin.binary.sha256], [paths.panel, pin.panel.sha256]]) {
|
|
try { fs.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') refuse('proxy-install-missing'); refuse('proxy-install-invalid'); }
|
|
if (sha256(regularBytes(file, 'proxy-install-invalid')) !== hash) refuse('proxy-install-invalid');
|
|
}
|
|
return paths;
|
|
}
|
|
async function reservePort(wanted) {
|
|
if (wanted !== undefined && (!Number.isInteger(wanted) || wanted < 1024 || wanted > 65535 || wanted === 8317)) refuse('proxy-config-invalid');
|
|
const server = net.createServer();
|
|
try {
|
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(wanted ?? 0, '127.0.0.1', resolve); });
|
|
const port = server.address().port;
|
|
if (port === 8317 || port < 1024) { await new Promise(r => server.close(r)); return reservePort(wanted); }
|
|
return { port, release: () => new Promise(r => server.close(r)) };
|
|
} catch { refuse('proxy-port-in-use'); }
|
|
}
|
|
export function initialConfig({ dataRoot, port, key, sentinel }) {
|
|
return {
|
|
'config-version': 8,
|
|
server: { host: '127.0.0.1', port },
|
|
management: { 'allow-remote': false, 'secret-key': key, 'disable-control-panel': false, 'disable-auto-update-panel': true },
|
|
access: { 'api-keys': [sentinel] },
|
|
oauth: { 'auth-dir': path.join(dataRoot, 'proxy', 'auth') },
|
|
observability: { logs: { 'logging-to-file': true } },
|
|
routing: { strategy: 'fill-first', 'session-affinity': true, 'session-affinity-rebind-on': 'quota', retry: { 'request-retry': 0 } },
|
|
};
|
|
}
|
|
// Called only by the bus's path-bound human verb, never by an agent or the tester.
|
|
export async function initializeProxy({ configDir, dataRoot, key, port, pin = PIN, ...options }) {
|
|
if (!validManagementKey(key)) refuse('proxy-key-invalid');
|
|
const timing = proxyTiming(options);
|
|
safePath(configDir, 'proxy-init-failed'); safePath(dataRoot, 'proxy-init-failed');
|
|
const paths = verifyInstall(dataRoot, pin);
|
|
const settingsFile = path.join(configDir, 'proxy.json'), proxyDir = path.join(dataRoot, 'proxy');
|
|
const secrets = path.join(configDir, 'secrets'), keyFile = path.join(secrets, 'proxy-management.key');
|
|
for (const file of [settingsFile, proxyDir, keyFile]) {
|
|
try { fs.lstatSync(file); refuse('proxy-already-initialized'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
}
|
|
const lockFile = path.join(configDir, '.proxy-init.lock');
|
|
let lock;
|
|
try { lock = fs.openSync(lockFile, 'wx', 0o600); } catch { refuse('proxy-init-busy'); }
|
|
let reservation, ownsProxy = false, ownsKey = false, ownsSettings = false, ownsSecrets = false;
|
|
try {
|
|
reservation = await reservePort(port);
|
|
safePath(secrets, 'proxy-init-failed');
|
|
try { fs.mkdirSync(secrets, { mode: 0o700 }); ownsSecrets = true; }
|
|
catch (e) { if (e.code !== 'EEXIST') throw e; }
|
|
const stat = fs.lstatSync(secrets);
|
|
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o700) refuse('proxy-init-failed');
|
|
fs.mkdirSync(proxyDir, { mode: 0o700 }); ownsProxy = true;
|
|
for (const name of ['auth', 'logs']) fs.mkdirSync(path.join(proxyDir, name), { mode: 0o700 });
|
|
const keyFd = fs.openSync(keyFile, 'wx', 0o600); ownsKey = true;
|
|
try { fs.writeFileSync(keyFd, key); } finally { fs.closeSync(keyFd); }
|
|
const tag = sha256(configDir).slice(0, 12);
|
|
const sentinel = `msk-${tag}-sentinel-${randomBytes(32).toString('base64url')}`;
|
|
fs.writeFileSync(path.join(proxyDir, 'config.yaml'), JSON.stringify(initialConfig({ dataRoot, port: reservation.port, key, sentinel }), null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
const settings = { port: reservation.port, binary: paths.binary, ...timing };
|
|
const settingsFd = fs.openSync(settingsFile, 'wx', 0o600); ownsSettings = true;
|
|
try { fs.writeFileSync(settingsFd, JSON.stringify(settings, null, 2) + '\n'); } finally { fs.closeSync(settingsFd); }
|
|
return settings;
|
|
} catch (e) {
|
|
if (ownsSettings) fs.unlinkSync(settingsFile);
|
|
if (ownsKey) fs.unlinkSync(keyFile);
|
|
if (ownsProxy) fs.rmSync(proxyDir, { recursive: true });
|
|
if (ownsSecrets) fs.rmdirSync(secrets);
|
|
if (e.code?.startsWith('proxy-')) throw e;
|
|
refuse('proxy-init-failed');
|
|
} finally {
|
|
if (reservation) await reservation.release();
|
|
fs.closeSync(lock); fs.unlinkSync(lockFile);
|
|
}
|
|
}
|