Files

131 lines
5.3 KiB
JavaScript

import { createServer } from 'node:http'
import { outboundProxyEnvironment, outboundProxyScopeEnabled } from '../../../../platform/lib/outbound-proxy.mjs'
import { sanitizedProxyConfiguration } from './contracts.mjs'
import { ProxyConfigurationError, proxyErrorBody } from './errors.mjs'
const MAX_CONTROL_BODY_BYTES = 64 * 1024
const MAX_PROVIDER_HANDLE_BODY_BYTES = 4096
const TEST_TASK_ROUTE = /^\/v1\/test\/tasks\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/
async function jsonBody(request, maximum = MAX_CONTROL_BODY_BYTES) {
const chunks = []
let total = 0
for await (const chunk of request) {
total += chunk.byteLength
if (total > maximum) throw new ProxyConfigurationError('control request body is too large')
chunks.push(chunk)
}
try { return JSON.parse(Buffer.concat(chunks).toString('utf8')) } catch {
throw new ProxyConfigurationError('control request body is invalid')
}
}
function send(response, status, value) {
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
response.end(`${JSON.stringify(value)}\n`)
}
function configurationView(snapshot, routeHealth, state = {}) {
return sanitizedProxyConfiguration(snapshot.configuration, snapshot.revision, {
componentReady: true,
routeHealth: routeHealth.status(snapshot),
lastTest: state.lastTest ?? null,
})
}
export function createOutboundProxyControl({
getSnapshot,
routeHealth,
providerHandles,
proxyTests,
commitConfiguration,
getTestState = () => ({ lastTest: null }),
}) {
return createServer(async (request, response) => {
const url = new URL(request.url ?? '/', 'http://outbound-proxy.internal')
const snapshot = getSnapshot()
if (request.method === 'GET' && url.pathname === '/v1/status') {
send(response, 200, {
componentReady: true,
revision: snapshot.revision,
recovery: snapshot.recovery,
routeHealth: routeHealth.status(snapshot),
})
return
}
if (request.method === 'GET' && url.pathname === '/v1/configuration') {
send(response, 200, configurationView(snapshot, routeHealth, getTestState()))
return
}
if (request.method === 'PUT' && url.pathname === '/v1/configuration') {
try {
if (commitConfiguration === undefined) throw new Error('proxy configuration updates are unavailable')
const body = await jsonBody(request)
if (body === null || typeof body !== 'object' || Array.isArray(body)
|| Object.keys(body).sort().join(',') !== 'baseRevision,value') {
throw new ProxyConfigurationError('proxy configuration update is invalid')
}
const activated = await commitConfiguration(body)
providerHandles?.clear()
send(response, 200, configurationView(activated, routeHealth, getTestState()))
} catch (error) {
send(response, error?.statusCode ?? 400, proxyErrorBody(error))
}
return
}
if (request.method === 'POST' && url.pathname === '/v1/test') {
try {
if (proxyTests === undefined) throw new Error('proxy connection tests are unavailable')
send(response, 202, await proxyTests.start(await jsonBody(request), snapshot))
} catch (error) {
send(response, error?.statusCode ?? 400, proxyErrorBody(error))
}
return
}
if (request.method === 'GET' && TEST_TASK_ROUTE.test(url.pathname)) {
try {
if (proxyTests === undefined) throw new Error('proxy connection tests are unavailable')
send(response, 200, proxyTests.get(TEST_TASK_ROUTE.exec(url.pathname)[1]))
} catch (error) {
send(response, error?.statusCode ?? 400, proxyErrorBody(error))
}
return
}
if (request.method === 'DELETE' && TEST_TASK_ROUTE.test(url.pathname)) {
try {
if (proxyTests === undefined) throw new Error('proxy connection tests are unavailable')
send(response, 202, await proxyTests.cancel(TEST_TASK_ROUTE.exec(url.pathname)[1]))
} catch (error) {
send(response, error?.statusCode ?? 400, proxyErrorBody(error))
}
return
}
if (request.method === 'GET' && url.pathname === '/v1/environment') {
try {
const scope = url.searchParams.get('scope')
const environment = outboundProxyEnvironment(scope, {
noProxy: [...snapshot.configuration.noProxy.system, ...snapshot.configuration.noProxy.user],
allProxy: snapshot.configuration.environment.allProxy === 'scope-proxy',
enabled: outboundProxyScopeEnabled(snapshot.configuration, scope),
})
send(response, 200, { scope, revision: snapshot.revision, environment })
} catch (error) {
send(response, 400, { error: error instanceof Error ? error.message : 'invalid scope' })
}
return
}
if (request.method === 'POST' && url.pathname === '/v1/provider-handles') {
try {
if (providerHandles === undefined) throw new Error('provider handles are unavailable')
send(response, 201, providerHandles.issue(await jsonBody(request, MAX_PROVIDER_HANDLE_BODY_BYTES), snapshot))
} catch (error) {
send(response, error?.statusCode ?? 400, proxyErrorBody(error))
}
return
}
send(response, 404, { error: 'not found' })
})
}
export const outboundProxyControlInternals = Object.freeze({ configurationView })