Files
harbor-net/src/server/http/routes/liveTrafficRoute.ts
T
dokril 3c2eefe108
Build and Deploy Gateway / build-and-push (push) Successful in 32s
Build and Deploy Gateway / deploy (push) Successful in 7s
Persist traffic settings and support multi-device traffic views
2026-08-31 06:46:59 +03:00

116 lines
3.9 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from 'node:http';
import { isDeepStrictEqual } from 'node:util';
import {
assertLiveTrafficSnapshot,
type LiveTrafficSnapshot,
} from '../../../shared/liveTraffic.js';
import type { StoredState } from '../../../shared/contracts/state.js';
import { HarborError } from '../../../shared/errors.js';
import {
normalizeTrafficSettings,
type TrafficSettings,
} from '../../../shared/trafficSettings.js';
import { sendJson } from '../response.js';
interface LiveTrafficReader {
snapshot(): unknown | Promise<unknown>;
}
interface DeviceInventoryReader {
snapshot(): unknown;
}
interface TrafficSettingsState {
read(): { revision?: unknown; traffic?: TrafficSettings };
update(mutator: (state: StoredState) => Record<string, unknown>): unknown;
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
export function enrichLiveTrafficDeviceLabels(
snapshot: LiveTrafficSnapshot,
inventory: unknown,
): LiveTrafficSnapshot {
const devices = Array.isArray(record(inventory).devices)
? (record(inventory).devices as unknown[]).map(record)
: [];
const labels = new Map(devices.flatMap((device) => {
const id = String(device.id || '');
if (!/^dev_[a-f0-9]{16}$/.test(id)) return [];
const label = [device.alias, device.hostname, device.ip]
.find((value) => typeof value === 'string' && value.trim());
return label ? [[id, String(label).trim()] as const] : [];
}));
if (!labels.size) return snapshot;
return {
...snapshot,
connections: snapshot.connections.map((connection) => {
const label = connection.origin.kind === 'device' && connection.origin.id
? labels.get(connection.origin.id)
: null;
return label ? {
...connection,
origin: { ...connection.origin, label },
} : connection;
}),
};
}
export function createLiveTrafficRoute({
traffic,
deviceInventory = null,
settingsState = null,
readBody = null,
sendState = null,
}: {
traffic: LiveTrafficReader | null;
deviceInventory?: DeviceInventoryReader | null;
settingsState?: TrafficSettingsState | null;
readBody?: ((req: IncomingMessage) => Promise<Record<string, unknown>>) | null;
sendState?: ((res: ServerResponse) => Promise<void>) | null;
}) {
return {
async handle(req: IncomingMessage, res: ServerResponse) {
const pathname = new URL(req.url || '/', 'http://localhost').pathname;
if (pathname === '/api/traffic/live') {
if (req.method !== 'GET' || !traffic) throw new HarborError('ENDPOINT_NOT_FOUND');
const snapshot = assertLiveTrafficSnapshot(await traffic.snapshot());
const enriched = deviceInventory
? enrichLiveTrafficDeviceLabels(snapshot, deviceInventory.snapshot())
: snapshot;
sendJson(res, 200, enriched);
return true;
}
if (pathname === '/api/traffic/settings') {
if (req.method !== 'PUT' || !settingsState || !readBody || !sendState) {
throw new HarborError('ENDPOINT_NOT_FOUND');
}
const body = await readBody(req);
const expectedRevision = body.expectedRevision;
if (!Number.isSafeInteger(expectedRevision) || Number(expectedRevision) < 0) {
throw new HarborError('REQUEST_INVALID');
}
let settings: TrafficSettings;
try {
settings = normalizeTrafficSettings(body.settings, { strict: true });
} catch (cause) {
throw new HarborError('REQUEST_INVALID', { cause });
}
const current = settingsState.read();
if (current.revision !== expectedRevision) throw new HarborError('STATE_CONFLICT');
if (!isDeepStrictEqual(current.traffic, settings)) {
settingsState.update((state) => ({ ...state, traffic: settings }));
}
await sendState(res);
return true;
}
return false;
},
};
}