const http = require('http'); const https = require('https'); const { URL } = require('url'); /** * Нормализует базовый URL API EvoBGP до вида …/v1 (без завершающего слэша). */ function normalizeEvobgpBaseUrl(raw) { if (!raw || !String(raw).trim()) return ''; const input = String(raw).trim(); try { const u = new URL(input); let path = (u.pathname || '').replace(/\/+$/, ''); if (!path || path === '/') { u.pathname = '/v1'; } else if (!/\/v1$/i.test(path)) { u.pathname = `${path}/v1`; } else { u.pathname = path; } return `${u.origin}${u.pathname}`.replace(/\/+$/, ''); } catch { return ''; } } /** * Прокси EvoBGP: браузер бьёт в тот же origin (/api/evobgp/…), сервер добавляет Bearer и ходит в EvoBGP по сети Docker (без CORS). */ function createEvobgpProxy({ logger = console } = {}) { return function evobgpProxy(req, res) { const base = normalizeEvobgpBaseUrl(process.env.EVOBGP_API_URL || ''); const token = String(process.env.EVOBGP_API_TOKEN || '').trim(); if (!base) { return res.status(503).json({ code: 'E_EVOBGP_PROXY_NOT_CONFIGURED', message: 'Прокси EvoBGP не настроен: задайте EVOBGP_API_URL (и при необходимости EVOBGP_API_TOKEN) в окружении контейнера.', }); } if (!token) { return res.status(503).json({ code: 'E_EVOBGP_PROXY_NO_TOKEN', message: 'Прокси EvoBGP: не задан EVOBGP_API_TOKEN в окружении сервера.', }); } const suffix = req.url && req.url.length ? req.url : '/'; const targetUrl = `${base}${suffix.startsWith('/') ? suffix : `/${suffix}`}`; let parsed; try { parsed = new URL(targetUrl); } catch { return res.status(500).json({ code: 'E_EVOBGP_PROXY_BAD_URL', message: 'Некорректный EVOBGP_API_URL', }); } const lib = parsed.protocol === 'https:' ? https : http; const headers = { ...req.headers }; delete headers.host; delete headers.connection; delete headers['content-length']; delete headers.cookie; headers.authorization = `Bearer ${token}`; headers.host = parsed.host; const opts = { hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.pathname + parsed.search, method: req.method, headers, timeout: Math.min(120000, Number(process.env.EVOBGP_PROXY_TIMEOUT_MS) || 60000), }; const clientReq = lib.request(opts, (clientRes) => { res.status(clientRes.statusCode); // Не отбрасываем content-encoding: иначе upstream может отдать gzip-тело, // а клиент не узнает, что его нужно распаковать (в браузере будет «мусор»). const skip = new Set(['connection', 'transfer-encoding']); Object.keys(clientRes.headers).forEach((k) => { if (!skip.has(k.toLowerCase())) { res.setHeader(k, clientRes.headers[k]); } }); clientRes.pipe(res); }); clientReq.on('error', (err) => { try { (req.log || logger).error({ err: err.message, target: targetUrl }, 'evobgp proxy'); } catch { /* no-op */ } if (!res.headersSent) { res.status(502).json({ code: 'E_EVOBGP_PROXY_UPSTREAM', message: err.message || 'Не удалось связаться с EvoBGP', }); } }); clientReq.on('timeout', () => { clientReq.destroy(); if (!res.headersSent) { res.status(504).json({ code: 'E_EVOBGP_PROXY_TIMEOUT', message: 'Таймаут запроса к EvoBGP', }); } }); if (req.method === 'OPTIONS' || req.method === 'GET' || req.method === 'HEAD') { clientReq.end(); return; } if (req.method === 'DELETE') { clientReq.end(); return; } const body = req.body !== undefined && req.body !== null && typeof req.body === 'object' ? JSON.stringify(req.body) : typeof req.body === 'string' ? req.body : ''; if (body) { const buf = Buffer.from(body, 'utf8'); clientReq.setHeader('Content-Type', req.headers['content-type'] || 'application/json'); clientReq.setHeader('Content-Length', buf.length); clientReq.end(buf); } else { clientReq.end(); } }; } module.exports = { createEvobgpProxy, normalizeEvobgpBaseUrl, };