/** * Роуты для сбора статистики трафика с MikroTik RouterOS (интерфейсы по jumphost). * GET /api/traffic/interface-stats — статистика rx/tx по каждому интерфейсу для всех jumphost. */ const { sendError } = require('../middleware/errorHandler'); const { decrypt } = require('../utils/encryption'); const { readServersFromS3 } = require('./serversRoutes'); const { createRosClient } = require('../services/mikrotikApplyService'); const { readS3TextObject, writeS3JsonObject } = require('../services/s3Service'); /** Получить MikroTik credentials из сервера (jumphost или home) */ function getMikrotikCredentials(server) { if (!server || (server.type !== 'jumphost' && server.type !== 'home')) return null; const host = server.mikrotikHost || server.ip || server.dns; if (!host) return null; const port = server.mikrotikPort; const restPort = (port === 80 || port === 443 || port === 8443) ? port : 80; const user = server.mikrotikUser || 'admin'; let password = ''; if (server.encryptedMikrotikPassword) { try { password = decrypt(server.encryptedMikrotikPassword); } catch (_) { return null; } } return { host, port: restPort, user, password, secure: false }; } // UI-настройки (общие с остальным UI) const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json'; const INTERFACE_SPEED_CACHE_PREFIX = 'interface-speed-cache/'; async function loadUiSettings() { try { const data = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' })); const parsed = JSON.parse(data?.body || '{}'); return parsed && typeof parsed === 'object' ? parsed : {}; } catch (_) { return {}; } } function interfaceSpeedCacheKey(serverId, interfaceName) { const key = `${serverId || ''}|${interfaceName || ''}`; // Хэш не нужен: ключи короткие, но на всякий случай экранируем странные символы return ( INTERFACE_SPEED_CACHE_PREFIX + String(key) .trim() .replace(/[^a-zA-Z0-9_.:-]+/g, '_') + '.json' ); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * GET /api/traffic/interface-stats * Возвращает по каждому jumphost список интерфейсов с rx-byte, tx-byte (из RouterOS REST /interface). */ async function getInterfaceStats(req, res) { try { const servers = await readServersFromS3(); const jumphosts = (Array.isArray(servers) ? servers : []).filter( (s) => s && (String(s.type || '').toLowerCase() === 'jumphost' || String(s.type || '').toLowerCase() === 'home') ); const results = await Promise.all( jumphosts.map(async (server) => { const creds = getMikrotikCredentials(server); if (!creds) { return { serverId: server.id, name: server.name || server.dns || server.ip || server.id, host: server.mikrotikHost || server.ip || server.dns, error: 'MikroTik API не настроен или нет пароля', interfaces: [], }; } try { const client = createRosClient(creds); const apiRes = await client.print('interface'); const raw = apiRes?.data; const list = Array.isArray(raw) ? raw : raw ? [raw] : []; const interfaces = list.map((iface) => { const rx = iface['rx-byte'] != null ? parseInt(String(iface['rx-byte']), 10) : 0; const tx = iface['tx-byte'] != null ? parseInt(String(iface['tx-byte']), 10) : 0; const total = rx + tx; return { name: iface.name != null ? String(iface.name) : '', type: iface.type != null ? String(iface.type) : '', rxBytes: Number.isNaN(rx) ? 0 : rx, txBytes: Number.isNaN(tx) ? 0 : tx, totalBytes: Number.isNaN(total) ? 0 : total, }; }).filter((i) => i.name && i.totalBytes >= 0); return { serverId: server.id, name: server.name || server.dns || server.ip || server.id, host: server.mikrotikHost || server.ip || server.dns, interfaces, }; } catch (err) { const msg = err.response?.data?.message || err.message || 'Ошибка запроса к RouterOS'; return { serverId: server.id, name: server.name || server.dns || server.ip || server.id, host: server.mikrotikHost || server.ip || server.dns, error: msg, interfaces: [], }; } }) ); return res.json({ jumphosts: results }); } catch (error) { console.error('getInterfaceStats:', error); return sendError(res, 500, error.message || 'Ошибка сбора статистики трафика', 'E_TRAFFIC'); } } /** * POST /api/traffic/interface-speed-test * Замеряет среднюю скорость по выбранному интерфейсу RouterOS на jumphost/home. * * Body: * - serverId (обязательный) — id/dns/ip сервера из servers.json * - interfaceName (обязательный) — имя интерфейса в RouterOS (как в /interface) * - durationSeconds? — время замера (по умолчанию из ui-settings.interfaceSpeedTestDurationSeconds или 10 сек) * * Использует счётчики rx-byte/tx-byte: берёт значение в начале и в конце интервала * и делит дельту на время замера. */ async function measureInterfaceSpeed(req, res) { try { const { serverId, interfaceName } = req.body || {}; let { durationSeconds } = req.body || {}; if (!serverId || !interfaceName) { return sendError( res, 400, 'serverId и interfaceName обязательны', 'E_BAD_REQUEST' ); } const uiSettings = await loadUiSettings(); const defaultDuration = Math.max( 1, Math.min( 600, parseInt(uiSettings.interfaceSpeedTestDurationSeconds, 10) || 10 ) ); durationSeconds = Math.max( 1, Math.min( 600, parseInt(durationSeconds != null ? durationSeconds : defaultDuration, 10) || defaultDuration ) ); const cacheMinutes = Math.max( 0, parseInt(uiSettings.interfaceSpeedTestCacheMinutes, 10) || 0 ); const cacheKey = cacheMinutes > 0 ? interfaceSpeedCacheKey(serverId, interfaceName) : null; if (cacheKey) { try { const raw = await readS3TextObject(cacheKey).catch(() => null); if (raw?.body) { const cached = JSON.parse(raw.body); const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0; const ttlMs = cacheMinutes * 60 * 1000; if (cachedAt && Date.now() - cachedAt < ttlMs) { return res.json({ ...cached, cached: true }); } } } catch (_) { // Если кеш не прочитался — просто продолжаем без него } } const servers = await readServersFromS3(); const server = (Array.isArray(servers) ? servers : []).find( (s) => (s.id || s.dns || s.ip) === serverId ) || null; if ( !server || (server.type !== 'jumphost' && String(server.type || '').toLowerCase() !== 'home') ) { return sendError( res, 400, 'Сервер (jumphost или home) не найден', 'E_NOT_FOUND' ); } const creds = getMikrotikCredentials(server); if (!creds) { return sendError( res, 400, 'MikroTik API не настроен или нет пароля', 'E_CREDENTIALS' ); } const client = createRosClient(creds); async function readOne() { const apiRes = await client.print('interface'); const raw = apiRes?.data; const list = Array.isArray(raw) ? raw : raw ? [raw] : []; const iface = list.find( (i) => i && i.name != null && String(i.name).trim() === String(interfaceName).trim() ); if (!iface) { return null; } const rx = iface['rx-byte'] != null ? parseInt(String(iface['rx-byte']), 10) : 0; const tx = iface['tx-byte'] != null ? parseInt(String(iface['tx-byte']), 10) : 0; return { rxBytes: Number.isNaN(rx) ? 0 : rx, txBytes: Number.isNaN(tx) ? 0 : tx, }; } const notFoundMessage = `Интерфейс "${interfaceName}" не найден на RouterOS (${serverId})`; const start = await readOne(); if (!start) { return sendError(res, 400, notFoundMessage, 'E_IFACE_NOT_FOUND'); } await sleep(durationSeconds * 1000); const end = await readOne(); if (!end) { return sendError(res, 400, notFoundMessage, 'E_IFACE_NOT_FOUND'); } const rxBytesDelta = Math.max(0, (end.rxBytes || 0) - (start.rxBytes || 0)); const txBytesDelta = Math.max(0, (end.txBytes || 0) - (start.txBytes || 0)); const totalBytesDelta = rxBytesDelta + txBytesDelta; const duration = durationSeconds || 1; const rxBps = rxBytesDelta * 8 / duration; const txBps = txBytesDelta * 8 / duration; const totalBps = totalBytesDelta * 8 / duration; const payload = { ok: true, serverId, interfaceName, durationSeconds: duration, rxBytesStart: start.rxBytes, txBytesStart: start.txBytes, rxBytesEnd: end.rxBytes, txBytesEnd: end.txBytes, rxBytesDelta, txBytesDelta, totalBytesDelta, rxBps, txBps, totalBps, cached: false, }; if (cacheKey) { writeS3JsonObject(cacheKey, { ...payload, cached: false, cachedAt: Date.now(), }).catch((err) => console.warn( '[traffic][interface-speed-test] cache write failed:', err?.message || err ) ); } return res.json(payload); } catch (error) { console.error('measureInterfaceSpeed:', error); return sendError( res, 500, error?.message || 'Ошибка замера скорости по интерфейсу', 'E_IFACE_SPEED' ); } } module.exports = { getInterfaceStats, measureInterfaceSpeed, };