Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
/**
|
|
* Роуты для генерации конфигурации MikroTik (интерфейсы, маршруты)
|
|
* Поддерживает форматы: text (как раньше) и json (структурированные операции для API)
|
|
*/
|
|
|
|
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
const { s3, BUCKET_NAME, streamToString } = require('../services/s3Service');
|
|
const { sendError } = require('../middleware/errorHandler');
|
|
const { decrypt } = require('../utils/encryption');
|
|
const {
|
|
buildMikrotikInterfaceBlocks,
|
|
buildMikrotikRecursiveRoutes,
|
|
buildMikrotikConfig,
|
|
} = require('../utils/mikrotikInterfaceGenerator');
|
|
const { readS3TextObject } = require('../services/s3Service');
|
|
const { RouterOSAPI } = require('node-routeros');
|
|
const { applyBlock } = require('../services/mikrotikApplyService');
|
|
|
|
async function fetchJsonFromS3(key, defaultValue = null) {
|
|
try {
|
|
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: key }));
|
|
const body = await streamToString(data.Body);
|
|
const parsed = JSON.parse(body || 'null');
|
|
return parsed ?? defaultValue;
|
|
} catch (e) {
|
|
if (e?.name === 'NoSuchKey' || e?.$metadata?.httpStatusCode === 404) {
|
|
return defaultValue;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function fetchIpsecPasswordMap(passwordIds) {
|
|
const ids = [...new Set((passwordIds || []).filter(Boolean))];
|
|
if (ids.length === 0) return {};
|
|
|
|
const data = await readS3TextObject('network-config/ipsec-passwords.json').catch(() => ({ body: '[]' }));
|
|
let passwords = [];
|
|
try {
|
|
passwords = JSON.parse(data.body || '[]');
|
|
if (!Array.isArray(passwords)) passwords = [];
|
|
} catch {
|
|
passwords = [];
|
|
}
|
|
|
|
const map = {};
|
|
for (const p of passwords) {
|
|
if (ids.includes(p.id) && p.encryptedPassword) {
|
|
try {
|
|
map[p.id] = decrypt(p.encryptedPassword) || '';
|
|
} catch {
|
|
map[p.id] = '';
|
|
}
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* POST /api/mikrotik/generate
|
|
* Body: { format?: 'text'|'json', serverId?: string, type?: 'interfaces'|'recursive'|'all', config?: object, servers?: array }
|
|
* Если config/servers не переданы — загружаются из S3
|
|
*/
|
|
async function generateMikrotikConfig(req, res) {
|
|
try {
|
|
const { format = 'text', serverId, type = 'all', config: bodyConfig, servers: bodyServers } = req.body || {};
|
|
|
|
if (!['text', 'json'].includes(format)) {
|
|
return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
let config = bodyConfig;
|
|
let servers = bodyServers;
|
|
|
|
if (!config) {
|
|
config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [], ipPools: [] });
|
|
if (!config || typeof config !== 'object') config = { gateways: [], tunnelInterfaces: [], ipPools: [] };
|
|
}
|
|
|
|
if (!Array.isArray(servers)) {
|
|
servers = await fetchJsonFromS3('servers.json', []);
|
|
if (!Array.isArray(servers)) servers = [];
|
|
}
|
|
|
|
const passwordIds = (config.tunnelInterfaces || [])
|
|
.filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '')
|
|
.map(i => i.ipsecPasswordId.trim());
|
|
const passwordMap = await fetchIpsecPasswordMap(passwordIds);
|
|
|
|
const options = { format, serverId };
|
|
const includeInterfaces = type === 'interfaces' || type === 'all';
|
|
const includeRecursive = type === 'recursive' || type === 'all';
|
|
|
|
const blocks = await buildMikrotikConfig(config, servers, passwordMap, {
|
|
...options,
|
|
includeInterfaces,
|
|
includeRecursive,
|
|
});
|
|
|
|
res.json({ blocks });
|
|
} catch (error) {
|
|
console.error('Error generating MikroTik config:', error);
|
|
return sendError(res, 500, error.message || 'Error generating configuration', 'E_SERVER');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/mikrotik/generate-interfaces?format=text|json&serverId=...
|
|
* Генерация только интерфейсов
|
|
*/
|
|
async function generateInterfaces(req, res) {
|
|
try {
|
|
const format = (req.query.format || 'text').toLowerCase();
|
|
const serverId = req.query.serverId || null;
|
|
|
|
if (!['text', 'json'].includes(format)) {
|
|
return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] });
|
|
const servers = await fetchJsonFromS3('servers.json', []);
|
|
const passwordIds = (config.tunnelInterfaces || [])
|
|
.filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '')
|
|
.map(i => i.ipsecPasswordId.trim());
|
|
const passwordMap = await fetchIpsecPasswordMap(passwordIds);
|
|
|
|
const blocks = await buildMikrotikInterfaceBlocks(config, servers, passwordMap, {
|
|
format,
|
|
serverId: serverId || undefined,
|
|
});
|
|
|
|
res.json({ blocks });
|
|
} catch (error) {
|
|
console.error('Error generating interfaces:', error);
|
|
return sendError(res, 500, error.message || 'Error generating interfaces', 'E_SERVER');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/mikrotik/generate-recursive-routes?format=text|json&serverId=...
|
|
* Генерация только рекурсивных маршрутов
|
|
*/
|
|
async function generateRecursiveRoutes(req, res) {
|
|
try {
|
|
const format = (req.query.format || 'text').toLowerCase();
|
|
const serverId = req.query.serverId || null;
|
|
|
|
if (!['text', 'json'].includes(format)) {
|
|
return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] });
|
|
const servers = await fetchJsonFromS3('servers.json', []);
|
|
|
|
const blocks = await buildMikrotikRecursiveRoutes(config, servers, {
|
|
format,
|
|
serverId: serverId || undefined,
|
|
});
|
|
|
|
res.json({ blocks });
|
|
} catch (error) {
|
|
console.error('Error generating recursive routes:', error);
|
|
return sendError(res, 500, error.message || 'Error generating routes', 'E_SERVER');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/mikrotik/test-connection
|
|
* Body: { serverId?: string, host?, port?, user?, password? }
|
|
* Если host/port/user/password переданы — тестирует с ними (без сохранения).
|
|
* Иначе берёт credentials из сервера по serverId.
|
|
*/
|
|
async function testMikrotikConnection(req, res) {
|
|
try {
|
|
const { serverId, host: bodyHost, port: bodyPort, user: bodyUser, password: bodyPassword } = req.body || {};
|
|
|
|
let host, port, user, password;
|
|
|
|
if (bodyHost && bodyPassword) {
|
|
host = bodyHost;
|
|
port = parseInt(bodyPort || '8728', 10) || 8728;
|
|
user = bodyUser || 'admin';
|
|
password = bodyPassword;
|
|
} else if (serverId) {
|
|
const servers = await fetchJsonFromS3('servers.json', []);
|
|
const server = servers.find(s => s.id === serverId || s.dns === serverId || s.ip === serverId);
|
|
if (!server) {
|
|
return sendError(res, 404, 'Server not found', 'E_NOT_FOUND');
|
|
}
|
|
if (server.type !== 'jumphost') {
|
|
return sendError(res, 400, 'Only jumphost servers have MikroTik credentials', 'E_BAD_REQUEST');
|
|
}
|
|
if (!server.encryptedMikrotikPassword) {
|
|
return sendError(res, 400, 'MikroTik password not configured for this server', 'E_BAD_REQUEST');
|
|
}
|
|
try {
|
|
password = decrypt(server.encryptedMikrotikPassword);
|
|
} catch (decErr) {
|
|
return sendError(res, 500, 'Failed to decrypt MikroTik password', 'E_DECRYPT');
|
|
}
|
|
host = server.mikrotikHost || server.ip || server.dns;
|
|
port = parseInt(server.mikrotikPort || '8728', 10) || 8728;
|
|
user = server.mikrotikUser || 'admin';
|
|
} else {
|
|
return sendError(res, 400, 'Provide serverId or (host + password)', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
const conn = new RouterOSAPI({
|
|
host: String(host),
|
|
user: String(user),
|
|
password: String(password),
|
|
port: Number(port) || 8728,
|
|
});
|
|
|
|
await conn.connect();
|
|
await conn.write('/system/resource/print', []);
|
|
conn.close();
|
|
|
|
res.json({ ok: true, message: 'Соединение успешно' });
|
|
} catch (error) {
|
|
const msg = error.message || String(error);
|
|
res.status(400).json({
|
|
ok: false,
|
|
message: msg.includes('ECONNREFUSED') ? 'Соединение отклонено. Проверьте хост и порт.' :
|
|
msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' :
|
|
msg,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/mikrotik/apply
|
|
* Body: { serverId: string, type?: 'interfaces'|'recursive'|'all', dryRun?: boolean }
|
|
* Применяет конфигурацию (интерфейсы, маршруты) на MikroTik через RouterOS API.
|
|
* dryRun=true — только показать план, не выполнять.
|
|
*/
|
|
async function applyMikrotikConfig(req, res) {
|
|
try {
|
|
const { serverId, type = 'all', dryRun = true } = req.body || {};
|
|
if (!serverId) {
|
|
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
const servers = await fetchJsonFromS3('servers.json', []);
|
|
const server = servers.find(s => s.id === serverId || s.dns === serverId || s.ip === serverId);
|
|
if (!server) {
|
|
return sendError(res, 404, 'Server not found', 'E_NOT_FOUND');
|
|
}
|
|
if (server.type !== 'jumphost') {
|
|
return sendError(res, 400, 'Only jumphost servers support apply via API', 'E_BAD_REQUEST');
|
|
}
|
|
if (!server.encryptedMikrotikPassword) {
|
|
return sendError(res, 400, 'MikroTik password not configured. Add credentials in Server settings.', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
let password;
|
|
try {
|
|
password = decrypt(server.encryptedMikrotikPassword);
|
|
} catch (decErr) {
|
|
return sendError(res, 500, 'Failed to decrypt MikroTik password', 'E_DECRYPT');
|
|
}
|
|
|
|
const host = server.mikrotikHost || server.ip || server.dns;
|
|
const port = parseInt(server.mikrotikPort || '8728', 10) || 8728;
|
|
const user = server.mikrotikUser || 'admin';
|
|
|
|
const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] });
|
|
const passwordIds = (config.tunnelInterfaces || [])
|
|
.filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '')
|
|
.map(i => i.ipsecPasswordId.trim());
|
|
const passwordMap = await fetchIpsecPasswordMap(passwordIds);
|
|
|
|
const includeInterfaces = type === 'interfaces' || type === 'all';
|
|
const includeRecursive = type === 'recursive' || type === 'all';
|
|
const blocks = await buildMikrotikConfig(config, servers, passwordMap, {
|
|
format: 'json',
|
|
serverId,
|
|
includeInterfaces,
|
|
includeRecursive,
|
|
});
|
|
|
|
const conn = new RouterOSAPI({
|
|
host: String(host),
|
|
user: String(user),
|
|
password: String(password),
|
|
port: Number(port) || 8728,
|
|
});
|
|
|
|
await conn.connect();
|
|
|
|
const allResults = [];
|
|
for (const block of blocks) {
|
|
const blockResults = await applyBlock(conn, block, !!dryRun);
|
|
allResults.push({
|
|
blockType: block.type,
|
|
serverName: block.serverName,
|
|
results: blockResults,
|
|
});
|
|
}
|
|
|
|
conn.close();
|
|
|
|
const summary = {
|
|
created: allResults.flatMap(b => b.results).filter(r => r.status === 'created' || r.status === 'would_create').length,
|
|
updated: allResults.flatMap(b => b.results).filter(r => r.status === 'updated' || r.status === 'would_update').length,
|
|
skipped: allResults.flatMap(b => b.results).filter(r => r.status === 'skip').length,
|
|
errors: allResults.flatMap(b => b.results).filter(r => r.status === 'error'),
|
|
};
|
|
|
|
res.json({
|
|
ok: true,
|
|
dryRun: !!dryRun,
|
|
summary,
|
|
results: allResults,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error applying MikroTik config:', error);
|
|
const msg = error.message || String(error);
|
|
return res.status(500).json({
|
|
ok: false,
|
|
error: msg.includes('ECONNREFUSED') ? 'Соединение отклонено' :
|
|
msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' : msg,
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
generateMikrotikConfig,
|
|
generateInterfaces,
|
|
generateRecursiveRoutes,
|
|
testMikrotikConnection,
|
|
applyMikrotikConfig,
|
|
};
|