diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js deleted file mode 100644 index ba317b0..0000000 --- a/backend/routes/mikrotikConfigRoutes.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * Роуты для генерации конфигурации 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, - }); - } -} - -const APPLY_TIMEOUT_MS = Number(process.env.MIKROTIK_APPLY_TIMEOUT_MS) || 60000; - -/** - * POST /api/mikrotik/apply - * Body: { serverId: string, type?: 'interfaces'|'recursive'|'all', dryRun?: boolean } - * Применяет конфигурацию (интерфейсы, маршруты) на MikroTik через RouterOS API. - * dryRun=true — только показать план, не выполнять. - */ -async function applyMikrotikConfig(req, res) { - let conn = null; - const mikrotikRequests = []; - const body = req.body || {}; - const streamLogs = req.headers['x-stream-logs'] === '1' || req.headers['x-stream-logs'] === 'true'; - - const log = (msg, data = {}) => { - console.log('[MikroTik apply]', msg, data); - if (streamLogs && res.write) { - try { - res.write(JSON.stringify({ t: 'log', msg, ...data }) + '\n'); - } catch (_) {} - } - }; - - const sendSafe = (status, data) => { - try { - if (streamLogs && res.write) { - res.write(JSON.stringify({ t: 'result', ok: status < 400, ...data }) + '\n'); - res.end(); - } else if (!res.headersSent) { - res.status(status).json(data); - } - } catch (e) { - console.error('[MikroTik apply] sendSafe failed:', e); - } - }; - - const sendErrorSafe = (status, data) => { - try { - if (streamLogs && res.write) { - res.write(JSON.stringify({ t: 'error', ok: false, status, ...data }) + '\n'); - res.end(); - } else if (!res.headersSent) { - res.status(status).json(data); - } - } catch (e) { - console.error('[MikroTik apply] sendErrorSafe failed:', e); - } - }; - - try { - if (streamLogs) { - res.setHeader('Content-Type', 'application/x-ndjson'); - res.setHeader('Transfer-Encoding', 'chunked'); - res.status(200); - } - log('Request body', { serverId: body.serverId, type: body.type, dryRun: body.dryRun }); - const { serverId, type = 'all', dryRun = true } = body; - if (!serverId) { - sendErrorSafe(400, { error: 'serverId is required' }); - return; - } - - log('Fetching servers from S3'); - const servers = await fetchJsonFromS3('servers.json', []); - const server = servers.find(s => s.id === serverId || s.dns === serverId || s.ip === serverId); - if (!server) { - sendErrorSafe(404, { error: 'Server not found' }); - return; - } - if (server.type !== 'jumphost') { - sendErrorSafe(400, { error: 'Only jumphost servers support apply via API' }); - return; - } - if (!server.encryptedMikrotikPassword) { - sendErrorSafe(400, { error: 'MikroTik password not configured. Add credentials in Server settings.' }); - return; - } - - log('Decrypting password'); - let password; - try { - password = decrypt(server.encryptedMikrotikPassword); - } catch (decErr) { - sendErrorSafe(500, { error: 'Failed to decrypt MikroTik password' }); - return; - } - - const host = server.mikrotikHost || server.ip || server.dns; - const port = parseInt(server.mikrotikPort || '8728', 10) || 8728; - const user = server.mikrotikUser || 'admin'; - log('Target MikroTik', { host, port, user }); - - log('Fetching network config from S3'); - 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()); - log('Fetching IPSec passwords', { count: passwordIds.length }); - const passwordMap = await fetchIpsecPasswordMap(passwordIds); - - const includeInterfaces = type === 'interfaces' || type === 'all'; - const includeRecursive = type === 'recursive' || type === 'all'; - log('Building MikroTik config blocks', { includeInterfaces, includeRecursive }); - const blocks = await buildMikrotikConfig(config, servers, passwordMap, { - format: 'json', - serverId, - includeInterfaces, - includeRecursive, - }); - log('Blocks built', { count: blocks.length }); - - const runApply = async () => { - conn = new RouterOSAPI({ - host: String(host), - user: String(user), - password: String(password), - port: Number(port) || 8728, - }); - conn.on('error', (e) => log('RouterOS API error', { error: e?.message || String(e), errno: e?.errno })); - conn.on('close', () => log('RouterOS API connection closed')); - try { - log('Connecting to MikroTik', { host: host + ':' + port }); - await conn.connect(); - if (conn.connector) { - conn.connector.on('error', (e) => log('RouterOS connector error', { error: e?.message || String(e) })); - conn.connector.on('timeout', () => log('RouterOS connector timeout')); - } - log('Connected'); - const originalWrite = conn.write.bind(conn); - conn.write = async function (path, args) { - const req = { path, args: args || [] }; - mikrotikRequests.push(req); - log('MikroTik API', { path, args: req.args }); - return originalWrite(path, args); - }; - const allResults = []; - for (const block of blocks) { - log('Applying block', { blockType: block.type, serverName: block.serverName }); - const blockResults = await applyBlock(conn, block, !!dryRun); - allResults.push({ - blockType: block.type, - serverName: block.serverName, - results: blockResults, - }); - } - conn.close(); - conn = null; - 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'), - }; - log('Done', { summary }); - return { ok: true, dryRun: !!dryRun, summary, results: allResults, mikrotikRequests }; - } catch (err) { - if (conn) try { conn.close(); } catch (_) {} - conn = null; - throw err; - } - }; - - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Таймаут: MikroTik недоступен или операция заняла слишком много времени. Проверьте хост, порт и доступность роутера.')), APPLY_TIMEOUT_MS); - }); - - const result = await Promise.race([runApply(), timeoutPromise]); - return sendSafe(200, result); - } catch (error) { - if (conn) try { conn.close(); } catch (_) {} - const msg = error.message || String(error); - log('Error', { error: msg }); - const errPayload = { - ok: false, - error: msg.includes('ECONNREFUSED') ? 'Соединение отклонено. Проверьте хост и порт.' : - msg.includes('ETIMEDOUT') || msg.includes('Таймаут') ? msg : - msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' : msg, - }; - if (mikrotikRequests.length > 0) errPayload.mikrotikRequests = mikrotikRequests; - sendErrorSafe(500, errPayload); - } -} - -module.exports = { - generateMikrotikConfig, - generateInterfaces, - generateRecursiveRoutes, - testMikrotikConnection, - applyMikrotikConfig, -}; diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index 02e51a9..c7e193f 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -2242,7 +2242,7 @@ function NetworkConfigManager() { body: JSON.stringify(requestBody), }); const contentType = response.headers.get('Content-Type') || ''; - const isStream = streamLogs && (contentType.includes('ndjson') || contentType.includes('x-ndjson'); + const isStream = streamLogs && (contentType.includes('ndjson') || contentType.includes('x-ndjson')); let data = {}; if (isStream && response.body) {