From 348dfa5f62f2942824834bfca81dd531e8b642e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 6 Nov 2025 17:25:36 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/ssh.js | 16 ++- backend/utils/logger.js | 174 ++++++++++++++++++++++++- backend/wsHub.js | 51 ++++---- docker-compose.yml | 1 + frontend/src/components/WebSshForm.vue | 28 +++- frontend/src/services/webSshService.js | 21 ++- 6 files changed, 258 insertions(+), 33 deletions(-) diff --git a/backend/routes/ssh.js b/backend/routes/ssh.js index 9d12db4..b5a32f3 100644 --- a/backend/routes/ssh.js +++ b/backend/routes/ssh.js @@ -13,6 +13,7 @@ const express = require('express'); const router = express.Router(); const { promisify } = require('util'); +const { domainToASCII } = require('url'); const dns = require('dns'); const resolve4 = promisify(dns.resolve4); @@ -30,10 +31,21 @@ router.get('/dns-check/:domain', async (req, res) => { }); } - console.log(`Checking DNS for domain: ${domain}`); + const normalizedDomain = domain.trim().toLowerCase(); + const asciiDomain = domainToASCII(normalizedDomain); + + if (!asciiDomain) { + return res.status(400).json({ + success: false, + domain, + message: `Некорректное доменное имя: ${domain}` + }); + } + + console.log(`Checking DNS for domain: ${domain} (ASCII: ${asciiDomain})`); // Используем встроенный DNS resolver Node.js - const addresses = await resolve4(domain); + const addresses = await resolve4(asciiDomain); if (addresses && addresses.length > 0) { const ip = addresses[0]; diff --git a/backend/utils/logger.js b/backend/utils/logger.js index 26841ec..c4a1de6 100644 --- a/backend/utils/logger.js +++ b/backend/utils/logger.js @@ -1,21 +1,189 @@ const winston = require('winston'); const path = require('path'); +const SENSITIVE_KEY_REGEX = /(address|wallet|signature|provider_id|private|secret|rpc|api(?:key)?|auth|token_address|contract)/i; +const IPV4_REGEX = /\b(\d{1,3}\.){3}\d{1,3}\b/g; +const IPV6_REGEX = /([a-f0-9]{1,4}:){1,7}[a-f0-9]{1,4}/gi; +const ETH_ADDRESS_REGEX = /0x[a-fA-F0-9]{40}/g; +const ETH_TX_REGEX = /0x[a-fA-F0-9]{64}/g; +const EMAIL_REGEX = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi; + +const maskEthereumAddress = (value) => { + return value.replace(ETH_ADDRESS_REGEX, (match) => `${match.slice(0, 6)}...${match.slice(-4)}`); +}; + +const maskEthereumHash = (value) => { + return value.replace(ETH_TX_REGEX, (match) => `${match.slice(0, 10)}...${match.slice(-6)}`); +}; + +const maskIpAddresses = (value) => { + return value + .replace(IPV4_REGEX, '***.***.***.***') + .replace(IPV6_REGEX, '[REDACTED_IP]'); +}; + +const maskEmails = (value) => { + return value.replace(EMAIL_REGEX, (match) => { + const [local, domain] = match.split('@'); + if (!domain) { + return '[REDACTED_EMAIL]'; + } + const hiddenLocal = local.length <= 2 ? '**' : `${local.slice(0, 2)}***`; + return `${hiddenLocal}@${domain}`; + }); +}; + +const redactString = (value) => { + if (!value) { + return value; + } + let sanitized = value; + sanitized = maskEthereumAddress(sanitized); + sanitized = maskEthereumHash(sanitized); + sanitized = maskIpAddresses(sanitized); + sanitized = maskEmails(sanitized); + return sanitized; +}; + +const sanitizeValue = (value, keyPath = '', seen = new WeakSet()) => { + if (typeof value === 'string') { + if (SENSITIVE_KEY_REGEX.test(keyPath)) { + return '[REDACTED]'; + } + return redactString(value); + } + + if (typeof value === 'number') { + if (SENSITIVE_KEY_REGEX.test(keyPath)) { + return '[REDACTED]'; + } + return value; + } + + if (!value || typeof value !== 'object') { + return value; + } + + if (seen.has(value)) { + return '[REDACTED]'; + } + seen.add(value); + + if (value instanceof Error) { + const sanitizedError = {}; + Object.getOwnPropertyNames(value).forEach((prop) => { + sanitizedError[prop] = sanitizeValue(value[prop], `${keyPath}.${prop}`, seen); + }); + return sanitizedError; + } + + if (Array.isArray(value)) { + return value.map((item, index) => sanitizeValue(item, `${keyPath}[${index}]`, seen)); + } + + const sanitizedObject = {}; + Object.keys(value).forEach((key) => { + const nextPath = keyPath ? `${keyPath}.${key}` : key; + if (SENSITIVE_KEY_REGEX.test(key)) { + sanitizedObject[key] = '[REDACTED]'; + } else { + sanitizedObject[key] = sanitizeValue(value[key], nextPath, seen); + } + }); + return sanitizedObject; +}; + +const sanitizeInfo = (info) => { + const sanitizedInfo = { ...info }; + sanitizedInfo.message = sanitizeValue(info.message, 'message'); + const splat = Symbol.for('splat'); + if (info[splat]) { + sanitizedInfo[splat] = info[splat].map((entry, index) => sanitizeValue(entry, `splat[${index}]`)); + } + + Object.keys(info).forEach((key) => { + if (['level', 'message', 'timestamp'].includes(key)) { + return; + } + sanitizedInfo[key] = sanitizeValue(info[key], key); + }); + + return sanitizedInfo; +}; + +const sanitizeFormat = winston.format((info) => sanitizeInfo(info)); + +const jsonFormat = winston.format.combine( + sanitizeFormat(), + winston.format.timestamp(), + winston.format.json() +); + +const consoleFormat = winston.format.combine( + sanitizeFormat(), + winston.format.timestamp(), + winston.format.colorize({ all: true }), + winston.format.printf((info) => { + const { timestamp, level, message, ...rest } = info; + const metaParts = []; + const splat = info[Symbol.for('splat')]; + if (splat && Array.isArray(splat)) { + splat.forEach((entry) => { + if (entry === undefined) { + return; + } + if (typeof entry === 'string') { + metaParts.push(entry); + } else { + metaParts.push(JSON.stringify(entry)); + } + }); + } + + Object.keys(rest) + .filter((key) => !['level', 'message', 'timestamp'].includes(key)) + .forEach((key) => { + if (key === Symbol.for('splat')) { + return; + } + const value = rest[key]; + if (value !== undefined) { + metaParts.push(`${key}=${JSON.stringify(value)}`); + } + }); + + const metaString = metaParts.length ? ` ${metaParts.join(' ')}` : ''; + return `${timestamp} ${level}: ${message}${metaString}`; + }) +); + const logger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', // Уровень по умолчанию 'info' для показа логов ботов - format: winston.format.combine(winston.format.timestamp(), winston.format.json()), + level: process.env.LOG_LEVEL || 'info', + format: jsonFormat, transports: [ new winston.transports.Console({ - format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + format: consoleFormat, }), new winston.transports.File({ filename: path.join(__dirname, '../logs/error.log'), level: 'error', + format: jsonFormat, }), new winston.transports.File({ filename: path.join(__dirname, '../logs/combined.log'), + format: jsonFormat, }), ], }); +const wrapConsoleMethod = (methodName) => { + const original = console[methodName].bind(console); + console[methodName] = (...args) => { + const sanitizedArgs = args.map((arg, index) => sanitizeValue(arg, `${methodName}[${index}]`)); + original(...sanitizedArgs); + }; +}; + +['log', 'info', 'warn', 'error', 'debug'].forEach(wrapConsoleMethod); + module.exports = logger; diff --git a/backend/wsHub.js b/backend/wsHub.js index b7b7326..95a3684 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -14,6 +14,7 @@ const WebSocket = require('ws'); const tokenBalanceService = require('./services/tokenBalanceService'); const deploymentTracker = require('./utils/deploymentTracker'); const deploymentWebSocketService = require('./services/deploymentWebSocketService'); +const logger = require('./utils/logger'); let wss = null; // Храним клиентов по userId для персонализированных уведомлений @@ -29,7 +30,7 @@ const TAGS_UPDATE_DEBOUNCE = 100; // 100ms function initWSS(server) { wss = new WebSocket.Server({ server, path: '/ws' }); - console.log('🔌 [WebSocket] Сервер инициализирован на пути /ws'); + logger.info('🔌 [WebSocket] Сервер инициализирован на пути /ws'); // Инициализируем deploymentWebSocketService с WebSocket сервером после создания wss deploymentWebSocketService.initialize(server, wss); @@ -40,15 +41,15 @@ function initWSS(server) { }); // Дополнительная инициализация deploymentWebSocketService после создания wss - console.log('[wsHub] Инициализируем deploymentWebSocketService с wss:', !!wss); + logger.debug('[wsHub] Инициализируем deploymentWebSocketService с wss:', !!wss); deploymentWebSocketService.setWebSocketServer(wss); - console.log('[wsHub] deploymentWebSocketService инициализирован'); + logger.debug('[wsHub] deploymentWebSocketService инициализирован'); wss.on('connection', (ws, req) => { - console.log('🔌 [WebSocket] Новое подключение'); - console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress); - console.log('🔌 [WebSocket] User-Agent:', req.headers['user-agent']); - console.log('🔌 [WebSocket] Origin:', req.headers.origin); + logger.debug('🔌 [WebSocket] Новое подключение'); + logger.debug('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress); + logger.debug('🔌 [WebSocket] User-Agent:', req.headers['user-agent']); + logger.debug('🔌 [WebSocket] Origin:', req.headers.origin); // Добавляем клиента в общий список if (!wsClients.has('anonymous')) { @@ -77,7 +78,7 @@ function initWSS(server) { if (data.type === 'ollama_ready') { // Уведомление о готовности Ollama - запускаем инициализацию ботов - console.log('🚀 [WebSocket] Получено уведомление о готовности Ollama!'); + logger.debug('🚀 [WebSocket] Получено уведомление о готовности Ollama!'); handleOllamaReady(); } @@ -105,11 +106,11 @@ function initWSS(server) { data.type === 'deployment_update') { // Эти сообщения обрабатываются в deploymentWebSocketService // Просто логируем для отладки - console.log(`[WebSocket] Получено сообщение деплоя: ${data.type}`); - console.log(`[WebSocket] Данные:`, JSON.stringify(data, null, 2)); + logger.debug(`[WebSocket] Получено сообщение деплоя: ${data.type}`); + logger.debug('[WebSocket] Данные:', JSON.stringify(data, null, 2)); } } catch (error) { - // console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error); + logger.debug('❌ [WebSocket] Ошибка парсинга сообщения:', error); } }); @@ -128,7 +129,7 @@ function initWSS(server) { }); ws.on('error', (error) => { - // console.error('❌ [WebSocket] Ошибка соединения:', error.message); + logger.debug('❌ [WebSocket] Ошибка соединения:', error.message); }); }); @@ -503,14 +504,14 @@ function broadcastTokenBalanceChanged(userId, tokenAddress, newBalance, network) function broadcastDeploymentUpdate(data) { if (!wss) return; - console.log(`📡 [WebSocket] broadcastDeploymentUpdate вызвана с данными:`, JSON.stringify(data, null, 2)); + logger.debug('📡 [WebSocket] broadcastDeploymentUpdate вызвана с данными:', JSON.stringify(data, null, 2)); const message = JSON.stringify({ type: 'deployment_update', data: data }); - console.log(`📡 [WebSocket] Отправляем сообщение:`, message); + logger.debug('📡 [WebSocket] Отправляем сообщение:', message); // Отправляем всем подключенным клиентам wss.clients.forEach(client => { @@ -518,12 +519,12 @@ function broadcastDeploymentUpdate(data) { try { client.send(message); } catch (error) { - console.error('[WebSocket] Ошибка при отправке deployment update:', error); + logger.error('[WebSocket] Ошибка при отправке deployment update:', error); } } }); - console.log(`📡 [WebSocket] Отправлено deployment update: deployment_update`); + logger.debug('📡 [WebSocket] Отправлено deployment update: deployment_update'); } // broadcastModulesUpdate удалена - используем deploymentWebSocketService.broadcastToDLE @@ -554,13 +555,13 @@ module.exports = { // Обработчик запроса балансов токенов async function handleTokenBalancesRequest(ws, address, userId) { try { - console.log(`[WebSocket] Запрос балансов для адреса: ${address}`); + logger.debug(`[WebSocket] Запрос балансов для адреса: ${address}`); // Получаем балансы через отдельный сервис без зависимостей от wsHub const balances = await tokenBalanceService.getUserTokenBalances(address); - console.log(`[WebSocket] Получены балансы для ${address}:`, balances); - console.log(`[WebSocket] Количество токенов:`, balances?.length || 0); + logger.debug(`[WebSocket] Получены балансы для ${address}:`, balances); + logger.debug('[WebSocket] Количество токенов:', balances?.length || 0); // Отправляем ответ клиенту const response = { @@ -572,10 +573,10 @@ async function handleTokenBalancesRequest(ws, address, userId) { } }; - console.log(`[WebSocket] Отправляем ответ:`, JSON.stringify(response, null, 2)); + logger.debug('[WebSocket] Отправляем ответ:', JSON.stringify(response, null, 2)); ws.send(JSON.stringify(response)); } catch (error) { - console.error('[WebSocket] Ошибка при получении балансов:', error); + logger.error('[WebSocket] Ошибка при получении балансов:', error); // Определяем тип ошибки для лучшей диагностики let errorType = 'Неизвестная ошибка'; @@ -602,7 +603,7 @@ async function handleTokenBalancesRequest(ws, address, userId) { } }; - console.log('[WebSocket] Отправляем ошибку клиенту:', JSON.stringify(errorResponse, null, 2)); + logger.debug('[WebSocket] Отправляем ошибку клиенту:', JSON.stringify(errorResponse, null, 2)); ws.send(JSON.stringify(errorResponse)); } } @@ -612,11 +613,11 @@ async function handleTokenBalancesRequest(ws, address, userId) { */ async function handleOllamaReady() { try { - console.log('✅ [WebSocket] Ollama готов к работе'); + logger.debug('✅ [WebSocket] Ollama готов к работе'); // Уведомляем всех подключенных клиентов о готовности системы broadcastSystemReady(); } catch (error) { - console.error('❌ [WebSocket] Ошибка обработки Ollama ready:', error); + logger.error('❌ [WebSocket] Ошибка обработки Ollama ready:', error); } } @@ -642,5 +643,5 @@ function broadcastSystemReady() { }); }); - console.log('📢 [WebSocket] Уведомление о готовности системы отправлено всем клиентам'); + logger.debug('📢 [WebSocket] Уведомление о готовности системы отправлено всем клиентам'); } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 69882d2..cb6b0a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -150,6 +150,7 @@ services: - OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest} # FRONTEND_URL настраивается в коде, не через env - VECTOR_SEARCH_URL=http://vector-search:8001 + - LOG_LEVEL=${LOG_LEVEL:-warn} # Factory адреса теперь хранятся в базе данных # Убираем порты для продакшна - доступ только через nginx # ports: diff --git a/frontend/src/components/WebSshForm.vue b/frontend/src/components/WebSshForm.vue index 02ad484..dc20bb1 100644 --- a/frontend/src/components/WebSshForm.vue +++ b/frontend/src/components/WebSshForm.vue @@ -146,6 +146,19 @@ import { useWebSshLogs } from '../composables/useWebSshLogs'; const webSshService = useWebSshService(); +const encodeDomainForRequest = (domain) => { + if (!domain) return null; + + try { + const normalized = domain.trim().toLowerCase(); + const url = new URL(`http://${normalized}`); + return url.hostname; + } catch (error) { + console.warn('[WebSSH] Некорректное доменное имя:', domain, error.message); + return null; + } +}; + // Используем композабл для real-time логов const { logs, @@ -196,8 +209,19 @@ const checkDomainDNS = async () => { try { domainStatus.value = { type: 'loading', message: 'Проверка DNS...' }; - - const response = await fetch(`http://localhost:8000/api/dns-check/${form.domain}`); + + const asciiDomain = encodeDomainForRequest(form.domain); + + if (!asciiDomain) { + domainStatus.value = { + type: 'error', + message: '❌ Некорректное доменное имя' + }; + addLog('error', `DNS ошибка: Некорректное доменное имя (${form.domain})`); + return; + } + + const response = await fetch(`/api/dns-check/${encodeURIComponent(asciiDomain)}`); const data = await response.json(); if (data.success) { diff --git a/frontend/src/services/webSshService.js b/frontend/src/services/webSshService.js index 25e191f..65ce290 100644 --- a/frontend/src/services/webSshService.js +++ b/frontend/src/services/webSshService.js @@ -16,6 +16,20 @@ */ const LOCAL_AGENT_URL = 'http://localhost:3000'; +const API_BASE_PATH = '/api'; + +const normalizeDomainToAscii = (domain) => { + if (!domain) return null; + + try { + const normalized = domain.trim().toLowerCase(); + const url = new URL(`http://${normalized}`); + return url.hostname; + } catch (error) { + console.warn('[WebSshService] Некорректное доменное имя:', domain, error.message); + return null; + } +}; // Функция для генерации nginx конфигурации function getNginxConfig(domain, serverPort) { @@ -274,7 +288,12 @@ EOF console.log(`Получение IP адреса для домена ${domain}...`); // Используем backend API для проверки DNS - const response = await fetch(`http://localhost:8000/api/dns-check/${domain}`); + const asciiDomain = normalizeDomainToAscii(domain); + if (!asciiDomain) { + return { success: false, error: 'Некорректное доменное имя' }; + } + + const response = await fetch(`${API_BASE_PATH}/dns-check/${encodeURIComponent(asciiDomain)}`); const data = await response.json(); if (data.success) {