import * as plugins from '../../plugins.js'; import * as paths from '../../paths.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; import { requireOpsAuth } from '../helpers/auth.js'; export class ConfigHandler { constructor(private opsServerRef: OpsServer) { this.registerHandlers(); } private registerHandlers(): void { // Config endpoint registers directly on viewRouter (valid identity required via middleware) const router = this.opsServerRef.viewRouter; // Get Configuration Handler (read-only) router.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getConfiguration', async (dataArg, toolsArg) => { await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'config:read' }); const config = await this.getConfiguration(); return { config, section: dataArg.section, }; } ) ); } private async getConfiguration(): Promise { const dcRouter = this.opsServerRef.dcRouterRef; const opts = dcRouter.options; const resolvedPaths = dcRouter.resolvedPaths; // --- System --- const storageBackend: 'smartdata-external' | 'smartdata-local' = opts.dbConfig?.mongoDbUrl ? 'smartdata-external' : 'smartdata-local'; const proxyIps = opts.proxyIps || []; const system: interfaces.requests.IConfigData['system'] = { baseDir: resolvedPaths.dcrouterHomeDir, dataDir: resolvedPaths.dataDir, publicIp: opts.publicIp || dcRouter.detectedPublicIp || null, proxyIps, uptime: Math.floor(process.uptime()), storageBackend, storagePath: opts.dbConfig?.mongoDbUrl ? null : opts.dbConfig?.storagePath || resolvedPaths.defaultTsmDbPath, }; // --- SmartProxy --- const acmeConfig = dcRouter.acmeConfigManager?.getConfig(); let acmeInfo: interfaces.requests.IConfigData['smartProxy']['acme'] = null; if (acmeConfig) { acmeInfo = { enabled: acmeConfig.enabled, accountEmail: acmeConfig.accountEmail, useProduction: acmeConfig.useProduction, autoRenew: acmeConfig.autoRenew, renewThresholdDays: acmeConfig.renewThresholdDays, }; } let routeCount = 0; if (dcRouter.routeConfigManager) { try { const merged = await dcRouter.routeConfigManager.getMergedRoutes(); routeCount = merged.routes.length; } catch { routeCount = opts.smartProxyConfig?.routes?.length || 0; } } else if (opts.smartProxyConfig?.routes) { routeCount = opts.smartProxyConfig.routes.length; } const smartProxy: interfaces.requests.IConfigData['smartProxy'] = { enabled: !!dcRouter.smartProxy, routeCount, acme: acmeInfo, }; // --- Email --- let emailDomains: string[] = []; if (dcRouter.emailServer && (dcRouter.emailServer as any).domainRegistry) { emailDomains = (dcRouter.emailServer as any).domainRegistry.getAllDomains(); } else if (opts.emailConfig?.domains) { emailDomains = opts.emailConfig.domains.map((d: any) => typeof d === 'string' ? d : d.domain ); } let portMapping: Record | null = null; const emailSettings = dcRouter.emailSettingsManager?.getPublicSettings(); const rawPortMapping = emailSettings?.portMapping || opts.emailPortConfig?.portMapping; if (rawPortMapping) { portMapping = {}; for (const [ext, int] of Object.entries(rawPortMapping)) { portMapping[String(ext)] = int as number; } } const email: interfaces.requests.IConfigData['email'] = { enabled: emailSettings?.enabled ?? !!dcRouter.emailServer, ports: emailSettings?.ports || opts.emailConfig?.ports || [], portMapping, hostname: emailSettings?.hostname || opts.emailConfig?.hostname || null, // Absent everywhere means the 'remoteIngress' default applies — never // silently claim 'direct' (that masking was the outbound activation bug). outboundMode: emailSettings?.outboundMode || opts.emailOutboundMode || 'remoteIngress', domains: emailDomains, emailRouteCount: emailSettings?.routeCount ?? opts.emailConfig?.routes?.length ?? 0, receivedEmailsPath: emailSettings?.receivedEmailsPath || opts.emailPortConfig?.receivedEmailsPath || null, }; // --- DNS --- const dnsRecords = (opts.dnsRecords || []).map(r => ({ name: r.name, type: r.type, value: r.value, ttl: r.ttl, })); // dnsChallenge: true when at least one DnsProviderDoc exists in the DB. let dnsChallengeEnabled = false; try { dnsChallengeEnabled = (await dcRouter.dnsManager?.hasAnyManagedDomain()) ?? false; } catch { dnsChallengeEnabled = false; } const dns: interfaces.requests.IConfigData['dns'] = { enabled: !!dcRouter.dnsServer, port: 53, nsDomains: opts.dnsNsDomains || [], // The delegation-verified authority set. Deployment configuration no // longer contributes: this is database state, and it is the only state. scopes: dcRouter.dnsAuthorityManager?.getEffectiveZoneNames() || [], recordCount: dnsRecords.length, records: dnsRecords, dnsChallenge: dnsChallengeEnabled, }; // --- TLS --- let tlsSource: 'acme' | 'static' | 'none' = 'none'; if (opts.tls?.certPath && opts.tls?.keyPath) { tlsSource = 'static'; } else if (acmeConfig?.enabled) { tlsSource = 'acme'; } const tls: interfaces.requests.IConfigData['tls'] = { contactEmail: acmeConfig?.accountEmail || null, domain: opts.tls?.domain || null, source: tlsSource, certPath: opts.tls?.certPath || null, keyPath: opts.tls?.keyPath || null, }; // --- Database --- const dbConfig = opts.dbConfig; const cache: interfaces.requests.IConfigData['cache'] = { enabled: dbConfig?.enabled !== false, storagePath: dbConfig?.mongoDbUrl ? null : dbConfig?.storagePath || resolvedPaths.defaultTsmDbPath, dbName: dbConfig?.dbName || 'dcrouter', defaultTTLDays: 30, cleanupIntervalHours: dbConfig?.cleanupIntervalHours || 1, ttlConfig: {}, }; // --- RADIUS --- const radiusCfg = opts.radiusConfig; const radius: interfaces.requests.IConfigData['radius'] = { enabled: !!dcRouter.radiusServer, authPort: radiusCfg?.authPort || null, acctPort: radiusCfg?.acctPort || null, bindAddress: radiusCfg?.bindAddress || null, clientCount: radiusCfg?.clients?.length || 0, vlanDefaultVlan: radiusCfg?.vlanAssignment?.defaultVlan ?? null, vlanAllowUnknownMacs: radiusCfg?.vlanAssignment?.allowUnknownMacs ?? null, vlanMappingCount: radiusCfg?.vlanAssignment?.mappings?.length || 0, }; // --- Remote Ingress --- const riCfg = opts.remoteIngressConfig; const riSettings = dcRouter.remoteIngressManager?.getHubSettings(); const connectedEdgeIps = dcRouter.tunnelManager?.getConnectedEdgeIps() || []; // Determine TLS mode: custom certs > ACME from cert store > self-signed fallback let tlsMode: 'custom' | 'acme' | 'self-signed' = 'self-signed'; if (riCfg?.tls?.certPath && riCfg?.tls?.keyPath) { tlsMode = 'custom'; } else if (riSettings?.hubDomain) { try { const { ProxyCertDoc } = await import('../../db/index.js'); const stored = await ProxyCertDoc.findByDomain(riSettings.hubDomain); if (stored?.publicKey && stored?.privateKey) { tlsMode = 'acme'; } } catch { /* no stored cert */ } } const remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = { enabled: !!riSettings?.enabled, tunnelPort: riSettings?.tunnelPort || null, hubDomain: riSettings?.hubDomain || null, tlsMode, connectedEdgeIps, performance: riSettings?.performance, }; return { system, smartProxy, email, dns, tls, cache, radius, remoteIngress, }; } }