import crypto from 'node:crypto' import dns from 'node:dns/promises' import net from 'node:net' import { URL } from 'node:url' import express from 'express' const app = express() const PORT = Math.max(1, Number(process.env.PORT || 8787) || 8787) const PROXY_TOKEN = String(process.env.PROXY_TOKEN || '').trim() const CONTOUR_API_BASE_URL = String( process.env.CONTOUR_API_BASE_URL || 'https://api-zakupki.kontur.ru/external/v1', ).trim().replace(/\/+$/, '') const CONTOUR_API_KEY = String(process.env.CONTOUR_API_KEY || '').trim() const REQUEST_TIMEOUT_MS = Math.max(1000, Number(process.env.REQUEST_TIMEOUT_MS || 30000) || 30000) const MAX_REDIRECTS = Math.max(0, Number(process.env.MAX_REDIRECTS || 5) || 5) const MAX_JSON_RESPONSE_BYTES = Math.max( 1024 * 64, Number(process.env.MAX_JSON_RESPONSE_BYTES || 10 * 1024 * 1024) || (10 * 1024 * 1024), ) const MAX_FETCH_BYTES = Math.max( 1024 * 256, Number(process.env.MAX_FETCH_BYTES || 50 * 1024 * 1024) || (50 * 1024 * 1024), ) const MAX_CESIUM_FETCH_BYTES = Math.max( 1024 * 256, Number(process.env.MAX_CESIUM_FETCH_BYTES || 128 * 1024 * 1024) || (128 * 1024 * 1024), ) const BLOCK_PRIVATE_NETWORKS = String(process.env.BLOCK_PRIVATE_NETWORKS || '1').trim() !== '0' const LOG_LEVEL = String(process.env.LOG_LEVEL || 'info').trim().toLowerCase() || 'info' const DEBUG_FETCH = ['1', 'true', 'yes', 'on'].includes(String(process.env.DEBUG_FETCH || '').trim().toLowerCase()) // Map Gateway has a dedicated outbound route. It is deliberately separate // from /proxy/fetch: only the provider hosts required by the canonical Map // Page can use it, and Cesium credentials never become generic proxy headers. const CESIUM_EGRESS_HOSTS = 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 CREDENTIAL_QUERY_KEYS = new Set([ 'access_token', 'accesstoken', 'iontoken', 'token', 'key', 'apikey', 'api_key', 'signature', 'sig', ]) const LOG_LEVEL_WEIGHT = { debug: 10, info: 20, warn: 30, error: 40, } app.set('trust proxy', true) app.disable('x-powered-by') app.use(express.json({ limit: '2mb', type: ['application/json', 'application/*+json'] })) function shouldLog(level = 'info') { const wanted = LOG_LEVEL_WEIGHT[String(LOG_LEVEL || '').toLowerCase()] || LOG_LEVEL_WEIGHT.info const current = LOG_LEVEL_WEIGHT[String(level || '').toLowerCase()] || LOG_LEVEL_WEIGHT.info return current >= wanted } function log(level, message, fields = {}) { if (!shouldLog(level)) return const line = { ts: new Date().toISOString(), level, message, ...fields, } // eslint-disable-next-line no-console console.log(JSON.stringify(line)) } function getRequestId(req) { const fromHeader = String(req.headers['x-request-id'] || '').trim() return fromHeader || crypto.randomUUID() } function sanitizeErrorMessage(error, fallback = 'proxy_request_failed') { const text = String(error?.message || '').trim() if (!text) return fallback return text.slice(0, 500) } function parseBoolean(raw, fallback = false) { const text = String(raw ?? '').trim().toLowerCase() if (!text) return fallback if (['1', 'true', 'yes', 'on'].includes(text)) return true if (['0', 'false', 'no', 'off'].includes(text)) return false return fallback } function isIpv4Private(address) { const parts = String(address || '').split('.').map((s) => Number(s)) if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return false const [a, b] = parts if (a === 10) return true if (a === 127) return true if (a === 0) return true if (a === 169 && b === 254) return true if (a === 172 && b >= 16 && b <= 31) return true if (a === 192 && b === 168) return true if (a === 100 && b >= 64 && b <= 127) return true if (a === 198 && (b === 18 || b === 19)) return true if (a >= 224) return true return false } function ipv6ToBigInt(address) { const input = String(address || '').toLowerCase() if (!input) return null const percent = input.indexOf('%') const src = percent >= 0 ? input.slice(0, percent) : input if (!src.includes(':')) return null const hasDouble = src.includes('::') if (hasDouble && src.indexOf('::') !== src.lastIndexOf('::')) return null const [headRaw, tailRaw] = hasDouble ? src.split('::') : [src, ''] const head = headRaw ? headRaw.split(':').filter(Boolean) : [] const tail = tailRaw ? tailRaw.split(':').filter(Boolean) : [] const missing = 8 - (head.length + tail.length) if (missing < 0) return null const chunks = hasDouble ? [...head, ...new Array(missing).fill('0'), ...tail] : src.split(':') if (chunks.length !== 8) return null let out = 0n for (const chunk of chunks) { if (!/^[0-9a-f]{1,4}$/i.test(chunk)) return null const value = BigInt(parseInt(chunk, 16)) out = (out << 16n) + value } return out } function isIpv6Private(address) { const normalized = String(address || '').toLowerCase() if (!normalized) return false if (normalized === '::1' || normalized === '::') return true const value = ipv6ToBigInt(normalized) if (value === null) return false // fc00::/7 if ((value & 0xfe00_0000_0000_0000_0000_0000_0000_0000n) === 0xfc00_0000_0000_0000_0000_0000_0000_0000n) return true // fe80::/10 if ((value & 0xffc0_0000_0000_0000_0000_0000_0000_0000n) === 0xfe80_0000_0000_0000_0000_0000_0000_0000n) return true // ff00::/8 multicast if ((value & 0xff00_0000_0000_0000_0000_0000_0000_0000n) === 0xff00_0000_0000_0000_0000_0000_0000_0000n) return true return false } function isPrivateAddress(address) { const kind = net.isIP(address) if (kind === 4) return isIpv4Private(address) if (kind === 6) return isIpv6Private(address) return false } async function assertUrlIsSafe(targetUrl) { let parsed = null try { parsed = new URL(String(targetUrl || '')) } catch { const err = new Error('invalid_target_url') err.statusCode = 400 throw err } const protocol = String(parsed.protocol || '').toLowerCase() if (protocol !== 'http:' && protocol !== 'https:') { const err = new Error('unsupported_target_protocol') err.statusCode = 400 throw err } const hostname = String(parsed.hostname || '').trim() if (!hostname) { const err = new Error('target_hostname_required') err.statusCode = 400 throw err } if (!BLOCK_PRIVATE_NETWORKS) return parsed const lowerHost = hostname.toLowerCase() if (lowerHost === 'localhost' || lowerHost.endsWith('.localhost')) { const err = new Error('target_host_not_allowed') err.statusCode = 403 throw err } const literalIpKind = net.isIP(hostname) if (literalIpKind && isPrivateAddress(hostname)) { const err = new Error('target_private_ip_not_allowed') err.statusCode = 403 throw err } let resolved = [] try { resolved = await dns.lookup(hostname, { all: true, verbatim: true }) } catch { // If DNS cannot resolve now, let fetch handle it later. return parsed } for (const entry of resolved) { const address = String(entry?.address || '').trim() if (!address) continue if (isPrivateAddress(address)) { const err = new Error('target_resolves_to_private_network') err.statusCode = 403 throw err } } return parsed } function assertCesiumEgressTarget(target) { const hostname = String(target?.hostname || '').trim().toLowerCase() if (CESIUM_EGRESS_HOSTS.has(hostname)) return target const err = new Error('cesium_egress_host_not_allowed') err.statusCode = 403 throw err } function redactCredentialUrl(rawUrl) { try { const parsed = new URL(String(rawUrl || '')) for (const key of [...parsed.searchParams.keys()]) { if (CREDENTIAL_QUERY_KEYS.has(key.toLowerCase())) parsed.searchParams.set(key, '') } return parsed.toString() } catch { return '[invalid-target-url]' } } function mapEgressReferer(rawReferer) { const value = String(rawReferer || '').trim() if (!value) return '' let parsed try { parsed = new URL(value) } catch { const err = new Error('invalid_map_egress_referer') err.statusCode = 400 throw err } if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) { const err = new Error('invalid_map_egress_referer') err.statusCode = 400 throw err } parsed.search = '' parsed.hash = '' return parsed.toString() } function mapEgressHeaders(req, target, requestId) { const authorization = String(req.headers['x-nodedc-cesium-authorization'] || '').trim() if (authorization && !/^Bearer\s+[A-Za-z0-9._~+/=-]{16,4096}$/i.test(authorization)) { const err = new Error('invalid_cesium_egress_authorization') err.statusCode = 400 throw err } const range = String(req.headers.range || '').trim() if (range && !/^bytes=\d*-\d*(?:,\d*-\d*)*$/i.test(range)) { const err = new Error('invalid_map_egress_range') err.statusCode = 400 throw err } const accept = String(req.headers.accept || 'application/json, application/octet-stream, image/*, */*;q=0.5').trim().slice(0, 512) const referer = mapEgressReferer(req.headers['x-nodedc-map-referer']) const headers = { accept: accept || '*/*', 'user-agent': 'nodedc-map-egress/1.0', 'x-request-id': requestId, ...(referer ? { referer } : {}), ...(range ? { range } : {}), } // The master Ion token may only be sent to the Ion API. Redirects are // validated per hop and never carry it to an asset/Bing host. if (String(target?.hostname || '').toLowerCase() === 'api.cesium.com' && authorization) { headers.authorization = authorization } return headers } function getProxyTokenFromReq(req) { const fromHeader = String(req.headers['x-proxy-token'] || '').trim() if (fromHeader) return fromHeader const auth = String(req.headers.authorization || '').trim() if (!auth) return '' const m = /^Bearer\s+(.+)$/i.exec(auth) return m ? String(m[1] || '').trim() : '' } function setAccessLogExtra(req, fields = {}) { if (!req || typeof req !== 'object') return const extra = req.accessLogExtra && typeof req.accessLogExtra === 'object' ? req.accessLogExtra : {} req.accessLogExtra = { ...extra, ...(fields && typeof fields === 'object' && !Array.isArray(fields) ? fields : {}), } } function authMiddleware(req, res, next) { if (!PROXY_TOKEN) return next() const token = getProxyTokenFromReq(req) if (!token || token !== PROXY_TOKEN) { return res.status(401).json({ ok: false, error: 'unauthorized', message: 'Invalid proxy token', }) } return next() } app.use((req, res, next) => { const requestId = getRequestId(req) req.requestId = requestId res.setHeader('x-request-id', requestId) next() }) app.use((req, res, next) => { const startedAt = process.hrtime.bigint() req.accessLogExtra = {} res.on('finish', () => { const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6 const payload = { requestId: req.requestId || '', method: req.method, path: req.path, status: Number(res.statusCode || 0), durationMs: Math.round(durationMs * 1000) / 1000, ...(req.accessLogExtra && typeof req.accessLogExtra === 'object' ? req.accessLogExtra : {}), } const level = Number(res.statusCode || 0) >= 500 ? 'error' : (Number(res.statusCode || 0) >= 400 ? 'warn' : 'info') log(level, 'proxy access', payload) }) next() }) app.use(authMiddleware) app.get('/healthz', (req, res) => { res.json({ ok: true, service: 'proxy-contur', time: new Date().toISOString(), mode: { blockPrivateNetworks: BLOCK_PRIVATE_NETWORKS, timeoutMs: REQUEST_TIMEOUT_MS, maxRedirects: MAX_REDIRECTS, maxFetchBytes: MAX_FETCH_BYTES, hasContourApiKey: Boolean(CONTOUR_API_KEY), }, }) }) function pickApiKey(req) { if (CONTOUR_API_KEY) return CONTOUR_API_KEY const headerApiKey = String(req.headers['x-kontur-apikey'] || '').trim() if (headerApiKey) return headerApiKey const queryApiKey = String(req.query?.apiKey || req.query?.api_key || '').trim() if (queryApiKey) return queryApiKey const bodyApiKey = String(req.body?.apiKey || req.body?.api_key || '').trim() return bodyApiKey } async function fetchWithTimeout(url, { method = 'GET', headers = {}, body = undefined, timeoutMs = REQUEST_TIMEOUT_MS, redirect = 'manual', } = {}) { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), Math.max(1000, timeoutMs)) try { return await fetch(url, { method, headers, body, redirect, signal: controller.signal, }) } finally { clearTimeout(timer) } } async function readResponseTextLimited(response, maxBytes = MAX_JSON_RESPONSE_BYTES) { if (!response?.body) return '' const reader = response.body.getReader() const chunks = [] let total = 0 try { while (true) { const { done, value } = await reader.read() if (done) break const buf = Buffer.from(value) total += buf.length if (total > maxBytes) { const err = new Error('upstream_response_too_large') err.statusCode = 413 throw err } chunks.push(buf) } } finally { reader.releaseLock() } return Buffer.concat(chunks).toString('utf8') } function setJsonPassthroughHeaders(res, upstream) { const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8' res.setHeader('content-type', contentType) } app.post('/proxy/contour/search', async (req, res) => { const requestId = req.requestId const apiKey = pickApiKey(req) if (!apiKey) { return res.status(400).json({ ok: false, error: 'contour_apikey_required', message: 'X-Kontur-Apikey is required', requestId, }) } const targetUrl = `${CONTOUR_API_BASE_URL}/search` const timeoutMs = Math.max(1000, Number(req.query?.timeoutMs || REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS) try { await assertUrlIsSafe(targetUrl) const upstream = await fetchWithTimeout(targetUrl, { method: 'POST', timeoutMs, headers: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json', 'x-kontur-apikey': apiKey, 'x-request-id': requestId, }, body: JSON.stringify(req.body && typeof req.body === 'object' ? req.body : {}), }) const bodyText = await readResponseTextLimited(upstream) setAccessLogExtra(req, { route: 'proxy_contour_search', upstreamStatus: Number(upstream.status || 0), responseBytes: Buffer.byteLength(bodyText || '', 'utf8'), }) setJsonPassthroughHeaders(res, upstream) return res.status(upstream.status).send(bodyText) } catch (error) { const statusCode = Number(error?.statusCode || 502) || 502 log('error', 'proxy contour search failed', { requestId, statusCode, message: sanitizeErrorMessage(error), }) return res.status(statusCode).json({ ok: false, error: 'proxy_contour_search_failed', message: sanitizeErrorMessage(error), requestId, }) } }) app.get('/proxy/contour/purchases/:purchaseId', async (req, res) => { const requestId = req.requestId const purchaseId = String(req.params?.purchaseId || '').trim() if (!purchaseId) { return res.status(400).json({ ok: false, error: 'purchase_id_required', message: 'purchaseId is required', requestId, }) } const apiKey = pickApiKey(req) if (!apiKey) { return res.status(400).json({ ok: false, error: 'contour_apikey_required', message: 'X-Kontur-Apikey is required', requestId, }) } const targetUrl = `${CONTOUR_API_BASE_URL}/purchases/${encodeURIComponent(purchaseId)}` const timeoutMs = Math.max(1000, Number(req.query?.timeoutMs || REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS) try { await assertUrlIsSafe(targetUrl) const upstream = await fetchWithTimeout(targetUrl, { method: 'GET', timeoutMs, headers: { accept: 'application/json, text/plain, */*', 'x-kontur-apikey': apiKey, 'x-request-id': requestId, }, }) const bodyText = await readResponseTextLimited(upstream) setAccessLogExtra(req, { route: 'proxy_contour_purchase', purchaseId, upstreamStatus: Number(upstream.status || 0), responseBytes: Buffer.byteLength(bodyText || '', 'utf8'), }) setJsonPassthroughHeaders(res, upstream) return res.status(upstream.status).send(bodyText) } catch (error) { const statusCode = Number(error?.statusCode || 502) || 502 log('error', 'proxy contour purchase failed', { requestId, statusCode, purchaseId, message: sanitizeErrorMessage(error), }) return res.status(statusCode).json({ ok: false, error: 'proxy_contour_purchase_failed', message: sanitizeErrorMessage(error), requestId, }) } }) app.get('/proxy/contour/limit-groups', async (req, res) => { const requestId = req.requestId const apiKey = pickApiKey(req) if (!apiKey) { return res.status(400).json({ ok: false, error: 'contour_apikey_required', message: 'X-Kontur-Apikey is required', requestId, }) } const targetUrl = `${CONTOUR_API_BASE_URL}/limitGroups` const timeoutMs = Math.max(1000, Number(req.query?.timeoutMs || REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS) try { await assertUrlIsSafe(targetUrl) const upstream = await fetchWithTimeout(targetUrl, { method: 'GET', timeoutMs, headers: { accept: 'application/json, text/plain, */*', 'x-kontur-apikey': apiKey, 'x-request-id': requestId, }, }) const bodyText = await readResponseTextLimited(upstream) setAccessLogExtra(req, { route: 'proxy_contour_limit_groups', upstreamStatus: Number(upstream.status || 0), responseBytes: Buffer.byteLength(bodyText || '', 'utf8'), }) setJsonPassthroughHeaders(res, upstream) return res.status(upstream.status).send(bodyText) } catch (error) { const statusCode = Number(error?.statusCode || 502) || 502 log('error', 'proxy contour limit groups failed', { requestId, statusCode, message: sanitizeErrorMessage(error), }) return res.status(statusCode).json({ ok: false, error: 'proxy_contour_limit_groups_failed', message: sanitizeErrorMessage(error), requestId, }) } }) function copyFetchHeaders(res, upstreamHeaders, { omitContentLength = false } = {}) { const passHeaders = [ 'content-type', 'content-length', 'content-disposition', 'cache-control', 'etag', 'last-modified', 'accept-ranges', ] for (const name of passHeaders) { if (omitContentLength && name === 'content-length') continue const value = upstreamHeaders.get(name) if (!value) continue res.setHeader(name, value) } } async function fetchWithSafeRedirects(initialUrl, { timeoutMs = REQUEST_TIMEOUT_MS, maxRedirects = MAX_REDIRECTS, headers = {}, headersForTarget = null, validateTarget = null, redactForLog = null, requestId = '', } = {}) { let currentUrl = String(initialUrl || '') let redirects = 0 while (true) { const parsed = await assertUrlIsSafe(currentUrl) if (typeof validateTarget === 'function') validateTarget(parsed) const requestHeaders = typeof headersForTarget === 'function' ? headersForTarget(parsed) : headers const upstream = await fetchWithTimeout(parsed.toString(), { method: 'GET', timeoutMs, redirect: 'manual', headers: requestHeaders, }) const status = Number(upstream.status || 0) const location = String(upstream.headers.get('location') || '').trim() const isRedirect = status >= 300 && status < 400 && location if (!isRedirect) { return { upstream, finalUrl: parsed.toString(), redirects } } if (redirects >= maxRedirects) { const err = new Error('too_many_redirects') err.statusCode = 508 throw err } let nextUrl = '' try { nextUrl = new URL(location, parsed.toString()).toString() } catch { const err = new Error('invalid_redirect_url') err.statusCode = 502 throw err } if (DEBUG_FETCH) { const safe = typeof redactForLog === 'function' ? redactForLog : (value) => value log('debug', 'proxy fetch redirect', { requestId: String(requestId || ''), from: String(safe(parsed.toString())).slice(0, 512), to: String(safe(nextUrl)).slice(0, 512), status, redirects: redirects + 1, }) } currentUrl = nextUrl redirects += 1 } } function createFetchRoute({ route, errorCode, buildHeaders, validateTarget = null, redactTarget = false, allowRequestTuning = true, omitContentLength = false, fixedMaxBytes = MAX_FETCH_BYTES, }) { return async (req, res) => { const requestId = req.requestId const rawUrl = String(req.query?.url || '').trim() const safeTarget = redactTarget ? redactCredentialUrl(rawUrl) : rawUrl.slice(0, 400) if (!rawUrl) { return res.status(400).json({ ok: false, error: 'url_required', message: 'query parameter "url" is required', requestId, }) } const timeoutMs = allowRequestTuning ? Math.max(1000, Number(req.query?.timeoutMs || REQUEST_TIMEOUT_MS) || REQUEST_TIMEOUT_MS) : REQUEST_TIMEOUT_MS const maxBytes = allowRequestTuning ? Math.max(1024 * 64, Number(req.query?.maxBytes || MAX_FETCH_BYTES) || MAX_FETCH_BYTES) : fixedMaxBytes try { const { upstream, finalUrl, redirects } = await fetchWithSafeRedirects(rawUrl, { timeoutMs, maxRedirects: MAX_REDIRECTS, headersForTarget: (target) => buildHeaders(req, target, requestId), validateTarget, redactForLog: redactTarget ? redactCredentialUrl : null, requestId, }) const safeFinalUrl = redactTarget ? redactCredentialUrl(finalUrl) : finalUrl const contentType = String(upstream.headers.get('content-type') || '').split(';')[0].trim().toLowerCase() const contentLength = Number(upstream.headers.get('content-length') || 0) if (Number.isFinite(contentLength) && contentLength > 0 && contentLength > maxBytes) { setAccessLogExtra(req, { route, upstreamStatus: Number(upstream.status || 0), finalUrl: String(safeFinalUrl || '').slice(0, 512), redirects, contentType, bytes: contentLength, target: safeTarget, error: 'fetch_too_large', }) return res.status(413).json({ ok: false, error: 'fetch_too_large', message: 'Content-Length exceeds maxBytes', requestId, finalUrl: safeFinalUrl, contentLength, maxBytes, }) } copyFetchHeaders(res, upstream.headers, { omitContentLength }) res.setHeader('x-proxy-final-url', safeFinalUrl) res.setHeader('x-proxy-redirects', String(redirects)) res.status(upstream.status) if (!upstream.body) { setAccessLogExtra(req, { route, upstreamStatus: Number(upstream.status || 0), finalUrl: String(safeFinalUrl || '').slice(0, 512), redirects, contentType, bytes: 0, target: safeTarget, }) return res.end() } const reader = upstream.body.getReader() let total = 0 const onClose = () => { reader.cancel().catch(() => {}) } req.on('close', onClose) try { while (true) { const { done, value } = await reader.read() if (done) break const chunk = Buffer.from(value) total += chunk.length if (total > maxBytes) { setAccessLogExtra(req, { route, upstreamStatus: Number(upstream.status || 0), finalUrl: String(safeFinalUrl || '').slice(0, 512), redirects, contentType, bytes: total, target: safeTarget, error: 'fetch_too_large_stream', }) return res.status(413).end() } if (!res.write(chunk)) { await new Promise((resolve) => res.once('drain', resolve)) } } setAccessLogExtra(req, { route, upstreamStatus: Number(upstream.status || 0), finalUrl: String(safeFinalUrl || '').slice(0, 512), redirects, contentType, bytes: total, target: safeTarget, }) return res.end() } finally { req.off('close', onClose) reader.releaseLock() } } catch (error) { const statusCode = Number(error?.statusCode || 502) || 502 setAccessLogExtra(req, { route, target: safeTarget, error: sanitizeErrorMessage(error), }) log('error', 'proxy fetch failed', { requestId, statusCode, message: sanitizeErrorMessage(error), target: safeTarget, }) if (res.headersSent) return res.end() return res.status(statusCode).json({ ok: false, error: errorCode, message: sanitizeErrorMessage(error), requestId, }) } } } app.get('/proxy/fetch', createFetchRoute({ route: 'proxy_fetch', errorCode: 'proxy_fetch_failed', buildHeaders: (req, _target, requestId) => { const includeApiKey = parseBoolean(req.query?.includeApiKey, true) const apiKey = includeApiKey ? pickApiKey(req) : '' return { accept: String(req.headers.accept || '*/*'), 'user-agent': String(req.headers['user-agent'] || 'proxy-contur/0.1'), ...(apiKey ? { 'x-kontur-apikey': apiKey } : {}), 'x-request-id': requestId, } }, })) app.get('/proxy/cesium/fetch', createFetchRoute({ route: 'proxy_cesium_fetch', errorCode: 'proxy_cesium_fetch_failed', buildHeaders: mapEgressHeaders, validateTarget: assertCesiumEgressTarget, redactTarget: true, allowRequestTuning: false, omitContentLength: true, fixedMaxBytes: MAX_CESIUM_FETCH_BYTES, })) app.use((req, res) => { res.status(404).json({ ok: false, error: 'not_found', message: 'Route is not defined', }) }) app.use((error, req, res, _next) => { const requestId = req?.requestId || '' const statusCode = Number(error?.statusCode || 500) || 500 log('error', 'unhandled error', { requestId, statusCode, message: sanitizeErrorMessage(error, 'internal_error'), }) res.status(statusCode).json({ ok: false, error: 'internal_error', message: sanitizeErrorMessage(error, 'internal_error'), requestId, }) }) app.listen(PORT, () => { log('info', 'proxy-contur started', { port: PORT, contourApiBaseUrl: CONTOUR_API_BASE_URL, blockPrivateNetworks: BLOCK_PRIVATE_NETWORKS, hasProxyToken: Boolean(PROXY_TOKEN), hasContourApiKey: Boolean(CONTOUR_API_KEY), logLevel: LOG_LEVEL, debugFetch: DEBUG_FETCH, }) })