diff --git a/DOCKER.md b/DOCKER.md index 2376588..edb9954 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -75,6 +75,9 @@ services: environment: - NODE_ENV=production - ENCRYPTION_KEY=${ENCRYPTION_KEY} # Обязательно! Используйте .env файл или secrets + # Прокси EvoBGP для UI (Communities / Domains / …) — без CORS в браузере: + # - EVOBGP_API_URL=http://evobgp:8080 + # - EVOBGP_API_TOKEN=... s3-lists-manager-tabler: image: git.shts.su/[repository]:tabler @@ -104,6 +107,8 @@ docker-compose up -d | `NODE_ENV` | Окружение Node.js | `production` | Нет | | `PORT` | Порт для запуска сервера | `3001` | Нет | | `ENCRYPTION_KEY` | Ключ шифрования для IPSec паролей (64 hex символа) | - | **Да** | +| `EVOBGP_API_URL` | Базовый URL API EvoBGP для серверного прокси `/api/evobgp` (например `http://evobgp:8080` или `http://10.0.0.5:8080`; при необходимости к пути добавляется `/v1`) | - | Да, если используете интеграцию EvoBGP в UI | +| `EVOBGP_API_TOKEN` | Bearer-токен к API EvoBGP (только на сервере, не в браузере) | - | Да, если используете интеграцию EvoBGP в UI | ### ENCRYPTION_KEY diff --git a/Dockerfile.fast b/Dockerfile.fast index b98f143..e267822 100644 --- a/Dockerfile.fast +++ b/Dockerfile.fast @@ -50,6 +50,10 @@ COPY backend/services ./services COPY backend/utils ./utils COPY --from=frontend-builder /app/frontend/dist ./public +# EvoBGP (справочник community / router-lists через прокси /api/evobgp, без CORS в браузере): +# EVOBGP_API_URL — например http://evobgp:8080 или http://192.168.1.10:8080 (к /v1 приведётся автоматически при необходимости) +# EVOBGP_API_TOKEN — Bearer-токен API EvoBGP +# # ENCRYPTION_KEY должен быть установлен через переменные окружения при запуске контейнера # Это обязательный параметр для работы с IPSec паролями # Пример: docker run -e ENCRYPTION_KEY=your-64-hex-character-key ... diff --git a/backend/routes/evobgpProxyRoutes.js b/backend/routes/evobgpProxyRoutes.js new file mode 100644 index 0000000..dc56e40 --- /dev/null +++ b/backend/routes/evobgpProxyRoutes.js @@ -0,0 +1,146 @@ +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); + const skip = new Set(['connection', 'transfer-encoding', 'content-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, +}; diff --git a/backend/server.js b/backend/server.js index d082ecd..e944c56 100644 --- a/backend/server.js +++ b/backend/server.js @@ -38,6 +38,7 @@ const { initNetworkMapScheduler } = require('./services/networkMapScheduler'); const { initPingServicesScheduler } = require('./services/pingServicesScheduler'); const { initUptimeMonitorScheduler } = require('./services/uptimeMonitorScheduler'); const schedulerRoutes = require('./routes/schedulerRoutes'); +const { createEvobgpProxy } = require('./routes/evobgpProxyRoutes'); const app = express(); const port = Number(process.env.PORT) || 3001; @@ -156,6 +157,9 @@ app.get('/api/version', (req, res) => { }); }); +// === EvoBGP reverse proxy (токен и URL только на сервере; без CORS в браузере) === +app.use('/api/evobgp', createEvobgpProxy({ logger })); + // === Cache TTL === const DEFAULT_CACHE_TTL = Math.max(0, Math.min(300, Number(process.env.CACHE_TTL_SECONDS) || 30)); app.use((req, res, next) => { diff --git a/frontend/src/CommunitiesManager.jsx b/frontend/src/CommunitiesManager.jsx index bf1774b..b6d2ea5 100644 --- a/frontend/src/CommunitiesManager.jsx +++ b/frontend/src/CommunitiesManager.jsx @@ -67,9 +67,9 @@ function CommunitiesManager() { const [evobgpSettingsOpen, setEvobgpSettingsOpen] = useState(() => { if (import.meta.env?.VITE_EVOBGP_API_URL) return false; try { - return !window.localStorage?.getItem('evobgp_api_url'); + return Boolean(window.localStorage?.getItem('evobgp_api_url')); } catch { - return true; + return false; } }); @@ -293,9 +293,26 @@ function CommunitiesManager() { const handleSave = () => saveAll(items); const saveEvobgpConnection = () => { - const norm = normalizeEvobgpApiBaseUrl(evobgpUrlDraft); + const trimmedUrl = String(evobgpUrlDraft || '').trim(); + if (!trimmedUrl) { + try { + window.localStorage.removeItem('evobgp_api_url'); + window.localStorage.removeItem('evobgp_api_token'); + } catch (_e) { + setError('Не удалось очистить настройки в localStorage.'); + return; + } + setEvobgpUrlDraft(''); + setEvobgpTokenDraft(''); + setError(''); + setSuccess('Включён серверный прокси /api/evobgp (EVOBGP_API_URL и EVOBGP_API_TOKEN в контейнере).'); + fetchItems(); + setTimeout(() => setSuccess(''), 5000); + return; + } + const norm = normalizeEvobgpApiBaseUrl(trimmedUrl); if (!norm) { - setError('Укажите корректный URL EvoBGP (например http://192.168.0.1:8080). Порт должен совпадать с API EvoBGP, не с этим интерфейсом.'); + setError('Укажите корректный URL EvoBGP (например http://192.168.0.1:8080). Порт — у API EvoBGP, не у этого интерфейса.'); return; } try { @@ -307,7 +324,7 @@ function CommunitiesManager() { } setEvobgpUrlDraft(norm); setError(''); - setSuccess('Подключение к EvoBGP сохранено. Загружаем данные…'); + setSuccess('Прямое подключение к EvoBGP из браузера сохранено. Загружаем данные…'); fetchItems(); setTimeout(() => setSuccess(''), 4000); }; @@ -370,24 +387,25 @@ function CommunitiesManager() { {evobgpSettingsOpen && (
- Укажите прямой URL сервера EvoBGP (схема + хост + порт API), например{' '}
- http://192.168.100.67:8080 — не адрес этого веб-интерфейса. К пути будет добавлено{' '}
- /v1 автоматически. Токен — Bearer из EvoBGP.
+ По умолчанию запросы идут на внутренний прокси /api/evobgp: URL и токен задаются в{' '}
+ переменных окружения контейнера — EVOBGP_API_URL и EVOBGP_API_TOKEN{' '}
+ (без CORS). Ниже — только опциональное прямое подключение из браузера к EvoBGP; оставьте URL пустым и
+ нажмите «Сохранить», чтобы снова использовать прокси.