/** * Роуты для генерации конфигурации 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, };