Files
3proxyUI/server/lib/config.ts

241 lines
6.5 KiB
TypeScript

import path from 'node:path';
import { dashboardSnapshot } from '../../src/data/mockDashboard';
import type {
ControlPlaneState,
CreateUserInput,
DashboardSnapshot,
DailyTrafficBucket,
ProxyServiceRecord,
ProxyUserRecord,
} from '../../src/shared/contracts';
import { quotaMbToBytes } from '../../src/shared/validation';
import type { ServiceState } from '../../src/lib/3proxy';
const MB = 1024 * 1024;
export interface RuntimeSnapshot {
status: Exclude<ServiceState, 'warn' | 'paused'>;
pid: number | null;
startedAt: string | null;
lastError: string | null;
}
export interface ObservedRuntimeState {
totalBytes: number;
liveConnections: number;
activeUsers: number;
daily: DailyTrafficBucket[];
userRecords: ProxyUserRecord[];
}
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 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: quotaMbToBytes(input.quotaMb),
};
}
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 = state.userRecords.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,
observed: ObservedRuntimeState,
previewConfig: string,
): DashboardSnapshot {
const liveUsers = observed.userRecords.filter((user) => !user.paused && user.status === 'live').length;
const exceededUsers = observed.userRecords.filter(
(user) => user.quotaBytes !== null && user.usedBytes >= user.quotaBytes,
).length;
const nearQuotaUsers = observed.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: {
totalBytes: observed.totalBytes,
liveConnections: observed.liveConnections,
activeUsers: observed.activeUsers,
daily: observed.daily,
},
users: {
total: observed.userRecords.length,
live: liveUsers,
nearQuota: nearQuotaUsers,
exceeded: exceededUsers,
},
attention,
userRecords: observed.userRecords,
system: {
...state.system,
previewConfig,
},
};
}
function renderUserCredential(user: ProxyUserRecord): string {
return `${user.username}:CL:${user.password}`;
}
function renderServiceCommand(service: ProxyServiceRecord): string {
if (service.command === 'socks') {
return `socks -p${service.port} -u2`;
}
return `proxy -p${service.port}`;
}
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`;
}