158 lines
6.2 KiB
JavaScript
158 lines
6.2 KiB
JavaScript
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)
|
|
}
|
|
}
|