diff --git a/backend/routes/serverConfigsRoutes.js b/backend/routes/serverConfigsRoutes.js index 29cc250..44be951 100644 --- a/backend/routes/serverConfigsRoutes.js +++ b/backend/routes/serverConfigsRoutes.js @@ -1,39 +1,137 @@ /** * Роуты для управления конфигурациями серверов + * + * ВАЖНО: /api/server-configs возвращает только jumphost серверы из servers.json + * Это нужно для EasySwitch, который работает только с jumphost серверами */ -const { s3, BUCKET_NAME, writeS3TextObject, deleteS3Object, headMeta } = require('../services/s3Service'); -const { sendError, sendOk } = require('../middleware/errorHandler'); -const { GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3'); +const { s3, BUCKET_NAME, writeS3TextObject, deleteS3Object, headMeta, writeS3JsonObject } = require('../services/s3Service'); +const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler'); +const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3'); const { streamToString } = require('../services/s3Service'); -const { createJsonDataRoutes } = require('./jsonDataRoutes'); const { getS3TextWithHeaders, getS3JsonWithHeaders } = require('../utils/s3Helpers'); -// GET /api/server-configs (список серверов) -const serverConfigsListRoutes = createJsonDataRoutes('servers.json', (server, i) => { - if (!server.id || !server.name) { - return `Server at index ${i} is missing required fields`; +const S3_KEY = 'servers.json'; + +// Валидация jumphost сервера +function validateJumphostServer(server, i) { + if (!server.ip || !server.dns || !server.country || !server.provider || !server.tunnel) { + return `Server at index ${i} is missing required fields (ip, dns, country, provider, tunnel)`; } - if (server.gateways !== undefined) { - if (!Array.isArray(server.gateways)) { - return `Server at index ${i} has invalid gateways (must be array)`; - } - if (server.gateways.length === 0) { - return `Server at index ${i} must have at least one gateway`; - } - const defaults = server.gateways.filter(g => g && g.primary); - if (defaults.length !== 1) { - return `Server at index ${i} must have exactly one primary gateway`; - } - for (let j = 0; j < server.gateways.length; j++) { - const gw = server.gateways[j] || {}; - if (!gw.name || String(gw.name).trim().length === 0) { - return `Server at index ${i} gateway at index ${j} is missing name`; - } + if (String(server.type || '').toLowerCase() !== 'jumphost') { + return `Server at index ${i} must have type 'jumphost'`; + } + if (!Array.isArray(server.gateways) || server.gateways.length === 0) { + return `Server at index ${i} must have at least one gateway`; + } + const primaries = server.gateways.filter(g => g && g.primary); + if (primaries.length !== 1) { + return `Server at index ${i} must have exactly one primary gateway`; + } + for (let j = 0; j < server.gateways.length; j++) { + const gw = server.gateways[j] || {}; + if (!gw.name || String(gw.name).trim().length === 0) { + return `Server at index ${i} gateway at index ${j} is missing name`; } } return null; -}); +} + +// GET /api/server-configs (список только jumphost серверов) +async function getServerConfigsList(req, res) { + try { + const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })).catch(() => null); + const etag = head?.ETag || null; + if (etag) res.set('ETag', String(etag)); + if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString()); + if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength)); + if (checkIfNoneMatch(req, res, etag)) return; + + const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })); + const fileContent = await streamToString(data.Body); + let allServers = []; + + try { + allServers = JSON.parse(fileContent); + if (!Array.isArray(allServers)) { + allServers = []; + } + } catch (parseError) { + console.error(`Error parsing ${S3_KEY}:`, parseError); + allServers = []; + } + + // Фильтруем только jumphost серверы и добавляем id/name для совместимости с фронтендом + const jumphostServers = allServers + .filter(s => String(s.type || '').toLowerCase() === 'jumphost') + .map(s => ({ + ...s, + // Для совместимости с EasySwitchManager добавляем id и name из dns + id: s.id || s.dns, + name: s.name || s.dns + })); + + res.json(jumphostServers); + } catch (error) { + if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) { + res.json([]); + } else { + console.error(error); + return sendError(res, 500, 'Error reading from S3', 'E_S3'); + } + } +} + +// POST /api/server-configs (сохранить jumphost серверы, сохраняя остальные) +async function postServerConfigsList(req, res) { + const { servers: jumphostServers } = req.body; + + if (!Array.isArray(jumphostServers)) { + return sendError(res, 400, 'Servers must be an array', 'E_BAD_REQUEST'); + } + + // Валидация jumphost серверов + for (let i = 0; i < jumphostServers.length; i++) { + const error = validateJumphostServer(jumphostServers[i], i); + if (error) { + return sendError(res, 400, error, 'E_SCHEMA'); + } + } + + try { + // Читаем текущие данные + let allServers = []; + try { + const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })); + const fileContent = await streamToString(data.Body); + allServers = JSON.parse(fileContent); + if (!Array.isArray(allServers)) allServers = []; + } catch (e) { + // Файл не существует или пустой - это нормально + allServers = []; + } + + // Оставляем только НЕ-jumphost серверы (exit и другие) + const nonJumphostServers = allServers.filter(s => + String(s.type || '').toLowerCase() !== 'jumphost' + ); + + // Объединяем: jumphost из запроса + остальные из файла + const updatedServers = [...jumphostServers, ...nonJumphostServers]; + + const meta = await writeS3JsonObject(S3_KEY, updatedServers); + return sendOk(res, meta); + } catch (error) { + console.error(error); + return sendError(res, 500, 'Error writing to S3', 'E_S3'); + } +} + +// Объект с GET и POST для совместимости с server.js +const serverConfigsListRoutes = { + get: getServerConfigsList, + post: postServerConfigsList +}; // GET /api/server-configs/:serverId (конкретная конфигурация) async function getServerConfig(req, res) { diff --git a/example/s3/README.md b/example/s3/README.md index 41db8f3..8324e36 100644 --- a/example/s3/README.md +++ b/example/s3/README.md @@ -21,19 +21,31 @@ s3/ ## Описание файлов ### `servers.json` -Список всех серверов в расширенном режиме. +Список всех серверов. Содержит как jumphost (для EasySwitch), так и exit серверы. + +**ВАЖНО**: API `/api/server-configs` возвращает только серверы с `type: "jumphost"`. + ```json [ { - "id": "server-001", - "name": "SWE-HIPHOST", - "description": "Шведский сервер для стриминга и игр", - "enabled": true, - "createdAt": "2024-01-15T10:30:00.000Z" + "ip": "185.123.45.67", + "dns": "SWE-HIPHOST", + "country": "SE", + "provider": "HipHost", + "tunnel": "wg0", + "type": "jumphost", + "gateways": [ + { "name": "SWE-EXIT-1", "ip": "185.123.45.68", "comment": "Основной выход", "primary": true }, + { "name": "SWE-EXIT-2", "ip": "185.123.45.69", "comment": "Резервный выход", "primary": false } + ] } ] ``` +Типы серверов: +- `jumphost` — основные серверы для маршрутизации (используются в EasySwitch) +- `exit` — выходные серверы (не попадают в EasySwitch) + ### `filter-manager/simple-filters.json` Фильтры для упрощённого режима (применяются ко всем серверам). ```json diff --git a/example/s3/servers.json b/example/s3/servers.json index 9923af8..679fd2e 100644 --- a/example/s3/servers.json +++ b/example/s3/servers.json @@ -1,37 +1,59 @@ [ { - "id": "server-001", - "name": "SWE-HIPHOST", - "description": "Шведский сервер для стриминга и игр", - "enabled": true, - "createdAt": "2024-01-15T10:30:00.000Z" + "ip": "185.123.45.67", + "dns": "SWE-HIPHOST", + "country": "SE", + "provider": "HipHost", + "tunnel": "wg0", + "type": "jumphost", + "gateways": [ + { "name": "SWE-EXIT-1", "ip": "185.123.45.68", "comment": "Основной выход", "primary": true }, + { "name": "SWE-EXIT-2", "ip": "185.123.45.69", "comment": "Резервный выход", "primary": false } + ] }, { - "id": "server-002", - "name": "NL-AMSTERDAM", - "description": "Голландский сервер с низкой задержкой", - "enabled": true, - "createdAt": "2024-01-16T14:20:00.000Z" + "ip": "185.200.50.10", + "dns": "NL-AMSTERDAM", + "country": "NL", + "provider": "DataCenter NL", + "tunnel": "wg1", + "type": "jumphost", + "gateways": [ + { "name": "NL-EXIT-1", "ip": "185.200.50.11", "comment": "Амстердам выход", "primary": true } + ] }, { - "id": "server-003", - "name": "DE-FRANKFURT", - "description": "Немецкий сервер для торрентов", - "enabled": false, - "createdAt": "2024-01-17T09:15:00.000Z" + "ip": "195.100.30.20", + "dns": "DE-FRANKFURT", + "country": "DE", + "provider": "Hetzner", + "tunnel": "wg2", + "type": "exit", + "gateways": [ + { "name": "DE-EXIT-MAIN", "ip": "195.100.30.21", "comment": "Франкфурт", "primary": true } + ] }, { - "id": "server-004", - "name": "US-NEWYORK", - "description": "Американский сервер для Netflix", - "enabled": true, - "createdAt": "2024-01-18T16:45:00.000Z" + "ip": "104.200.100.50", + "dns": "US-NEWYORK", + "country": "US", + "provider": "DigitalOcean", + "tunnel": "wg3", + "type": "jumphost", + "gateways": [ + { "name": "US-EXIT-NYC", "ip": "104.200.100.51", "comment": "New York выход", "primary": true }, + { "name": "US-EXIT-LA", "ip": "104.200.100.52", "comment": "Los Angeles выход", "primary": false } + ] }, { - "id": "server-005", - "name": "JP-TOKYO", - "description": "Японский сервер для аниме и игр", - "enabled": true, - "createdAt": "2024-01-19T11:30:00.000Z" + "ip": "203.150.80.100", + "dns": "JP-TOKYO", + "country": "JP", + "provider": "Sakura Internet", + "tunnel": "wg4", + "type": "jumphost", + "gateways": [ + { "name": "JP-EXIT-1", "ip": "203.150.80.101", "comment": "Токио выход", "primary": true } + ] } ]