NODEDC_PLATFORM/services/dc-amd-proxy/server.mjs

988 lines
39 KiB
JavaScript

import { timingSafeEqual } from "node:crypto";
import { link, mkdir, open, readFile, unlink } from "node:fs/promises";
import { createServer } from "node:http";
import https from "node:https";
import { connect as connectNet } from "node:net";
import { dirname } from "node:path";
import { connect as connectTls } from "node:tls";
const allowedHosts = new Set([
"api.cesium.com",
"assets.ion.cesium.com",
"dev.virtualearth.net",
"ecn.t0.tiles.virtualearth.net",
"ecn.t1.tiles.virtualearth.net",
"ecn.t2.tiles.virtualearth.net",
"ecn.t3.tiles.virtualearth.net",
]);
const connectionTimingSymbol = Symbol("nodedc.connection-timing");
const config = await readConfig();
const transport = createTransport(config);
const server = createServer(async (request, response) => {
const url = new URL(request.url || "/", "http://localhost");
try {
if (request.method === "GET" && ["/healthz", "/status"].includes(url.pathname)) return writeJson(response, 200, await statusBody());
if (request.method === "POST" && url.pathname === "/api/pair") return await pairConnector(request, response);
if (["GET", "HEAD"].includes(request.method || "") && url.pathname === "/proxy/cesium/fetch") return await forwardCesiumRequest(request, response, url);
return writeJson(response, 404, { ok: false, error: "not_found" });
} catch (error) {
if (response.destroyed || response.headersSent || response.writableEnded) {
if (!response.writableEnded) response.destroy();
return;
}
const errorCode = safeError(error);
const status = Number(error?.statusCode || 502);
console.warn(JSON.stringify({ event: "cesium_egress_request_failed", error: errorCode, status, ...telemetryLogFields(error?.telemetry) }));
return writeJson(response, status, { ok: false, error: errorCode }, { "x-nodedc-egress-error": errorCode });
}
});
server.listen(config.port, config.bindAddress, () => console.log(JSON.stringify({ event: "dc_amd_proxy_started", bindAddress: config.bindAddress, port: config.port, connector: `${config.connectorHost}:${config.connectorPort}`, directEgress: false })));
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => server.close(() => transport.close(() => process.exit(0))));
async function readConfig() {
const nodeEnv = String(process.env.NODE_ENV || "production").trim();
return {
port: parsePort(process.env.PORT, 8790),
bindAddress: parseIpv4(process.env.DC_AMD_PROXY_BIND_ADDRESS || "172.22.0.222", "bind_address"),
connectorHost: parseIpv4(process.env.DC_AMD_CONNECTOR_HOST || "172.22.0.183", "connector_host"),
connectorPort: parsePort(process.env.DC_AMD_CONNECTOR_PORT, 8791),
pairAllowedSource: parseIpv4(process.env.DC_AMD_PAIR_ALLOWED_SOURCE || "172.22.0.183", "pair_allowed_source"),
connectorTokenFile: String(process.env.DC_AMD_CONNECTOR_TOKEN_FILE || "/var/lib/dc-amd-proxy/connector-access").trim(),
mapToken: Buffer.from(await readSecretFile(process.env.DC_AMD_MAP_EGRESS_TOKEN_FILE || "/run/nodedc-secrets/map-egress-proxy-token", "map_egress_token"), "utf8"),
connectTimeoutMs: parseDuration(process.env.DC_AMD_CONNECT_TIMEOUT_SECONDS, 20),
upstreamTimeoutMs: parseDuration(process.env.DC_AMD_UPSTREAM_TIMEOUT_SECONDS, 30),
bodyIdleTimeoutMs: parseDuration(process.env.DC_AMD_BODY_IDLE_TIMEOUT_SECONDS, 30),
poolMaxSockets: parsePoolSize(process.env.DC_AMD_POOL_MAX_SOCKETS, 8),
poolMaxFreeSockets: parsePoolSize(process.env.DC_AMD_POOL_MAX_FREE_SOCKETS, 4),
slowRequestMs: parseDuration(process.env.DC_AMD_SLOW_REQUEST_SECONDS, 2),
allowInsecureTls: nodeEnv === "test" && parseBoolean(process.env.DC_AMD_PROXY_TEST_ALLOW_INSECURE_TLS, false),
};
}
async function statusBody() {
const paired = Boolean(await readConnectorToken());
return {
ok: true,
service: "dc-amd-proxy",
state: paired ? "paired" : "awaiting_pair",
forwarding: paired ? "amd_connector_only" : "disabled",
directEgress: false,
connector: `${config.connectorHost}:${config.connectorPort}`,
transport: transport.status(),
};
}
async function pairConnector(request, response) {
if (normalizeAddress(request.socket.remoteAddress) !== config.pairAllowedSource) return writeJson(response, 403, { ok: false, error: "pair_source_not_allowed" });
if (await readConnectorToken()) return writeJson(response, 409, { ok: false, error: "already_paired" });
const body = await readJsonBody(request, 1024);
const token = String(body?.connectorAccessToken || "").trim();
if (!isSecret(token)) return writeJson(response, 400, { ok: false, error: "connector_access_token_invalid" });
await persistConnectorToken(token);
console.log(JSON.stringify({ event: "amd_connector_paired" }));
return writeJson(response, 201, { ok: true, state: "paired" });
}
async function forwardCesiumRequest(request, response, requestUrl) {
if (!isMapAuthorized(request.headers["x-proxy-token"])) return writeJson(response, 401, { ok: false, error: "map_egress_unauthorized" });
const target = parseTarget(requestUrl.searchParams.get("url") || "");
const startedAt = Date.now();
const terminal = transport.beginRequest(target.hostname);
const controller = new AbortController();
let telemetry = emptyTelemetry();
let upstream;
let responseBytes = 0;
let upstreamEnded = false;
let downstreamFinished = false;
let clientAborted = false;
let terminalFinished = false;
let bodyIdleTimer;
let abortUpstream;
const clearBodyIdleTimer = () => {
if (bodyIdleTimer) clearTimeout(bodyIdleTimer);
bodyIdleTimer = undefined;
};
const cleanup = () => {
clearBodyIdleTimer();
request.off("aborted", onClientAbort);
response.off("close", onClientAbort);
if (abortUpstream) controller.signal.removeEventListener("abort", abortUpstream);
};
const finish = (outcome) => {
if (terminalFinished) return false;
terminalFinished = terminal.complete({
...outcome,
durationMs: Date.now() - startedAt,
bytes: responseBytes,
telemetry,
});
if (terminalFinished) cleanup();
return terminalFinished;
};
function onClientAbort() {
if (downstreamFinished || terminalFinished) return;
clientAborted = true;
if (!controller.signal.aborted) controller.abort(abortError());
// Before headers, requestOnce owns the active/queued ClientRequest and its
// signal. The await/catch below records its complete attempt telemetry.
if (upstream) finish({ type: "client-abort" });
}
const armBodyIdleTimer = () => {
clearBodyIdleTimer();
bodyIdleTimer = setTimeout(() => {
const error = typedError("amd_upstream_body_idle_timeout", 502);
error.code = "ETIMEDOUT";
upstream.destroy(error);
}, config.bodyIdleTimeoutMs);
bodyIdleTimer.unref?.();
};
request.once("aborted", onClientAbort);
response.once("close", onClientAbort);
try {
const connectorToken = await readConnectorToken();
if (clientAborted || controller.signal.aborted) {
finish({ type: "client-abort" });
return;
}
if (!connectorToken) {
finish({ type: "request-failure", errorCode: "amd_connector_not_paired" });
return writeJson(response, 503, { ok: false, error: "amd_connector_not_paired" }, { "x-nodedc-egress-error": "amd_connector_not_paired" });
}
const result = await requestThroughAmd(target, request.method || "GET", upstreamHeaders(request), controller.signal);
upstream = result.upstream;
telemetry = result.telemetry;
if (clientAborted || controller.signal.aborted) {
upstream.destroy();
finish({ type: "client-abort" });
return;
}
abortUpstream = () => {
if (!upstreamEnded && !upstream.destroyed) upstream.destroy(abortError());
};
controller.signal.addEventListener("abort", abortUpstream, { once: true });
if (controller.signal.aborted) abortUpstream();
upstream.on("data", (chunk) => {
responseBytes += chunk.length;
armBodyIdleTimer();
});
upstream.once("end", () => {
upstreamEnded = true;
clearBodyIdleTimer();
});
upstream.once("error", (error) => {
upstreamEnded = true;
clearBodyIdleTimer();
if (!finish({ type: clientAborted ? "client-abort" : "stream-failure", errorCode: safeTransportError(error) })) return;
if (!clientAborted) {
console.warn(JSON.stringify({ event: "cesium_egress_stream_failed", host: target.hostname, error: safeTransportError(error), bytes: responseBytes }));
}
if (!response.destroyed) response.destroy();
});
armBodyIdleTimer();
response.writeHead(upstream.statusCode || 502, safeResponseHeaders(upstream.headers));
response.once("finish", () => {
downstreamFinished = true;
clearBodyIdleTimer();
const durationMs = Date.now() - startedAt;
const status = Number(upstream.statusCode || 502);
if (!finish({ type: "response", status })) return;
// Normal tile traffic is intentionally not logged per object. Slow or
// failing responses are sufficient to diagnose VPN/transport regressions
// without creating a noisy, credential-bearing request log.
if (durationMs >= config.slowRequestMs || status >= 400) {
console.warn(JSON.stringify({
event: "cesium_egress_slow_or_failed",
host: target.hostname,
status,
durationMs,
queueMs: telemetry.queueMs,
connectionMs: telemetry.connectionMs,
ttfbMs: telemetry.ttfbMs,
retryMs: telemetry.retryMs,
attempts: telemetry.attempts,
bytes: responseBytes,
reusedSocket: telemetry.reusedSocket,
retries: telemetry.retries,
}));
}
});
upstream.pipe(response);
} catch (error) {
telemetry = mergeTelemetry(telemetry, error?.telemetry);
if (clientAborted || controller.signal.aborted) {
finish({ type: "client-abort" });
return;
}
if (upstream && !upstream.destroyed) upstream.destroy();
finish({ type: "request-failure", errorCode: safeTransportError(error) });
error.telemetry = telemetry;
throw error;
}
}
async function requestThroughAmd(initialTarget, method, headers, signal) {
let target = initialTarget;
let requestHeaders = { ...headers };
let aggregate = emptyTelemetry();
for (let redirects = 0; redirects <= 5; redirects += 1) {
let result;
try {
result = await requestWithSafeRetry(target, method, requestHeaders, signal);
} catch (error) {
error.telemetry = mergeTelemetry(aggregate, error?.telemetry);
throw error;
}
const { upstream } = result;
aggregate = mergeTelemetry(aggregate, result.telemetry);
const statusCode = Number(upstream.statusCode || 502);
const location = Array.isArray(upstream.headers.location) ? upstream.headers.location[0] : upstream.headers.location;
if ([301, 302, 303, 307, 308].includes(statusCode) && location) {
try {
aggregate.attemptMs += await drainRedirect(upstream, signal);
const redirectedTarget = parseTarget(new URL(location, target).toString());
if (redirectedTarget.origin !== target.origin) requestHeaders = withoutAuthorization(requestHeaders);
target = redirectedTarget;
} catch (error) {
aggregate.attemptMs += Math.max(0, Number(error?.redirectDrainMs || 0));
error.telemetry = aggregate;
throw error;
}
continue;
}
return { upstream, telemetry: aggregate };
}
const error = typedError("amd_upstream_redirect_limit", 502);
error.telemetry = aggregate;
throw error;
}
function withoutAuthorization(headers) {
const output = { ...headers };
delete output.authorization;
return output;
}
function drainRedirect(upstream, signal) {
return new Promise((resolve, reject) => {
const startedAt = Date.now();
let finished = false;
let idleTimer;
const cleanup = () => {
if (idleTimer) clearTimeout(idleTimer);
signal.removeEventListener("abort", onAbort);
upstream.off("data", armIdleTimer);
upstream.off("end", onEnd);
upstream.off("aborted", onUpstreamAbort);
upstream.off("error", onError);
};
const complete = () => {
if (finished) return;
finished = true;
cleanup();
resolve(Date.now() - startedAt);
};
const fail = (error) => {
if (finished) return;
finished = true;
cleanup();
error.redirectDrainMs = Date.now() - startedAt;
upstream.destroy();
reject(error);
};
const armIdleTimer = () => {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
const error = typedError("amd_upstream_redirect_body_idle_timeout", 502);
error.code = "ETIMEDOUT";
fail(error);
}, config.bodyIdleTimeoutMs);
idleTimer.unref?.();
};
const onAbort = () => fail(abortReason(signal));
const onEnd = () => complete();
const onUpstreamAbort = () => {
const error = typedError("amd_upstream_redirect_aborted", 502);
error.code = "ECONNRESET";
fail(error);
};
const onError = (error) => fail(error);
signal.addEventListener("abort", onAbort, { once: true });
upstream.on("data", armIdleTimer);
upstream.once("end", onEnd);
upstream.once("aborted", onUpstreamAbort);
upstream.once("error", onError);
if (signal.aborted) onAbort();
else {
armIdleTimer();
upstream.resume();
}
});
}
async function requestWithSafeRetry(target, method, headers, signal) {
let retries = 0;
let aggregate = emptyTelemetry();
while (true) {
try {
const result = await requestOnce(target, method, headers, signal);
aggregate = mergeTelemetry(aggregate, result.telemetry);
aggregate.retries = retries;
if (retries > 0) {
console.warn(JSON.stringify({
event: "cesium_egress_retry_completed",
host: target.hostname,
attempt: retries + 1,
status: Number(result.upstream.statusCode || 502),
...telemetryLogFields(result.telemetry),
totalAttemptMs: aggregate.attemptMs,
}));
}
return { upstream: result.upstream, telemetry: aggregate };
} catch (error) {
const failedAttemptTelemetry = error?.telemetry;
aggregate = mergeTelemetry(aggregate, failedAttemptTelemetry);
if (signal.aborted || retries >= 1 || !["GET", "HEAD"].includes(method) || error?.reusedSocket !== true || !isRetryableSocketError(error)) {
if (retries > 0) {
console.warn(JSON.stringify({
event: "cesium_egress_retry_failed",
host: target.hostname,
attempt: retries + 1,
error: safeTransportError(error),
...telemetryLogFields(failedAttemptTelemetry),
totalAttemptMs: aggregate.attemptMs,
}));
}
error.telemetry = aggregate;
throw error;
}
retries += 1;
aggregate.retryMs += Number(error?.telemetry?.attemptMs || 0);
aggregate.retries = retries;
const errorCode = safeTransportError(error);
transport.recordRetry(target.hostname, errorCode);
console.warn(JSON.stringify({
event: "cesium_egress_reused_socket_retry",
host: target.hostname,
attempt: retries,
error: errorCode,
...telemetryLogFields(failedAttemptTelemetry),
}));
}
}
}
function requestOnce(target, method, headers, externalSignal) {
return new Promise((resolve, reject) => {
const startedAt = Date.now();
let socketAssignedAt = null;
let socketTiming = null;
let reusedSocket = false;
let settled = false;
let timedOut = false;
const pendingConnectionTiming = { startedAt: null, readyAt: null };
const attemptController = new AbortController();
const abortAttempt = () => {
if (!attemptController.signal.aborted) attemptController.abort(abortReason(externalSignal));
};
if (externalSignal.aborted) abortAttempt();
else externalSignal.addEventListener("abort", abortAttempt, { once: true });
const client = https.request({
protocol: "https:",
hostname: target.hostname,
port: 443,
method,
path: `${target.pathname || "/"}${target.search || ""}`,
headers,
agent: transport.agent,
signal: attemptController.signal,
nodedcAbortSignal: attemptController.signal,
nodedcConnectionTiming: pendingConnectionTiming,
});
transport.observeQueuePeak();
queueMicrotask(() => transport.observeQueuePeak());
const timeoutError = typedError("amd_upstream_timeout", 502);
timeoutError.code = "ETIMEDOUT";
const timeout = setTimeout(() => {
timedOut = true;
if (!attemptController.signal.aborted) attemptController.abort(timeoutError);
}, config.upstreamTimeoutMs);
timeout.unref?.();
const cleanup = () => {
clearTimeout(timeout);
externalSignal.removeEventListener("abort", abortAttempt);
};
const attemptTelemetry = (endedAt, receivedHeaders, error) => {
const failedConnectionTiming = error?.[connectionTimingSymbol];
const timing = socketTiming || failedConnectionTiming || (pendingConnectionTiming.startedAt ? pendingConnectionTiming : null);
const newConnection = !reusedSocket && timing;
const queueEnd = newConnection?.startedAt || socketAssignedAt || endedAt;
const connectionMs = newConnection ? Math.max(0, Number(newConnection.readyAt || endedAt) - Number(newConnection.startedAt || endedAt)) : 0;
return {
queueMs: Math.max(0, Number(queueEnd) - startedAt),
connectionMs,
ttfbMs: receivedHeaders && socketAssignedAt ? Math.max(0, endedAt - socketAssignedAt) : 0,
attemptMs: Math.max(0, endedAt - startedAt),
retryMs: 0,
attempts: 1,
retries: 0,
reusedSocket,
};
};
client.once("socket", (socket) => {
socketAssignedAt = Date.now();
socketTiming = socket[connectionTimingSymbol] || null;
const priorAssignments = Number(socketTiming?.assignments || 0);
reusedSocket = client.reusedSocket === true || priorAssignments > 0;
if (socketTiming) socketTiming.assignments = priorAssignments + 1;
});
client.once("error", (rawError) => {
if (settled) return;
settled = true;
cleanup();
const error = timedOut && !externalSignal.aborted ? timeoutError : externalSignal.aborted ? abortReason(externalSignal) : rawError;
if (rawError?.[connectionTimingSymbol] && !error[connectionTimingSymbol]) error[connectionTimingSymbol] = rawError[connectionTimingSymbol];
error.reusedSocket = reusedSocket || client.reusedSocket === true;
error.telemetry = attemptTelemetry(Date.now(), false, error);
reject(error);
});
client.once("response", (upstream) => {
if (settled) {
upstream.destroy();
return;
}
settled = true;
cleanup();
const endedAt = Date.now();
resolve({
upstream,
telemetry: attemptTelemetry(endedAt, true),
});
});
client.end();
transport.observeQueuePeak();
});
}
function emptyTelemetry() {
return { queueMs: 0, connectionMs: 0, ttfbMs: 0, attemptMs: 0, retryMs: 0, attempts: 0, retries: 0, reusedSocket: false };
}
function telemetryLogFields(telemetry) {
if (!telemetry) return {};
return {
queueMs: Math.max(0, Number(telemetry.queueMs || 0)),
connectionMs: Math.max(0, Number(telemetry.connectionMs || 0)),
ttfbMs: Math.max(0, Number(telemetry.ttfbMs || 0)),
attemptMs: Math.max(0, Number(telemetry.attemptMs || 0)),
retryMs: Math.max(0, Number(telemetry.retryMs || 0)),
attempts: Math.max(0, Number(telemetry.attempts || 0)),
retries: Math.max(0, Number(telemetry.retries || 0)),
reusedSocket: telemetry.reusedSocket === true,
};
}
function mergeTelemetry(left, right) {
const merged = { ...emptyTelemetry(), ...(left || {}) };
if (!right) return merged;
for (const name of ["queueMs", "connectionMs", "ttfbMs", "attemptMs", "retryMs", "attempts", "retries"]) {
if (name === "retries") merged[name] = Math.max(Number(merged[name] || 0), Number(right[name] || 0));
else merged[name] += Math.max(0, Number(right[name] || 0));
}
merged.reusedSocket ||= right.reusedSocket === true;
return merged;
}
function createTransport(runtimeConfig) {
const metrics = {
openedTunnels: 0,
tunnelFailures: 0,
requests: 0,
terminalRequests: 0,
inFlightRequests: 0,
completedResponses: 0,
failedRequests: 0,
slowRequests: 0,
responseBytes: 0,
partialBytes: 0,
reusedSocketRequests: 0,
retries: 0,
streamFailures: 0,
clientAborts: 0,
maxQueuedRequests: 0,
timing: {
queueMs: { total: 0, max: 0 },
connectionMs: { total: 0, max: 0 },
ttfbMs: { total: 0, max: 0 },
attemptMs: { total: 0, max: 0 },
retryMs: { total: 0, max: 0 },
durationMs: { total: 0, max: 0 },
},
byHost: {},
lastFailure: null,
lastFailureAt: null,
};
const agent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 30_000,
maxSockets: runtimeConfig.poolMaxSockets,
maxFreeSockets: runtimeConfig.poolMaxFreeSockets,
scheduling: "lifo",
});
// https.Agent maintains a separate pool per origin. A Cesium/Bing tile
// therefore reuses an already authenticated NAS -> AMD -> VPN -> TLS path
// for its host instead of paying a fresh CONNECT and TLS handshake.
agent.createConnection = (options, callback) => {
const hostname = String(options.servername || options.hostname || options.host || "").toLowerCase();
const signal = options.nodedcAbortSignal;
const pendingConnectionTiming = options.nodedcConnectionTiming;
void (async () => {
if (!allowedHosts.has(hostname)) throw typedError("cesium_target_not_allowed", 403);
const connectorToken = await readConnectorToken();
if (!connectorToken) throw typedError("amd_connector_not_paired", 503);
const startedAt = Date.now();
if (pendingConnectionTiming) pendingConnectionTiming.startedAt = startedAt;
let connectedAt = startedAt;
try {
const tunnel = await openConnectorTunnel({ hostname }, connectorToken, signal);
connectedAt = Date.now();
const secureSocket = await openTlsTunnel(tunnel, hostname, signal);
const readyAt = Date.now();
if (pendingConnectionTiming) pendingConnectionTiming.readyAt = readyAt;
secureSocket[connectionTimingSymbol] = { startedAt, readyAt, assignments: 0 };
metrics.openedTunnels += 1;
console.log(JSON.stringify({
event: "cesium_egress_tunnel_opened",
host: hostname,
connectorMs: connectedAt - startedAt,
tlsMs: readyAt - connectedAt,
}));
callback(null, secureSocket);
} catch (error) {
const readyAt = Date.now();
if (pendingConnectionTiming) pendingConnectionTiming.readyAt = readyAt;
error[connectionTimingSymbol] = { startedAt, readyAt, assignments: 0 };
throw error;
}
})().catch((error) => {
const errorCode = safeError(error);
if (error?.code !== "ABORT_ERR") {
metrics.tunnelFailures += 1;
metrics.lastFailure = errorCode;
metrics.lastFailureAt = new Date().toISOString();
}
callback(error);
});
return undefined;
};
return {
agent,
close(callback) { agent.destroy(); callback(); },
beginRequest(host) {
const hostMetrics = perHostMetrics(metrics, host);
metrics.requests += 1;
hostMetrics.requests += 1;
metrics.inFlightRequests += 1;
hostMetrics.inFlightRequests += 1;
let finished = false;
return {
complete({ type, status = 0, durationMs = 0, bytes = 0, telemetry = emptyTelemetry(), errorCode = "" }) {
if (finished) return false;
finished = true;
metrics.terminalRequests += 1;
metrics.inFlightRequests = Math.max(0, metrics.inFlightRequests - 1);
hostMetrics.terminalRequests += 1;
hostMetrics.inFlightRequests = Math.max(0, hostMetrics.inFlightRequests - 1);
const safeBytes = Math.max(0, Number(bytes || 0));
if (telemetry.reusedSocket) {
metrics.reusedSocketRequests += 1;
hostMetrics.reusedSocketRequests += 1;
}
observe(metrics.timing.queueMs, telemetry.queueMs);
observe(metrics.timing.connectionMs, telemetry.connectionMs);
observe(metrics.timing.ttfbMs, telemetry.ttfbMs);
observe(metrics.timing.attemptMs, telemetry.attemptMs);
observe(metrics.timing.retryMs, telemetry.retryMs);
observe(metrics.timing.durationMs, durationMs);
if (type === "response") {
metrics.completedResponses += 1;
hostMetrics.completedResponses += 1;
metrics.responseBytes += safeBytes;
hostMetrics.bytes += safeBytes;
} else {
metrics.partialBytes += safeBytes;
hostMetrics.partialBytes += safeBytes;
}
if (type === "client-abort") {
metrics.clientAborts += 1;
hostMetrics.clientAborts += 1;
}
const failure = type === "stream-failure" || type === "request-failure" || type === "response" && Number(status) >= 400;
if (failure) {
metrics.failedRequests += 1;
hostMetrics.failedRequests += 1;
if (type === "stream-failure") {
metrics.streamFailures += 1;
hostMetrics.streamFailures += 1;
}
metrics.lastFailure = errorCode || `upstream_http_${status}`;
metrics.lastFailureAt = new Date().toISOString();
hostMetrics.lastTransportError = errorCode || `upstream_http_${status}`;
}
if (type !== "client-abort" && durationMs >= runtimeConfig.slowRequestMs) metrics.slowRequests += 1;
return true;
},
};
},
recordRetry(host, errorCode) {
metrics.retries += 1;
const hostMetrics = perHostMetrics(metrics, host);
hostMetrics.retries += 1;
hostMetrics.lastTransportError = errorCode;
},
observeQueuePeak() {
const queuedRequests = socketCount(agent.requests);
metrics.maxQueuedRequests = Math.max(metrics.maxQueuedRequests, queuedRequests);
},
status() {
const queuedRequests = socketCount(agent.requests);
metrics.maxQueuedRequests = Math.max(metrics.maxQueuedRequests, queuedRequests);
return {
pool: {
activeSockets: socketCount(agent.sockets),
idleSockets: socketCount(agent.freeSockets),
queuedRequests,
maxQueuedRequests: metrics.maxQueuedRequests,
maxSockets: runtimeConfig.poolMaxSockets,
maxFreeSockets: runtimeConfig.poolMaxFreeSockets,
byOrigin: poolByOrigin(agent),
},
metrics: publicTransportMetrics(metrics),
};
},
};
}
function perHostMetrics(metrics, host) {
const safeHost = allowedHosts.has(String(host || "").toLowerCase()) ? String(host).toLowerCase() : "unknown";
metrics.byHost[safeHost] ||= {
requests: 0,
terminalRequests: 0,
inFlightRequests: 0,
completedResponses: 0,
failedRequests: 0,
reusedSocketRequests: 0,
retries: 0,
streamFailures: 0,
clientAborts: 0,
bytes: 0,
partialBytes: 0,
lastTransportError: null,
};
return metrics.byHost[safeHost];
}
function observe(bucket, rawValue) {
const value = Math.max(0, Number(rawValue || 0));
bucket.total += value;
bucket.max = Math.max(bucket.max, value);
}
function publicTransportMetrics(metrics) {
const average = (bucket) => metrics.terminalRequests ? Math.round(bucket.total / metrics.terminalRequests) : 0;
return {
openedTunnels: metrics.openedTunnels,
tunnelFailures: metrics.tunnelFailures,
requests: metrics.requests,
terminalRequests: metrics.terminalRequests,
inFlightRequests: metrics.inFlightRequests,
completedResponses: metrics.completedResponses,
failedRequests: metrics.failedRequests,
slowRequests: metrics.slowRequests,
responseBytes: metrics.responseBytes,
partialBytes: metrics.partialBytes,
reusedSocketRequests: metrics.reusedSocketRequests,
retries: metrics.retries,
streamFailures: metrics.streamFailures,
clientAborts: metrics.clientAborts,
timing: {
queueMs: { average: average(metrics.timing.queueMs), max: metrics.timing.queueMs.max },
connectionMs: { average: average(metrics.timing.connectionMs), max: metrics.timing.connectionMs.max },
ttfbMs: { average: average(metrics.timing.ttfbMs), max: metrics.timing.ttfbMs.max },
attemptMs: { average: average(metrics.timing.attemptMs), max: metrics.timing.attemptMs.max },
retryMs: { average: average(metrics.timing.retryMs), max: metrics.timing.retryMs.max },
durationMs: { average: average(metrics.timing.durationMs), max: metrics.timing.durationMs.max },
},
byHost: metrics.byHost,
lastFailure: metrics.lastFailure,
lastFailureAt: metrics.lastFailureAt,
};
}
function poolByOrigin(agent) {
const origins = {};
for (const [key, sockets] of Object.entries(agent.sockets)) poolOrigin(origins, key).active += sockets.length;
for (const [key, sockets] of Object.entries(agent.freeSockets)) poolOrigin(origins, key).idle += sockets.length;
for (const [key, requests] of Object.entries(agent.requests)) poolOrigin(origins, key).queued += requests.length;
return origins;
}
function poolOrigin(origins, agentKey) {
const host = String(agentKey || "").split(":")[0].toLowerCase();
const safeHost = allowedHosts.has(host) ? host : "unknown";
origins[safeHost] ||= { active: 0, idle: 0, queued: 0 };
return origins[safeHost];
}
function openConnectorTunnel(target, connectorToken, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(abortReason(signal));
return;
}
const socket = connectNet({ host: config.connectorHost, port: config.connectorPort });
let buffer = Buffer.alloc(0);
let finished = false;
const timeoutError = typedError("amd_connector_timeout", 502);
timeoutError.code = "ETIMEDOUT";
const timeout = setTimeout(() => fail(timeoutError), config.connectTimeoutMs);
timeout.unref?.();
const cleanup = () => {
clearTimeout(timeout);
signal?.removeEventListener("abort", onAbort);
socket.off("connect", onConnect);
socket.off("error", fail);
socket.off("data", onData);
};
const finish = (value) => {
if (finished) return;
finished = true;
cleanup();
resolve(value);
};
const fail = (error) => {
if (finished) return;
finished = true;
cleanup();
socket.destroy();
reject(error);
};
const onAbort = () => fail(abortReason(signal));
const onConnect = () => socket.write(`CONNECT ${target.hostname}:443 HTTP/1.1\r\nHost: ${target.hostname}:443\r\nProxy-Authorization: Bearer ${connectorToken}\r\n\r\n`);
const onData = (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
if (buffer.length > 16 * 1024) return fail(typedError("amd_connector_response_invalid", 502));
const end = buffer.indexOf("\r\n\r\n");
if (end < 0) return;
if (!/^HTTP\/1\.[01] 200\b/.test(buffer.subarray(0, end).toString("ascii"))) return fail(typedError("amd_connector_rejected", 502));
const remainder = buffer.subarray(end + 4);
if (remainder.length) socket.unshift(remainder);
finish(socket);
};
signal?.addEventListener("abort", onAbort, { once: true });
socket.once("connect", onConnect);
socket.on("error", fail);
socket.on("data", onData);
});
}
function openTlsTunnel(socket, hostname, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
socket.destroy();
reject(abortReason(signal));
return;
}
const secureSocket = connectTls({ socket, servername: hostname, ALPNProtocols: ["http/1.1"], rejectUnauthorized: !config.allowInsecureTls });
let finished = false;
const timeoutError = typedError("amd_upstream_tls_timeout", 502);
timeoutError.code = "ETIMEDOUT";
const timeout = setTimeout(() => fail(timeoutError), config.connectTimeoutMs);
timeout.unref?.();
const cleanup = () => {
clearTimeout(timeout);
signal?.removeEventListener("abort", onAbort);
secureSocket.off("secureConnect", finish);
secureSocket.off("error", fail);
};
const finish = () => {
if (finished) return;
finished = true;
cleanup();
resolve(secureSocket);
};
const fail = (error) => {
if (finished) return;
finished = true;
cleanup();
secureSocket.destroy();
reject(error);
};
const onAbort = () => fail(abortReason(signal));
signal?.addEventListener("abort", onAbort, { once: true });
secureSocket.once("secureConnect", finish);
secureSocket.once("error", fail);
});
}
function upstreamHeaders(request) {
// Do not force `Connection: close`: the shared Agent owns a deliberately
// bounded keep-alive pool for the approved upstream hosts.
const headers = { accept: safeHeader(request.headers.accept, "*/*"), "accept-encoding": "identity" };
for (const name of ["range", "if-none-match", "if-modified-since"]) {
const value = safeHeader(request.headers[name], "");
if (value) headers[name] = value;
}
const authorization = safeHeader(request.headers["x-nodedc-cesium-authorization"], "");
if (authorization) headers.authorization = authorization;
const referer = safeHeader(request.headers["x-nodedc-map-referer"], "");
if (referer) headers.referer = referer;
return headers;
}
function safeResponseHeaders(headers) {
const output = {};
const skipped = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"]);
for (const [name, raw] of Object.entries(headers)) {
if (skipped.has(name.toLowerCase()) || raw === undefined) continue;
const value = Array.isArray(raw) ? raw.join(", ") : String(raw);
if (value.length <= 8192 && !/[\r\n\u0000]/.test(value)) output[name] = value;
}
return output;
}
function parseTarget(value) {
let target;
try { target = new URL(String(value)); } catch { throw typedError("cesium_target_invalid", 400); }
if (target.protocol !== "https:" || target.username || target.password || target.port && target.port !== "443" || !allowedHosts.has(target.hostname.toLowerCase())) throw typedError("cesium_target_not_allowed", 403);
target.hostname = target.hostname.toLowerCase();
return target;
}
async function readConnectorToken() {
try {
const token = (await readFile(config.connectorTokenFile, "utf8")).trim();
return isSecret(token) ? token : null;
} catch (error) {
if (error?.code === "ENOENT") return null;
throw typedError("connector_pair_state_unreadable", 500);
}
}
async function persistConnectorToken(token) {
await mkdir(dirname(config.connectorTokenFile), { recursive: true, mode: 0o700 });
const temporary = `${config.connectorTokenFile}.${process.pid}.${Date.now()}.tmp`;
let handle;
try {
handle = await open(temporary, "wx", 0o600);
await handle.writeFile(`${token}\n`, "ascii");
await handle.sync();
await handle.close();
handle = null;
// link(2) is the compare-and-set: unlike rename(), it cannot overwrite an
// existing pair state. Two concurrent pair attempts therefore produce one
// durable secret and one harmless 409 response.
try {
await link(temporary, config.connectorTokenFile);
} catch (error) {
if (error?.code === "EEXIST") throw typedError("already_paired", 409);
throw error;
}
} finally {
if (handle) await handle.close();
await unlink(temporary).catch(() => {});
}
}
async function readSecretFile(path, label) {
let value;
try { value = (await readFile(String(path), "utf8")).trim(); } catch { throw new Error(`${label}_unreadable`); }
if (!value || value.length > 4096 || /[\u0000-\u001f\u007f\s]/.test(value)) throw new Error(`${label}_invalid`);
return value;
}
function isMapAuthorized(raw) {
if (Array.isArray(raw)) return false;
const candidate = Buffer.from(String(raw || ""), "utf8");
return candidate.length === config.mapToken.length && timingSafeEqual(candidate, config.mapToken);
}
function readJsonBody(request, maxBytes) {
return new Promise((resolve, reject) => {
const chunks = [];
let bytes = 0;
request.on("data", (chunk) => {
bytes += chunk.length;
if (bytes > maxBytes) return reject(typedError("pair_payload_too_large", 413));
chunks.push(chunk);
});
request.once("error", reject);
request.once("end", () => {
try { resolve(JSON.parse(Buffer.concat(chunks).toString("utf8"))); } catch { reject(typedError("pair_payload_invalid", 400)); }
});
});
}
function writeJson(response, status, body, extraHeaders = {}) {
const payload = JSON.stringify(body);
response.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"content-length": Buffer.byteLength(payload),
"cache-control": "no-store",
...extraHeaders,
});
response.end(payload);
}
function parsePort(raw, fallback) {
const value = Number(String(raw || fallback).trim());
if (!Number.isInteger(value) || value < 1024 || value > 65535) throw new Error("invalid_port");
return value;
}
function parseDuration(raw, fallbackSeconds) {
const value = Number(String(raw || fallbackSeconds).trim());
if (!Number.isInteger(value) || value < 1 || value > 120) throw new Error("invalid_duration");
return value * 1000;
}
function parsePoolSize(raw, fallback) {
const value = Number(String(raw || fallback).trim());
if (!Number.isInteger(value) || value < 1 || value > 32) throw new Error("invalid_pool_size");
return value;
}
function socketCount(table) {
return Object.values(table).reduce((total, sockets) => total + (Array.isArray(sockets) ? sockets.length : 0), 0);
}
function parseIpv4(raw, label) {
const value = String(raw || "").trim();
const octets = value.split(".").map(Number);
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) throw new Error(`${label}_invalid`);
return value;
}
function normalizeAddress(value) { return String(value || "").replace(/^::ffff:/, ""); }
function safeHeader(raw, fallback) {
if (Array.isArray(raw)) return fallback;
const value = String(raw || "").trim();
return value.length <= 4096 && !/[\r\n\u0000]/.test(value) ? value : fallback;
}
function parseBoolean(value, fallback) { return value === undefined || value === "" ? fallback : ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase()); }
function isSecret(value) { return /^[A-Za-z0-9_-]{48,256}$/.test(value); }
function typedError(message, statusCode) { const error = new Error(message); error.statusCode = statusCode; return error; }
function abortError() { const error = typedError("amd_client_aborted", 499); error.code = "ABORT_ERR"; return error; }
function abortReason(signal) { return signal?.reason instanceof Error ? signal.reason : abortError(); }
function safeError(error) { return String(error?.message || "dc_amd_proxy_error").replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 120); }
function isRetryableSocketError(error) { return new Set(["ECONNRESET", "EPIPE", "ETIMEDOUT", "ECONNABORTED"]).has(String(error?.code || "").toUpperCase()); }
function safeTransportError(error) {
const code = String(error?.code || "").trim().toLowerCase();
if (/^[a-z0-9_]{1,48}$/.test(code)) return `amd_upstream_${code}`;
return safeError(error);
}