import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; interface MaterializeOptions { apiPort: number; secretPath: string; runtimeConfigPath: string; } function record(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } function message(error: unknown) { return error instanceof Error ? error.message : String(error); } function privateWrite(filePath: string, value: unknown) { fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }); const temporaryPath = `${filePath}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`; let descriptor: number | null = null; try { descriptor = fs.openSync( temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600, ); fs.writeFileSync(descriptor, JSON.stringify(value)); fs.fchmodSync(descriptor, 0o600); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = null; fs.renameSync(temporaryPath, filePath); fs.chmodSync(filePath, 0o600); const status = fs.lstatSync(filePath); if (!status.isFile() || status.isSymbolicLink() || (status.mode & 0o777) !== 0o600) { throw new Error('private runtime config is not a regular 0600 file'); } } finally { if (descriptor !== null) fs.closeSync(descriptor); fs.rmSync(temporaryPath, { force: true }); } } function openSecret(secretPath: string) { const readFlags = fs.constants.O_RDWR | fs.constants.O_NOFOLLOW; try { return { descriptor: fs.openSync(secretPath, readFlags), created: false }; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } try { return { descriptor: fs.openSync( secretPath, readFlags | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600, ), created: true, }; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; return { descriptor: fs.openSync(secretPath, readFlags), created: false }; } } export function ensureGatewayNativeApiSecret(secretPath: string) { fs.mkdirSync(path.dirname(secretPath), { recursive: true, mode: 0o700 }); const { descriptor, created } = openSecret(secretPath); try { const secret = created ? crypto.randomBytes(32).toString('hex') : fs.readFileSync(descriptor, 'utf8'); if (created) { fs.writeFileSync(descriptor, secret); fs.fsyncSync(descriptor); } if (!/^[0-9a-f]{64}$/.test(secret)) { throw new Error('native API secret must contain exactly 64 lowercase hex characters'); } fs.fchmodSync(descriptor, 0o600); const opened = fs.fstatSync(descriptor); const linked = fs.lstatSync(secretPath); if (!opened.isFile() || linked.isSymbolicLink() || !linked.isFile() || opened.dev !== linked.dev || opened.ino !== linked.ino || (opened.mode & 0o777) !== 0o600 || (linked.mode & 0o777) !== 0o600) { throw new Error('native API secret is not a regular 0600 file'); } return secret; } finally { fs.closeSync(descriptor); } } function withoutApiServices(config: unknown) { const safe = structuredClone(record(config)); const services = Array.isArray(safe.services) ? safe.services.filter((service) => record(service).type !== 'api') : []; if (services.length) safe.services = services; else delete safe.services; return safe; } export function materializeGatewaySnapshotConfig(config: unknown, runtimeConfigPath: string) { privateWrite(runtimeConfigPath, withoutApiServices(config)); return { configPath: runtimeConfigPath, secret: null, warning: null }; } function withAuthenticatedApi(config: unknown, secret: string) { const materialized = structuredClone(record(config)); const services = Array.isArray(materialized.services) ? materialized.services : []; materialized.services = services.map((service) => ( record(service).type === 'api' ? { ...record(service), secret } : service )); return materialized; } function validateApiService(config: unknown, apiPort: number) { const configuredServices = record(config).services; const services = Array.isArray(configuredServices) ? configuredServices : []; const apiServices = services.map(record).filter(({ type }) => type === 'api'); if (apiServices.length !== 1) throw new Error('expected exactly one native API service'); const [service] = apiServices; if (service.listen !== '127.0.0.1' || service.listen_port !== apiPort || service.dashboard !== false || Object.hasOwn(service, 'secret')) { throw new Error(`native API service must be unauthenticated base config on 127.0.0.1:${apiPort}`); } } export function materializeGatewayNativeConfig( config: unknown, { apiPort, secretPath, runtimeConfigPath }: MaterializeOptions, ) { let warning: string | null = null; try { validateApiService(config, apiPort); const secret = ensureGatewayNativeApiSecret(secretPath); privateWrite(runtimeConfigPath, withAuthenticatedApi(config, secret)); return { configPath: runtimeConfigPath, secret, warning }; } catch (error) { warning = `Native traffic API disabled: ${message(error)}`; } const safeConfig = withoutApiServices(config); try { privateWrite(runtimeConfigPath, safeConfig); return { configPath: runtimeConfigPath, secret: null, warning }; } catch (error) { warning = `${warning}; private runtime config unavailable: ${message(error)}`; const suffix = crypto.createHash('sha256').update(runtimeConfigPath).digest('hex').slice(0, 12); const fallbackPath = path.join(os.tmpdir(), `harbor-singbox-runtime-${process.pid}-${suffix}.json`); privateWrite(fallbackPath, safeConfig); return { configPath: fallbackPath, secret: null, warning }; } }