import path from 'node:path'; import { dashboardSnapshot } from '../../src/data/mockDashboard'; import type { ControlPlaneState, CreateUserInput, DashboardSnapshot, ProxyServiceRecord, ProxyUserRecord, } from '../../src/shared/contracts'; import type { ServiceState } from '../../src/lib/3proxy'; const MB = 1024 * 1024; export interface RuntimeSnapshot { status: Exclude; pid: number | null; startedAt: string | null; lastError: string | null; } export interface RuntimePaths { rootDir: string; configPath: string; counterPath: string; reportDir: string; logPath: string; pidPath: string; } export function createDefaultState(): ControlPlaneState { return structuredClone(dashboardSnapshot); } export function buildRuntimePaths(rootDir: string): RuntimePaths { return { rootDir, configPath: path.join(rootDir, 'generated', '3proxy.cfg'), counterPath: path.join(rootDir, 'state', 'counters.3cf'), reportDir: path.join(rootDir, 'state', 'reports'), logPath: path.join(rootDir, 'logs', '3proxy.log'), pidPath: path.join(rootDir, '3proxy.pid'), }; } export function validateCreateUserInput( input: Partial, services: ProxyServiceRecord[], ): CreateUserInput { const username = input.username?.trim() ?? ''; const password = input.password?.trim() ?? ''; const serviceId = input.serviceId?.trim() ?? ''; const quotaMb = input.quotaMb ?? null; assertSafeToken(username, 'Username'); assertSafeToken(password, 'Password'); if (!serviceId) { throw new Error('Service is required.'); } const service = services.find((entry) => entry.id === serviceId); if (!service || !service.enabled || !service.assignable) { throw new Error('Service must reference an enabled assignable entry.'); } if (quotaMb !== null && (!Number.isFinite(quotaMb) || quotaMb <= 0 || !Number.isInteger(quotaMb))) { throw new Error('Quota must be a positive integer number of megabytes.'); } return { username, password, serviceId, quotaMb, }; } export function createUserRecord(state: ControlPlaneState, input: CreateUserInput): ProxyUserRecord { if (state.userRecords.some((user) => user.username === input.username)) { throw new Error('Username already exists.'); } return { id: `u-${Math.random().toString(36).slice(2, 10)}`, username: input.username, password: input.password, serviceId: input.serviceId, status: 'idle', paused: false, usedBytes: 0, quotaBytes: input.quotaMb === null ? null : input.quotaMb * MB, }; } export function render3proxyConfig(state: ControlPlaneState, paths: RuntimePaths): string { const lines = [ `pidfile ${normalizePath(paths.pidPath)}`, `monitor ${normalizePath(paths.configPath)}`, `log ${normalizePath(paths.logPath)} D`, 'auth strong', ]; const activeUsers = state.userRecords.filter((user) => !user.paused); if (activeUsers.length > 0) { lines.push(`users ${activeUsers.map(renderUserCredential).join(' ')}`); } const quotaUsers = activeUsers.filter((user) => user.quotaBytes !== null); if (quotaUsers.length > 0) { lines.push( `counter ${normalizePath(paths.counterPath)} D ${normalizePath( path.join(paths.reportDir, '%Y-%m-%d.txt'), )}`, ); quotaUsers.forEach((user, index) => { lines.push( `countall ${index + 1} D ${Math.ceil((user.quotaBytes ?? 0) / MB)} ${user.username} * * * * * *`, ); }); } state.system.services .filter((service) => service.enabled) .forEach((service) => { lines.push('', 'flush'); if (service.assignable) { const usernames = activeUsers .filter((user) => user.serviceId === service.id) .map((user) => user.username); lines.push(usernames.length > 0 ? `allow ${usernames.join(',')}` : 'deny *'); } else { lines.push('allow *'); } lines.push(renderServiceCommand(service)); }); return `${lines.join('\n')}\n`; } export function deriveDashboardSnapshot( state: ControlPlaneState, runtime: RuntimeSnapshot, previewConfig: string, ): DashboardSnapshot { const liveUsers = state.userRecords.filter((user) => !user.paused && user.status === 'live').length; const exceededUsers = state.userRecords.filter( (user) => user.quotaBytes !== null && user.usedBytes >= user.quotaBytes, ).length; const nearQuotaUsers = state.userRecords.filter((user) => { if (user.paused || user.quotaBytes === null || user.usedBytes >= user.quotaBytes) { return false; } return user.usedBytes / user.quotaBytes >= 0.8; }).length; const attention = []; if (exceededUsers > 0) { attention.push({ level: 'fail' as const, title: 'Quota exceeded', message: `${exceededUsers} user profiles crossed their configured quota.`, }); } if (nearQuotaUsers > 0) { attention.push({ level: 'warn' as const, title: 'Quota pressure detected', message: `${nearQuotaUsers} user profiles are above 80% of their quota.`, }); } if (runtime.status === 'live') { attention.push({ level: 'live' as const, title: '3proxy runtime online', message: 'Backend control plane is currently attached to a live 3proxy process.', }); } else if (runtime.lastError) { attention.push({ level: 'fail' as const, title: 'Runtime issue detected', message: runtime.lastError, }); } if (attention.length === 0) { attention.push({ level: 'live' as const, title: 'State loaded', message: 'No critical runtime or quota issues are currently detected.', }); } return { service: { status: runtime.status, pidLabel: runtime.pid ? `pid ${runtime.pid}` : 'pid -', versionLabel: state.service.versionLabel, uptimeLabel: formatUptime(runtime.startedAt), lastEvent: state.service.lastEvent, }, traffic: { ...state.traffic, activeUsers: state.userRecords.filter((user) => !user.paused).length, }, users: { total: state.userRecords.length, live: liveUsers, nearQuota: nearQuotaUsers, exceeded: exceededUsers, }, attention, userRecords: state.userRecords, system: { ...state.system, previewConfig, }, }; } function renderUserCredential(user: ProxyUserRecord): string { return `${user.username}:CL:${user.password}`; } function renderServiceCommand(service: ProxyServiceRecord): string { if (service.command === 'admin') { return `admin -p${service.port} -s`; } if (service.command === 'socks') { return `socks -p${service.port} -u2`; } return `proxy -p${service.port}`; } function assertSafeToken(value: string, label: string): void { if (!value) { throw new Error(`${label} is required.`); } if (!/^[A-Za-z0-9._~!@#$%^&*+=:/-]+$/.test(value)) { throw new Error(`${label} contains unsupported characters.`); } } function normalizePath(value: string): string { return value.replace(/\\/g, '/'); } function formatUptime(startedAt: string | null): string { if (!startedAt) { return 'uptime -'; } const started = new Date(startedAt).getTime(); const diffMs = Date.now() - started; if (!Number.isFinite(diffMs) || diffMs <= 0) { return 'uptime 0m'; } const hours = Math.floor(diffMs / (1000 * 60 * 60)); const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); return `uptime ${hours}h ${minutes}m`; }