import { createGatewayServer, closeGatewayServer } from './proxy.mjs' import { createBrowserAuthentication } from './browser-auth.mjs' import { LocalApiClient } from '../../../modules/updater/lib/client.mjs' import { safeReturnPath } from './proxy.mjs' import { DshUpstreamAuthentication } from './dsh-upstream-auth.mjs' export const EXTERNAL_HOST = '0.0.0.0' export const EXTERNAL_PORT = 3080 function listen(server, port, host) { return new Promise((resolve, reject) => { const onError = (error) => { server.off('listening', onListening) reject(error) } const onListening = () => { server.off('error', onError) resolve() } server.once('error', onError) server.once('listening', onListening) server.listen(port, host) }) } export async function runGateway(config, { signalSource = process, gatewayFactory = createGatewayServer, externalHost = EXTERNAL_HOST, externalPort = EXTERNAL_PORT, managementPort = null, managementHost = externalHost, managementSocketPath = process.env.DSH_PLATFORM_MANAGEMENT_SOCKET ?? '/run/dsh-platform/management.sock', managementCliSocketPath = process.env.DSH_PLATFORM_MANAGEMENT_CLI_SOCKET ?? '/run/dsh-platform/management-cli.sock', maintenanceSocketPath = process.env.DSH_PLATFORM_MAINTENANCE_SOCKET ?? '/run/dsh-platform/maintenance.sock', lifecycleSocketPath = process.env.DSH_PLATFORM_LIFECYCLE_SOCKET ?? '/run/dsh-platform/dsh-lifecycle.sock', accessSocketPath = process.env.DSH_PLATFORM_ACCESS_SOCKET ?? '/run/dsh-platform/access/access.sock', accessClient, managementClient, platformStatusPollMs = 250, platformStatusCacheMs = 10_000, now = () => Date.now(), report = async () => {}, } = {}) { const record = (message, fields = {}) => Promise.resolve().then(() => report(message, fields)).catch(() => {}) const management = managementClient ?? new LocalApiClient(managementCliSocketPath) const access = accessClient ?? new LocalApiClient(accessSocketPath) const accessRequest = access.request.bind(access) const resilientAccess = accessClient === undefined ? { request: async (method, path, body, headers) => { for (let attempt = 0; ; attempt++) { try { return await accessRequest(method, path, body, headers) } catch (error) { const retryable = ['ENOENT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE'].includes(error?.code) if (!retryable || attempt >= 20) throw error await new Promise(resolve => setTimeout(resolve, Math.min(250, 25 * (attempt + 1)))) } } } } : access const browserAuthentication = createBrowserAuthentication({ access: resilientAccess, safeReturnPath, report: record }) const dshUpstreamAuthentication = new DshUpstreamAuthentication({ socketPath: lifecycleSocketPath }) const isolatedBrowserAuthentication = createBrowserAuthentication({ access: resilientAccess, safeReturnPath, report: record, paths: { authPrefix: '/auth/', accessPrefix: '/access/', transitionPrefix: '/transition/', consolePath: '/' }, }) let lastPlatformStatus let lastPlatformStatusAt = 0 const refreshPlatformStatus = async () => { const status = await management.request('GET', '/_dsh_platform/api/v1/status') lastPlatformStatus = status lastPlatformStatusAt = now() return status } const platformStatus = async () => { try { return await refreshPlatformStatus() } catch (error) { if (lastPlatformStatus !== undefined && now() - lastPlatformStatusAt <= platformStatusCacheMs) { return lastPlatformStatus } throw error } } const server = gatewayFactory({ polyfill: config.polyfill, trustedHosts: config.trustedHosts, managementSocketPath, maintenanceSocketPath, browserAuthentication, dshUpstreamAuthentication, platformStatus, report: record, surface: 'compat', }) const managementServer = managementPort === null || managementPort === undefined ? null : gatewayFactory({ polyfill: false, trustedHosts: config.trustedHosts, managementSocketPath, maintenanceSocketPath, browserAuthentication: isolatedBrowserAuthentication, dshUpstreamAuthentication, platformStatus, report: record, surface: 'management', }) const platformStatusTimer = setInterval(() => { void refreshPlatformStatus().catch(() => {}) }, platformStatusPollMs) platformStatusTimer.unref?.() void refreshPlatformStatus().catch(() => {}) let resolveSignal const receivedSignal = new Promise(resolve => { resolveSignal = resolve }) const onSignal = signal => resolveSignal(signal) const onSigint = () => onSignal('SIGINT') const onSigterm = () => onSignal('SIGTERM') signalSource.once('SIGINT', onSigint) signalSource.once('SIGTERM', onSigterm) try { await record('gateway.starting', { externalHost, externalPort, ...(managementServer === null ? {} : { managementHost, managementPort }), }) await listen(server, externalPort, externalHost) if (managementServer !== null) await listen(managementServer, managementPort, managementHost) await record('gateway.ready', { externalHost, externalPort, ...(managementServer === null ? {} : { managementHost, managementPort }), }) const serverFailed = new Promise((_, reject) => server.once('error', reject)) const signal = await Promise.race([receivedSignal, serverFailed]) await record('gateway.stopping', { signal }) await closeGatewayServer(server) if (managementServer !== null) await closeGatewayServer(managementServer) await record('gateway.stopped') return 0 } catch (error) { await record('gateway.fatal', { error }) if (server.listening || managementServer?.listening) { try { if (server.listening) await closeGatewayServer(server) if (managementServer?.listening) await closeGatewayServer(managementServer) } catch (closeError) { await record('gateway.stop.failed', { error: closeError, cause: error instanceof Error ? error.message : String(error) }) } } throw error } finally { clearInterval(platformStatusTimer) signalSource.off('SIGINT', onSigint) signalSource.off('SIGTERM', onSigterm) } }