Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m24s
256 lines
9.1 KiB
JavaScript
256 lines
9.1 KiB
JavaScript
/**
|
||
* Роуты для управления конфигурациями серверов
|
||
*
|
||
* ВАЖНО: /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,
|
||
};
|
||
|