/** * Роуты для управления конфигурациями серверов * * ВАЖНО: /api/server-configs возвращает только jumphost серверы из servers.json * Это нужно для EasySwitch, который работает только с jumphost серверами */ 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 { getS3TextWithHeaders, getS3JsonWithHeaders } = require('../utils/s3Helpers'); const { isValidServerId } = require('../lib/validators'); 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 (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 для совместимости с фронтендом // Fallback: если type не указан, но есть gateways - считаем jumphost (обратная совместимость) const jumphostServers = allServers .filter(s => { const serverType = String(s.type || '').toLowerCase(); // Явно jumphost if (serverType === 'jumphost') return true; // Явно exit - пропускаем if (serverType === 'exit') return false; // Тип не указан, но есть gateways - считаем jumphost (обратная совместимость) if (!serverType && Array.isArray(s.gateways) && s.gateways.length > 0) return true; return false; }) .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 }; function validateServerIdParam(req, res) { const serverId = req.params?.serverId; if (!serverId || !isValidServerId(serverId)) { sendError(res, 400, 'Invalid serverId: only alphanumeric, dot, dash, underscore allowed', 'E_BAD_REQUEST'); return null; } return serverId; } // GET /api/server-configs/:serverId (конкретная конфигурация) async function getServerConfig(req, res) { const serverId = validateServerIdParam(req, res); if (serverId === null) return; await getS3TextWithHeaders( `filter-manager/config-${serverId}.txt`, req, res, { transform: (text) => ({ config: text }), defaultValue: '// Конфигурация не найдена' } ); } // POST /api/server-configs/:serverId (сохранить конфигурацию) async function postServerConfig(req, res) { const serverId = validateServerIdParam(req, res); if (serverId === null) return; const { config } = req.body; if (!config) { return sendError(res, 400, 'Config is required', 'E_BAD_REQUEST'); } try { const meta = await writeS3TextObject(`filter-manager/config-${serverId}.txt`, config); return sendOk(res, meta); } catch (error) { console.error(error); return sendError(res, 500, 'Error writing server config to S3', 'E_S3'); } } // DELETE /api/server-configs/:serverId (удалить только конфигурацию) async function deleteServerConfig(req, res) { const serverId = validateServerIdParam(req, res); if (serverId === null) return; try { await deleteS3Object(`filter-manager/config-${serverId}.txt`); const meta = await headMeta(`filter-manager/config-${serverId}.txt`); return sendOk(res, meta); } catch (error) { console.error(error); return sendError(res, 500, 'Error deleting server config from S3', 'E_S3'); } } // DELETE /api/server-configs/:serverId/complete (удалить конфигурацию и фильтры) async function deleteServerComplete(req, res) { const serverId = validateServerIdParam(req, res); if (serverId === null) return; try { await Promise.allSettled([ deleteS3Object(`filter-manager/config-${serverId}.txt`), deleteS3Object(`filter-manager/server-filters-${serverId}.json`) ]); return res.json({ ok: true, etag: null, lastModified: null, contentLength: null }); } catch (error) { console.error(error); return sendError(res, 500, 'Error deleting server files from S3', 'E_S3'); } } // GET /api/server-filters/:serverId async function getServerFilters(req, res) { const serverId = validateServerIdParam(req, res); if (serverId === null) return; await getS3JsonWithHeaders( `filter-manager/server-filters-${serverId}.json`, req, res, { defaultValue: [] } ); } // POST /api/server-filters/:serverId async function postServerFilters(req, res) { const serverId = validateServerIdParam(req, res); if (serverId === null) return; const { filters } = req.body; if (!Array.isArray(filters)) { return sendError(res, 400, 'Filters must be an array', 'E_BAD_REQUEST'); } for (let i = 0; i < filters.length; i++) { const filter = filters[i]; if (!filter.community || !filter.gateway) { return sendError(res, 400, `Filter at index ${i} is missing required fields`, 'E_SCHEMA'); } } try { const { writeS3JsonObject } = require('../services/s3Service'); const meta = await writeS3JsonObject(`filter-manager/server-filters-${serverId}.json`, filters); return sendOk(res, meta); } catch (error) { console.error(error); return sendError(res, 500, 'Error writing server filters to S3', 'E_S3'); } } module.exports = { serverConfigsListRoutes, getServerConfig, postServerConfig, deleteServerConfig, deleteServerComplete, getServerFilters, postServerFilters, };