import * as plugins from '../plugins.js'; import type { StorageManager } from '../storage/classes.storagemanager.js'; import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js'; const STORAGE_PREFIX = '/remote-ingress/'; /** * Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array. */ function extractPorts(portRange: number | number[] | Array<{ from: number; to: number }>): number[] { const ports = new Set(); if (typeof portRange === 'number') { ports.add(portRange); } else if (Array.isArray(portRange)) { for (const entry of portRange) { if (typeof entry === 'number') { ports.add(entry); } else if (typeof entry === 'object' && 'from' in entry && 'to' in entry) { for (let p = entry.from; p <= entry.to; p++) { ports.add(p); } } } } return [...ports].sort((a, b) => a - b); } /** * Manages CRUD for remote ingress edge registrations. * Persists edge configs via StorageManager and provides * the allowed edges list for the Rust hub. */ export class RemoteIngressManager { private storageManager: StorageManager; private edges: Map = new Map(); private routes: IDcRouterRouteConfig[] = []; constructor(storageManager: StorageManager) { this.storageManager = storageManager; } /** * Load all edge registrations from storage into memory. */ public async initialize(): Promise { const keys = await this.storageManager.list(STORAGE_PREFIX); for (const key of keys) { const edge = await this.storageManager.getJSON(key); if (edge) { // Migration: old edges without autoDerivePorts default to true if ((edge as any).autoDerivePorts === undefined) { edge.autoDerivePorts = true; await this.storageManager.setJSON(key, edge); } this.edges.set(edge.id, edge); } } } /** * Store the current route configs for port derivation. */ public setRoutes(routes: IDcRouterRouteConfig[]): void { this.routes = routes; } /** * Derive listen ports for an edge from routes tagged with remoteIngress.enabled. * When a route specifies edgeFilter, only edges whose id or tags match get that route's ports. * When edgeFilter is absent, the route applies to all edges. */ public derivePortsForEdge(edgeId: string, edgeTags?: string[]): number[] { const ports = new Set(); for (const route of this.routes) { if (!route.remoteIngress?.enabled) continue; // Apply edge filter if present const filter = route.remoteIngress.edgeFilter; if (filter && filter.length > 0) { const idMatch = filter.includes(edgeId); const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false; if (!idMatch && !tagMatch) continue; } // Extract ports from the route match if (route.match?.ports) { for (const p of extractPorts(route.match.ports)) { ports.add(p); } } } return [...ports].sort((a, b) => a - b); } /** * Get the effective listen ports for an edge. * Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true. */ public getEffectiveListenPorts(edge: IRemoteIngress): number[] { const manualPorts = edge.listenPorts || []; const shouldDerive = edge.autoDerivePorts !== false; if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b); const derivedPorts = this.derivePortsForEdge(edge.id, edge.tags); return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b); } /** * Get manual and derived port breakdown for an edge (used in API responses). * Derived ports exclude any ports already present in the manual list. */ public getPortBreakdown(edge: IRemoteIngress): { manual: number[]; derived: number[] } { const manual = edge.listenPorts || []; const shouldDerive = edge.autoDerivePorts !== false; if (!shouldDerive) return { manual, derived: [] }; const manualSet = new Set(manual); const allDerived = this.derivePortsForEdge(edge.id, edge.tags); const derived = allDerived.filter((p) => !manualSet.has(p)); return { manual, derived }; } /** * Create a new edge registration. */ public async createEdge( name: string, listenPorts: number[] = [], tags?: string[], autoDerivePorts: boolean = true, ): Promise { const id = plugins.uuid.v4(); const secret = plugins.crypto.randomBytes(32).toString('hex'); const now = Date.now(); const edge: IRemoteIngress = { id, name, secret, listenPorts, enabled: true, autoDerivePorts, tags: tags || [], createdAt: now, updatedAt: now, }; await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge); this.edges.set(id, edge); return edge; } /** * Get an edge by ID. */ public getEdge(id: string): IRemoteIngress | undefined { return this.edges.get(id); } /** * Get all edge registrations. */ public getAllEdges(): IRemoteIngress[] { return Array.from(this.edges.values()); } /** * Update an edge registration. */ public async updateEdge( id: string, updates: { name?: string; listenPorts?: number[]; autoDerivePorts?: boolean; enabled?: boolean; tags?: string[]; }, ): Promise { const edge = this.edges.get(id); if (!edge) { return null; } if (updates.name !== undefined) edge.name = updates.name; if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts; if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts; if (updates.enabled !== undefined) edge.enabled = updates.enabled; if (updates.tags !== undefined) edge.tags = updates.tags; edge.updatedAt = Date.now(); await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge); this.edges.set(id, edge); return edge; } /** * Delete an edge registration. */ public async deleteEdge(id: string): Promise { if (!this.edges.has(id)) { return false; } await this.storageManager.delete(`${STORAGE_PREFIX}${id}`); this.edges.delete(id); return true; } /** * Regenerate the secret for an edge. */ public async regenerateSecret(id: string): Promise { const edge = this.edges.get(id); if (!edge) { return null; } edge.secret = plugins.crypto.randomBytes(32).toString('hex'); edge.updatedAt = Date.now(); await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge); this.edges.set(id, edge); return edge.secret; } /** * Verify an edge's secret using constant-time comparison. */ public verifySecret(id: string, secret: string): boolean { const edge = this.edges.get(id); if (!edge) { return false; } const expected = Buffer.from(edge.secret); const provided = Buffer.from(secret); if (expected.length !== provided.length) { return false; } return plugins.crypto.timingSafeEqual(expected, provided); } /** * Get the list of allowed edges (enabled only) for the Rust hub. */ public getAllowedEdges(): Array<{ id: string; secret: string }> { const result: Array<{ id: string; secret: string }> = []; for (const edge of this.edges.values()) { if (edge.enabled) { result.push({ id: edge.id, secret: edge.secret }); } } return result; } }