Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
160 lines
6.0 KiB
JavaScript
160 lines
6.0 KiB
JavaScript
/**
|
|
* Кастомные роуты для серверов с поддержкой зашифрованных MikroTik учётных данных
|
|
* Для jumphost и home (входной роутер): mikrotikHost, mikrotikPort, mikrotikUser, encryptedMikrotikPassword
|
|
*/
|
|
|
|
const { GetObjectCommand, PutObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
|
const { s3, BUCKET_NAME, streamToString } = require('../services/s3Service');
|
|
const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler');
|
|
const { encrypt, decrypt } = require('../utils/encryption');
|
|
|
|
const S3_KEY = 'servers.json';
|
|
const SERVER_TYPES = ['jumphost', 'exit', 'bgp', 'dns', 'home'];
|
|
const TYPES_NEED_TUNNEL = ['jumphost', 'exit'];
|
|
const TYPES_NEED_GATEWAYS = ['jumphost', 'exit'];
|
|
|
|
function validateServer(server, i) {
|
|
if (!server.ip || !server.dns || !server.country || !server.provider) {
|
|
return `Server at index ${i} is missing required fields (ip, dns, country, provider)`;
|
|
}
|
|
const normalizedType = String(server.type || '').toLowerCase();
|
|
if (!SERVER_TYPES.includes(normalizedType)) {
|
|
return `Server at index ${i} has invalid type (allowed: ${SERVER_TYPES.join(', ')})`;
|
|
}
|
|
if (TYPES_NEED_TUNNEL.includes(normalizedType) && !server.tunnel) {
|
|
return `Server at index ${i} (${normalizedType}) requires tunnel type`;
|
|
}
|
|
if (TYPES_NEED_GATEWAYS.includes(normalizedType)) {
|
|
if (!Array.isArray(server.gateways) || server.gateways.length === 0) {
|
|
return `Server at index ${i} must have gateways for type ${normalizedType}`;
|
|
}
|
|
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`;
|
|
}
|
|
}
|
|
}
|
|
server.type = normalizedType;
|
|
return null;
|
|
}
|
|
|
|
async function readServersFromS3() {
|
|
try {
|
|
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY }));
|
|
const body = await streamToString(data.Body);
|
|
const parsed = JSON.parse(body || '[]');
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch (e) {
|
|
if (e?.name === 'NoSuchKey' || e?.$metadata?.httpStatusCode === 404) return [];
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function writeServersToS3(servers) {
|
|
const body = JSON.stringify(servers, null, 2);
|
|
await s3.send(new PutObjectCommand({
|
|
Bucket: BUCKET_NAME,
|
|
Key: S3_KEY,
|
|
Body: body,
|
|
ContentType: 'application/json',
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* GET /api/servers — возвращает серверы, для jumphost заменяет encryptedMikrotikPassword на hasMikrotikPassword
|
|
*/
|
|
async function getServers(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 servers = await readServersFromS3();
|
|
const sanitized = servers.map(s => {
|
|
const out = { ...s };
|
|
const hasMikrotikApi = s.type === 'jumphost' || s.type === 'home';
|
|
if (hasMikrotikApi && out.encryptedMikrotikPassword) {
|
|
out.hasMikrotikPassword = true;
|
|
delete out.encryptedMikrotikPassword;
|
|
} else if (hasMikrotikApi) {
|
|
out.hasMikrotikPassword = false;
|
|
}
|
|
return out;
|
|
});
|
|
res.json(sanitized);
|
|
} catch (error) {
|
|
if (error?.name === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
|
|
return res.json([]);
|
|
}
|
|
console.error(error);
|
|
return sendError(res, 500, 'Error reading servers', 'E_S3');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/servers — сохраняет серверы, шифрует mikrotikPassword при наличии
|
|
*/
|
|
async function postServers(req, res) {
|
|
const items = req.body?.domains ?? req.body?.servers;
|
|
if (!Array.isArray(items)) {
|
|
return sendError(res, 400, 'Data must be an array (domains or servers)', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
const err = validateServer(items[i], i);
|
|
if (err) return sendError(res, 400, err, 'E_SCHEMA');
|
|
}
|
|
|
|
try {
|
|
const currentServers = await readServersFromS3();
|
|
const findCurrent = (srv) => currentServers.find(c =>
|
|
(srv.id && c.id === srv.id) || (srv.dns && c.dns === srv.dns) || (srv.ip && c.ip === srv.ip)
|
|
);
|
|
|
|
const toSave = items.map((srv) => {
|
|
const current = findCurrent(srv);
|
|
|
|
if (srv.type === 'jumphost' || srv.type === 'home') {
|
|
const out = { ...srv };
|
|
if (srv.mikrotikPassword !== undefined && srv.mikrotikPassword !== null && String(srv.mikrotikPassword).trim() !== '') {
|
|
try {
|
|
out.encryptedMikrotikPassword = encrypt(String(srv.mikrotikPassword).trim());
|
|
} catch (encErr) {
|
|
console.error('MikroTik password encryption failed:', encErr);
|
|
}
|
|
delete out.mikrotikPassword;
|
|
} else if (current?.encryptedMikrotikPassword) {
|
|
out.encryptedMikrotikPassword = current.encryptedMikrotikPassword;
|
|
}
|
|
return out;
|
|
}
|
|
return srv;
|
|
});
|
|
|
|
await writeServersToS3(toSave);
|
|
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })).catch(() => null);
|
|
return sendOk(res, {
|
|
etag: head?.ETag || null,
|
|
lastModified: head?.LastModified ? head.LastModified.toISOString() : null,
|
|
contentLength: typeof head?.ContentLength === 'number' ? head.ContentLength : null,
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
return sendError(res, 500, error.message || 'Error saving servers', 'E_S3');
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getServers,
|
|
postServers,
|
|
readServersFromS3,
|
|
};
|