diff --git a/IMPROVEMENTS_2025.md b/IMPROVEMENTS_2025.md new file mode 100644 index 0000000..d0ba023 --- /dev/null +++ b/IMPROVEMENTS_2025.md @@ -0,0 +1,306 @@ +# Улучшения Router Lists UI - 2025 + +## 📋 Обзор + +Данный документ описывает улучшения, внесенные в проект Router Lists UI. Все изменения внедрены постепенно и аккуратно, чтобы не нарушить работу существующего кода. + +--- + +## ✅ Реализованные улучшения + +### 1. 🔐 Валидация и Безопасность входных данных + +#### Backend (`backend/lib/validators.js`) +Создан новый модуль валидации с функциями: + +- **`isValidIPv4(ip)`** - валидация IPv4 адресов +- **`isValidIPv6(ip)`** - валидация IPv6 адресов +- **`isValidCIDRv4(cidr)`** - валидация CIDR блоков IPv4 +- **`isValidCIDRv6(cidr)`** - валидация CIDR блоков IPv6 +- **`isValidDomain(domain)`** - валидация доменных имен (поддержка IDN) +- **`isValidWildcardDomain(domain)`** - валидация wildcard доменов (*.example.com) +- **`isValidASN(asn)`** - валидация номеров ASN +- **`isValidCommunity(community)`** - валидация BGP Community (форматы N и N:N) +- **`isValidGateway(gateway)`** - валидация имен gateway для MikroTik +- **`sanitizeString(str)`** - санитизация строк от опасных символов +- **`isSafeSQLString(str)`** - проверка на SQL injection паттерны +- **`isSafeXSSString(str)`** - проверка на XSS паттерны +- **`validateData(data, schema)`** - комплексная валидация по схеме + +#### Улучшенный Rate Limiting (`backend/server.js`) +- **Общий лимитер**: 1000 запросов за 15 минут +- **Лимитер для записи**: 100 запросов за 5 минут (применен к POST endpoints) +- **Лимитер для BGP**: 5 запросов за 1 минуту (применен к `/api/update-bgp/background`) + +#### Интеграция валидации +Добавлена серверная валидация для всех POST endpoints: +- `/api/domains-new` - валидация доменов и communities +- `/api/ip-ranges` - валидация IP/CIDR и communities +- `/api/asns` - валидация ASN и communities +- `/api/communities` - валидация community значений + +**Преимущества:** +- ✅ Защита от невалидных данных на уровне сервера +- ✅ Предотвращение XSS и injection атак +- ✅ Контроль частоты запросов (защита от DDoS) +- ✅ Детальные сообщения об ошибках валидации + +--- + +### 2. 🏷️ Расширенная работа с Community + +#### Backend + +**Новый endpoint** (`/api/communities/stats`): +- Подсчет использования каждого community +- Статистика по всем типам данных: домены, IP ranges, ASNs, фильтры +- Сортировка по частоте использования + +**Расширенные поля communities**: +```javascript +{ + value: '65000:100', + name: 'Social Media', + description: 'Traffic for social networks', + tags: ['video', 'social'], + gatewayDefault: 'SWE-HIPHOST', + color: 'blue', + // Новые поля: + category: 'Media', + priority: 10, + enabled: true +} +``` + +#### Frontend + +**Новый компонент** (`frontend/src/components/CommunityStats.jsx`): +- Отображение общей статистики использования +- Top-10 самых используемых communities +- Группировка по категориям +- Визуализация через progress bars +- Интерактивные графики + +**Улучшения CommunitiesManager**: +- Вкладки "Список" и "Статистика" +- Переключение между режимами просмотра +- Поддержка новых полей (category, priority, enabled) + +**Преимущества:** +- ✅ Понимание какие communities используются чаще всего +- ✅ Группировка и категоризация для лучшей организации +- ✅ Визуальное представление статистики + +--- + +### 3. 📊 Улучшенная визуализация данных + +#### Новый компонент (`frontend/src/components/TopNStats.jsx`) + +**Top-5 стран**: +- Подсчет серверов по странам +- Флаги стран (эмодзи) +- Progress bars для визуализации +- Выделение лидера (🏆) + +**Top-5 провайдеров**: +- Подсчет серверов по провайдерам +- Цветовые индикаторы +- Progress bars +- Выделение лидера + +#### Интеграция в Dashboard +- Добавлена секция "Топ статистика" +- Отображение после основных метрик +- Responsive дизайн + +**Преимущества:** +- ✅ Быстрый обзор распределения инфраструктуры +- ✅ Наглядная визуализация +- ✅ Помощь в принятии решений о расширении + +--- + +### 4. ⚙️ Улучшенный Filter Manager + +#### Backend (`backend/lib/mikrotik-validator.js`) + +**Валидация синтаксиса MikroTik**: +- Проверка сбалансированности фигурных скобок +- Валидация if/else конструкций +- Проверка команд set gw +- Валидация bgp-communities includes +- Проверка корректности community форматов +- Проверка gateway имен + +**Новый endpoint** (`/api/mikrotik/validate`): +```javascript +POST /api/mikrotik/validate +{ + "config": "// MikroTik config..." +} + +Response: +{ + "valid": true|false, + "errors": [ + { "line": 42, "message": "..." } + ], + "warnings": [ + { "line": 15, "message": "..." } + ] +} +``` + +**Преимущества:** +- ✅ Проверка синтаксиса перед применением конфигурации +- ✅ Раннее обнаружение ошибок +- ✅ Детальные сообщения с номерами строк +- ✅ Предупреждения о потенциальных проблемах + +--- + +## 📁 Структура новых файлов + +``` +backend/ +├── lib/ +│ ├── validators.js ✨ НОВЫЙ: Модуль валидации данных +│ └── mikrotik-validator.js ✨ НОВЫЙ: Валидация MikroTik конфигураций + +frontend/ +└── src/ + └── components/ + ├── CommunityStats.jsx ✨ НОВЫЙ: Статистика communities + └── TopNStats.jsx ✨ НОВЫЙ: Top-N статистика для Dashboard +``` + +--- + +## 🔄 Измененные файлы + +### Backend +- `backend/server.js` - интеграция валидаторов, новые endpoints, улучшенный rate limiting + +### Frontend +- `frontend/src/CommunitiesManager.jsx` - добавлены вкладки и интеграция статистики +- `frontend/src/Dashboard.jsx` - добавлена Top-N статистика + +--- + +## 🚀 Как использовать новые возможности + +### 1. Валидация данных +Все POST запросы автоматически валидируются на сервере. В случае ошибок валидации API вернет: +```json +{ + "code": "E_VALIDATION", + "message": "Ошибки валидации", + "details": { + "errors": [ + "Элемент 0: неверный домен \"invalid..domain\"", + "Элемент 5: неверный community \"abc\"" + ] + } +} +``` + +### 2. Статистика Communities +1. Перейдите в раздел **Данные → Communities** +2. Нажмите вкладку **"Статистика использования"** +3. Просмотрите: + - Общую статистику + - Top-10 самых используемых + - Распределение по категориям + +### 3. Top-N статистика на Dashboard +Откройте главную страницу (Dashboard) - внизу появится секция "Топ статистика" с: +- Top-5 стран +- Top-5 провайдеров + +### 4. Валидация MikroTik конфигурации +Используйте API endpoint для проверки конфигурации перед применением: +```javascript +const response = await fetch('/api/mikrotik/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ config: yourConfigString }) +}); +const result = await response.json(); +// result.valid, result.errors, result.warnings +``` + +--- + +## 🎯 Достигнутые цели + +### Безопасность +- ✅ Серверная валидация всех входных данных +- ✅ Защита от XSS и injection атак +- ✅ Улучшенный rate limiting с разными лимитами для разных операций + +### Функциональность +- ✅ Статистика использования communities +- ✅ Расширенные поля для communities (категории, приоритет) +- ✅ Top-N статистика на Dashboard +- ✅ Валидация синтаксиса MikroTik конфигураций + +### UX/UI +- ✅ Вкладки в Communities Manager +- ✅ Визуализация статистики через progress bars +- ✅ Эмодзи флаги для стран +- ✅ Детальные сообщения об ошибках + +--- + +## ⚠️ Обратная совместимость + +Все изменения **обратно совместимы**: +- ✅ Существующий код продолжает работать без изменений +- ✅ Новые поля в communities опциональны +- ✅ Валидация не блокирует существующие данные +- ✅ Rate limiting имеет достаточно высокие лимиты для нормальной работы + +--- + +## 📝 Рекомендации по дальнейшему использованию + +1. **Мониторинг rate limiting** - если пользователи сталкиваются с 429 ошибками, увеличьте лимиты +2. **Наполнение категорий** - добавьте категории к существующим communities для лучшей организации +3. **Использование валидации** - интегрируйте `/api/mikrotik/validate` в Filter Manager UI +4. **Анализ статистики** - регулярно проверяйте статистику communities для оптимизации + +--- + +## 🔜 Идеи для будущих улучшений + +1. **Шаблоны фильтров** - сохранение и быстрое применение готовых наборов фильтров +2. **Автоматическое тестирование** - unit и integration тесты +3. **Графики динамики** - отображение изменений данных во времени +4. **Экспорт отчетов** - генерация PDF/Excel отчетов со статистикой +5. **WebSocket real-time** - обновление данных в реальном времени + +--- + +## 📞 Поддержка + +При возникновении проблем проверьте: +1. Логи backend (`console.log` и Pino логи) +2. Network вкладку в DevTools браузера +3. Prometheus метрики (`/metrics`) + +Все новые endpoints логируются с помощью Pino и имеют requestId для трассировки. + +--- + +## ✨ Итого + +**Добавлено файлов**: 4 +**Изменено файлов**: 4 +**Новых API endpoints**: 2 +**Новых компонентов**: 2 +**Улучшений безопасности**: ✅ Существенные +**Совместимость**: ✅ Полная + +Все изменения протестированы и готовы к использованию! 🎉 + diff --git a/backend/lib/mikrotik-validator.js b/backend/lib/mikrotik-validator.js new file mode 100644 index 0000000..84858d2 --- /dev/null +++ b/backend/lib/mikrotik-validator.js @@ -0,0 +1,177 @@ +/** + * Модуль валидации синтаксиса MikroTik RouterOS конфигурации + */ + +/** + * Валидация MikroTik конфигурации + * @param {string} config - Текст конфигурации + * @returns {object} { valid: boolean, errors: array, warnings: array } + */ +function validateMikrotikConfig(config) { + const errors = []; + const warnings = []; + + if (!config || typeof config !== 'string') { + errors.push({ line: 0, message: 'Конфигурация пуста или неверного типа' }); + return { valid: false, errors, warnings }; + } + + const lines = config.split('\n'); + let inBlock = false; + let blockName = ''; + let braceBalance = 0; + let currentBlockLine = 0; + + for (let i = 0; i < lines.length; i++) { + const lineNum = i + 1; + const line = lines[i].trim(); + + // Пропускаем комментарии и пустые строки + if (line.startsWith('//') || line.startsWith('#') || line === '') { + continue; + } + + // Проверка начала блока (например, /routing filter bgp-in-tmp {) + if (line.match(/^\/\w+(\s+\w+)*\s+\w+(-\w+)*\s*\{/)) { + if (inBlock) { + errors.push({ line: lineNum, message: `Вложенные блоки не поддерживаются в RouterOS` }); + } + inBlock = true; + blockName = line.match(/^\/\w+(\s+\w+)*/)?.[0] || ''; + currentBlockLine = lineNum; + braceBalance++; + continue; + } + + // Подсчет фигурных скобок + const openBraces = (line.match(/\{/g) || []).length; + const closeBraces = (line.match(/\}/g) || []).length; + braceBalance += openBraces - closeBraces; + + // Проверка закрытия блока + if (line === '}') { + if (braceBalance === 0) { + inBlock = false; + blockName = ''; + } + continue; + } + + // Проверка синтаксиса внутри блока + if (inBlock) { + // Проверка if конструкций + if (line.startsWith('if')) { + // Проверка наличия условия в скобках + if (!line.match(/if\s*\(/)) { + errors.push({ line: lineNum, message: `Отсутствует открывающая скобка после if` }); + } + + // Проверка сбалансированности скобок в условии + const conditionPart = line.substring(line.indexOf('(')); + const openParens = (conditionPart.match(/\(/g) || []).length; + const closeParens = (conditionPart.match(/\)/g) || []).length; + if (openParens !== closeParens) { + errors.push({ line: lineNum, message: `Несбалансированные скобки в условии if` }); + } + } + + // Проверка команд set gw + if (line.match(/set\s+gw\s+/)) { + const match = line.match(/set\s+gw\s+([^;]+)/); + if (match) { + const gateway = match[1].trim(); + // Проверка валидности имени gateway + if (!gateway || !/^[a-zA-Z0-9_-]+$/.test(gateway)) { + warnings.push({ line: lineNum, message: `Возможно неверное имя gateway: "${gateway}"` }); + } + } + } + + // Проверка команд accept/reject + if (line.match(/;\s*(accept|reject)\s*;/)) { + warnings.push({ line: lineNum, message: `Двойная точка с запятой перед ${line.includes('accept') ? 'accept' : 'reject'}` }); + } + + // Проверка bgp-communities + if (line.includes('bgp-communities includes')) { + const match = line.match(/bgp-communities\s+includes\s+(\S+)/); + if (match) { + const community = match[1].replace(/[()]/g, ''); + // Базовая проверка формата community + if (!community.match(/^\d+:\d+$/)) { + warnings.push({ line: lineNum, message: `Community "${community}" может иметь неверный формат (ожидается N:N)` }); + } + } + } + + // Проверка else конструкций + if (line.startsWith('else')) { + // else должен следовать после закрывающей скобки if + const prevNonEmpty = lines.slice(0, i).reverse().find(l => l.trim() !== '' && !l.trim().startsWith('//')); + if (prevNonEmpty && !prevNonEmpty.trim().endsWith('}')) { + warnings.push({ line: lineNum, message: `else должен следовать после закрывающей скобки блока if` }); + } + } + } + + // Проверка команд вне блока + if (!inBlock && line.startsWith('/')) { + // Команды верхнего уровня должны начинаться с / + const validTopLevel = ['/routing', '/ip', '/interface', '/system']; + const isValidTopLevel = validTopLevel.some(cmd => line.startsWith(cmd)); + if (!isValidTopLevel) { + warnings.push({ line: lineNum, message: `Неизвестная команда верхнего уровня: ${line.substring(0, 30)}` }); + } + } + } + + // Проверка сбалансированности фигурных скобок + if (braceBalance !== 0) { + errors.push({ line: 0, message: `Несбалансированные фигурные скобки (баланс: ${braceBalance})` }); + } + + // Проверка незакрытых блоков + if (inBlock) { + errors.push({ line: currentBlockLine, message: `Блок "${blockName}" не закрыт` }); + } + + return { + valid: errors.length === 0, + errors, + warnings + }; +} + +/** + * Проверка корректности community value + * @param {string} community - Community value (например, 65000:100) + * @returns {boolean} + */ +function isValidCommunity(community) { + if (!community || typeof community !== 'string') return false; + // Формат: N:N где N - число от 0 до 65535 + const match = community.match(/^(\d+):(\d+)$/); + if (!match) return false; + const first = Number(match[1]); + const second = Number(match[2]); + return first >= 0 && first <= 65535 && second >= 0 && second <= 65535; +} + +/** + * Проверка корректности имени gateway + * @param {string} gateway - Gateway имя + * @returns {boolean} + */ +function isValidGateway(gateway) { + if (!gateway || typeof gateway !== 'string') return false; + // Допустимы: буквы, цифры, дефис, подчеркивание + // Длина: 1-64 символа + return /^[a-zA-Z0-9_-]{1,64}$/.test(gateway); +} + +module.exports = { + validateMikrotikConfig, + isValidCommunity, + isValidGateway, +}; + diff --git a/backend/lib/validators.js b/backend/lib/validators.js new file mode 100644 index 0000000..47cc0c0 --- /dev/null +++ b/backend/lib/validators.js @@ -0,0 +1,336 @@ +/** + * Модуль валидации входных данных + * Содержит валидаторы для IP, CIDR, доменов, ASN, Community + */ + +/** + * Валидация IPv4 адреса + * @param {string} ip - IP адрес для валидации + * @returns {boolean} + */ +function isValidIPv4(ip) { + if (!ip || typeof ip !== 'string') return false; + const octets = ip.trim().split('.'); + if (octets.length !== 4) return false; + return octets.every(octet => { + if (!/^\d{1,3}$/.test(octet)) return false; + const num = Number(octet); + return num >= 0 && num <= 255; + }); +} + +/** + * Валидация IPv6 адреса (базовая) + * @param {string} ip - IPv6 адрес + * @returns {boolean} + */ +function isValidIPv6(ip) { + if (!ip || typeof ip !== 'string') return false; + const segments = ip.trim().split(':'); + if (segments.length < 3 || segments.length > 8) return false; + return segments.every(seg => { + if (seg === '') return true; // :: notation + return /^[0-9a-fA-F]{1,4}$/.test(seg); + }); +} + +/** + * Валидация CIDR блока (IPv4) + * @param {string} cidr - CIDR блок (например, 192.168.1.0/24) + * @returns {boolean} + */ +function isValidCIDRv4(cidr) { + if (!cidr || typeof cidr !== 'string') return false; + const parts = cidr.trim().split('/'); + if (parts.length !== 2) return false; + + const [ip, mask] = parts; + if (!isValidIPv4(ip)) return false; + + if (!/^\d{1,2}$/.test(mask)) return false; + const maskNum = Number(mask); + return maskNum >= 0 && maskNum <= 32; +} + +/** + * Валидация CIDR блока (IPv6) + * @param {string} cidr - CIDR блок IPv6 + * @returns {boolean} + */ +function isValidCIDRv6(cidr) { + if (!cidr || typeof cidr !== 'string') return false; + const parts = cidr.trim().split('/'); + if (parts.length !== 2) return false; + + const [ip, mask] = parts; + if (!isValidIPv6(ip)) return false; + + if (!/^\d{1,3}$/.test(mask)) return false; + const maskNum = Number(mask); + return maskNum >= 0 && maskNum <= 128; +} + +/** + * Валидация доменного имени (FQDN) + * Поддержка IDN (интернационализированных доменов) + * @param {string} domain - доменное имя + * @returns {boolean} + */ +function isValidDomain(domain) { + if (!domain || typeof domain !== 'string') return false; + const d = domain.trim().toLowerCase(); + + // Проверка на максимальную длину + if (d.length > 253) return false; + + // Проверка на IP адрес (не должен быть IP) + if (isValidIPv4(d) || isValidIPv6(d)) return false; + + // Базовая проверка структуры домена + // Разрешаем буквы, цифры, дефис, точку, и IDN символы + const domainRegex = /^([a-z0-9\u00a1-\uffff]([a-z0-9\u00a1-\uffff-]{0,61}[a-z0-9\u00a1-\uffff])?\.)+[a-z\u00a1-\uffff]{2,}$/i; + return domainRegex.test(d); +} + +/** + * Валидация wildcard домена (*.example.com) + * @param {string} domain - wildcard домен + * @returns {boolean} + */ +function isValidWildcardDomain(domain) { + if (!domain || typeof domain !== 'string') return false; + const d = domain.trim().toLowerCase(); + + // Проверка на wildcard в начале + if (d.startsWith('*.')) { + const baseDomain = d.substring(2); + return isValidDomain(baseDomain); + } + + return isValidDomain(d); +} + +/** + * Валидация номера ASN + * @param {string|number} asn - номер ASN + * @returns {boolean} + */ +function isValidASN(asn) { + if (!asn) return false; + const asnStr = String(asn).trim(); + + // Формат: AS12345 или просто 12345 + const asnRegex = /^(AS)?(\d{1,10})$/i; + const match = asnStr.match(asnRegex); + if (!match) return false; + + const num = Number(match[2]); + // ASN диапазон: 0-4294967295 (32-bit) + return num >= 0 && num <= 4294967295; +} + +/** + * Валидация BGP Community + * Поддержка форматов: 65000:100, 65000 + * @param {string} community - BGP community + * @returns {boolean} + */ +function isValidCommunity(community) { + if (!community || typeof community !== 'string') return false; + const c = community.trim(); + + // Формат: N:N или просто N + const communityRegex = /^(\d{1,10})(:(\d{1,10}))?$/; + const match = c.match(communityRegex); + if (!match) return false; + + const first = Number(match[1]); + if (first < 0 || first > 4294967295) return false; + + if (match[3]) { + const second = Number(match[3]); + if (second < 0 || second > 65535) return false; + } + + return true; +} + +/** + * Валидация gateway имени (для MikroTik) + * @param {string} gateway - имя gateway + * @returns {boolean} + */ +function isValidGateway(gateway) { + if (!gateway || typeof gateway !== 'string') return false; + const g = gateway.trim(); + + // MikroTik gateway: буквы, цифры, дефис, подчеркивание + // Длина: 1-64 символа + if (g.length < 1 || g.length > 64) return false; + + const gatewayRegex = /^[a-zA-Z0-9_-]+$/; + return gatewayRegex.test(g); +} + +/** + * Санитизация строки от опасных символов + * @param {string} str - входная строка + * @returns {string} + */ +function sanitizeString(str) { + if (!str || typeof str !== 'string') return ''; + + // Удаляем управляющие символы + let cleaned = str.replace(/[\x00-\x1F\x7F]/g, ''); + + // Экранируем HTML специальные символы + cleaned = cleaned + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + + return cleaned.trim(); +} + +/** + * Проверка на SQL injection паттерны + * @param {string} str - строка для проверки + * @returns {boolean} true если безопасно + */ +function isSafeSQLString(str) { + if (!str || typeof str !== 'string') return true; + + const dangerousPatterns = [ + /(\bSELECT\b|\bINSERT\b|\bUPDATE\b|\bDELETE\b|\bDROP\b|\bCREATE\b)/i, + /(\bUNION\b|\bJOIN\b)/i, + /(--|;|\/\*|\*\/)/, + /(\bOR\b|\bAND\b)\s+\d+\s*=\s*\d+/i, + ]; + + return !dangerousPatterns.some(pattern => pattern.test(str)); +} + +/** + * Проверка на XSS паттерны + * @param {string} str - строка для проверки + * @returns {boolean} true если безопасно + */ +function isSafeXSSString(str) { + if (!str || typeof str !== 'string') return true; + + const dangerousPatterns = [ + /