feat(serverConfigs): implement GET and POST routes for jumphost server management, including validation and filtering logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m22s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m22s
This commit is contained in:
@@ -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) {
|
||||
|
||||
+18
-6
@@ -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
|
||||
|
||||
+47
-25
@@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user