Files
router-lists-ui/backend/routes/serverConfigsRoutes.js
T
denozord c6599efb21
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m24s
feat(serverConfigs): enhance jumphost server filtering logic for backward compatibility
2026-01-20 15:53:34 +07:00

256 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Роуты для управления конфигурациями серверов
*
* ВАЖНО: /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 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
};
// GET /api/server-configs/:serverId (конкретная конфигурация)
async function getServerConfig(req, res) {
const { serverId } = req.params;
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 } = req.params;
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 } = req.params;
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 } = req.params;
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 } = req.params;
await getS3JsonWithHeaders(
`filter-manager/server-filters-${serverId}.json`,
req,
res,
{ defaultValue: [] }
);
}
// POST /api/server-filters/:serverId
async function postServerFilters(req, res) {
const { serverId } = req.params;
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,
};