Files
DLE/frontend/src/services/webSshService.js

724 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/VC-HB3-Accelerator
*/
/**
* Сервис для управления WEB SSH туннелем
* Взаимодействует с локальным агентом на порту 3000
*/
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) {
return `# Rate limiting для защиты от DDoS
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_limit_per_ip:10m rate=50r/s;
# Блокировка известных сканеров и вредоносных ботов
map $http_user_agent $bad_bot {
default 0;
~*bot 1;
~*crawler 1;
~*spider 1;
~*scanner 1;
~*sqlmap 1;
~*nikto 1;
~*dirb 1;
~*gobuster 1;
~*wfuzz 1;
~*burp 1;
~*zap 1;
~*nessus 1;
~*openvas 1;
}
server {
listen 80;
server_name ${domain};
# Блокировка подозрительных ботов
if ($bad_bot = 1) {
return 403;
}
# Защита от path traversal
if ($request_uri ~* "(\\\\.\\\\.|\\\\.\\\\./|\\\\.\\\\.\\\\.\\\\|\\\\.\\\\.%2f|\\\\.\\\\.%5c)") {
return 404;
}
# Защита от опасных расширений
location ~* \\\\.(zip|rar|7z|tar|gz|bz2|xz|sql|sqlite|db|bak|backup|old|csv|php|asp|aspx|jsp|cgi|pl|py|sh|bash|exe|bat|cmd|com|pif|scr|vbs|vbe|jar|war|ear|dll|so|dylib|bin|sys|ini|log|tmp|temp|swp|swo|~)$ {
return 404;
}
# Защита от доступа к чувствительным файлам
location ~* /(\\\\.htaccess|\\\\.htpasswd|\\\\.env|\\\\.git|\\\\.svn|\\\\.DS_Store|Thumbs\\\\.db|web\\\\.config|robots\\\\.txt|sitemap\\\\.xml)$ {
deny all;
return 404;
}
# Основной location для фронтенда
location / {
# Rate limiting для основных страниц
limit_req zone=req_limit_per_ip burst=20 nodelay;
proxy_pass http://localhost:${serverPort};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Базовые заголовки безопасности
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:;" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
# API проксирование к backend через туннель
location /api/ {
# Rate limiting для API (более строгое)
limit_req zone=api_limit_per_ip burst=100 nodelay;
proxy_pass http://localhost:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Заголовки безопасности для API
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
# WebSocket поддержка
location /ws {
proxy_pass http://localhost:8000/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Скрытие информации о сервере
server_tokens off;
}`;
}
class WebSshService {
constructor() {
this.isAgentRunning = false;
this.connectionStatus = {
connected: false,
domain: null,
vdsConfigured: false
};
}
/**
* Проверка доступности локального агента
*/
async checkAgentStatus() {
try {
const response = await fetch(`${LOCAL_AGENT_URL}/health`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
this.isAgentRunning = true;
return { running: true };
}
this.isAgentRunning = false;
return { running: false };
} catch (error) {
// console.error('Агент не доступен:', error);
this.isAgentRunning = false;
return { running: false, error: error.message };
}
}
/**
* Автоматическая установка и запуск агента
* В новой архитектуре агент всегда запускается в Docker (dapp-webssh-agent),
* поэтому здесь просто проверяем его доступность.
*/
async installAndStartAgent() {
const status = await this.checkAgentStatus();
if (status.running) {
return { success: true, message: 'Агент уже запущен' };
}
return {
success: false,
message: 'WebSSH Agent не запущен. Убедитесь, что контейнер dapp-webssh-agent работает (docker compose up -d webssh-agent).'
};
}
/**
* Получение IP адреса из DNS записей домена через backend API
*/
async getDomainIP(domain) {
try {
console.log(`Получение IP адреса для домена ${domain}...`);
// Используем backend API для проверки DNS
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) {
console.log(`DNS запись найдена: ${domain}${data.ip}`);
return { success: true, ip: data.ip };
} else {
console.log(`DNS запись для домена ${domain} не найдена: ${data.message}`);
return { success: false, error: data.message };
}
} catch (error) {
console.warn(`Не удалось получить IP из DNS: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Настройка существующей VDS для туннелей
*/
async setupVDS(config) {
try {
// Получаем IP адрес из DNS записей домена
if (config.domain) {
const dnsResult = await this.getDomainIP(config.domain);
if (!dnsResult.success) {
return {
success: false,
message: `Домен ${config.domain} не настроен или недоступен: ${dnsResult.error}`
};
}
// Добавляем полученный IP в конфигурацию
config.vdsIp = dnsResult.ip;
}
// Проверяем, что агент запущен (в Docker)
const agentStatus = await this.checkAgentStatus();
if (!agentStatus.running) {
const installResult = await this.installAndStartAgent();
if (!installResult.success) {
return installResult;
}
}
// API ключ больше не нужен - агент защищен сетевым доступом
// Отправляем конфигурацию VDS агенту
const response = await fetch(`${LOCAL_AGENT_URL}/vds/setup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
vdsIp: config.vdsIp,
domain: config.domain,
email: config.email,
ubuntuUser: config.ubuntuUser,
dockerUser: config.dockerUser,
sshUser: 'root', // SSH пользователь для настройки ключей (root)
sshHost: config.sshHost, // SSH хост для подключения
sshPort: config.sshPort, // SSH порт для подключения
sshConnectUser: config.sshUser, // SSH пользователь для подключения
sshConnectPassword: config.sshPassword // SSH пароль для подключения
})
});
if (response.ok) {
const result = await response.json();
if (result.success) {
this.connectionStatus = {
connected: true,
domain: config.domain,
vdsIp: config.vdsIp
};
}
return result;
} else {
const error = await response.json();
return {
success: false,
message: error.message || 'Ошибка при настройке VDS'
};
}
} catch (error) {
// console.error('Ошибка при настройке VDS:', error);
return {
success: false,
message: `Ошибка подключения к агенту: ${error.message}`
};
}
}
/**
* Настройка почтового сервера
*/
async setupMailServer(ssh, config, domain) {
const mailDomain = config.mailDomain || `mail.${domain}`;
const adminEmail = config.adminEmail || config.email;
const adminPassword = config.adminPassword || 'Admin123!';
// Настройка Postfix
const postfixConfig = `
# Основные настройки
myhostname = ${mailDomain}
mydomain = ${domain}
myorigin = $mydomain
inet_interfaces = all
inet_protocols = ipv4
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
home_mailbox = Maildir/
# SSL настройки
smtpd_use_tls = yes
smtpd_tls_cert_file = /etc/letsencrypt/live/${domain}/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/${domain}/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_auth_only = no
smtp_tls_security_level = may
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
tls_random_source = dev:/dev/urandom
# Аутентификация
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination
`;
await ssh.execCommand(`echo '${postfixConfig}' > /etc/postfix/main.cf`);
// Настройка Dovecot
const dovecotConfig = `
# Основные настройки
protocols = imap pop3 lmtp
mail_location = maildir:~/Maildir
namespace inbox {
inbox = yes
}
passdb {
driver = pam
}
userdb {
driver = passwd
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
}
ssl = required
ssl_cert = </etc/letsencrypt/live/${domain}/fullchain.pem
ssl_key = </etc/letsencrypt/live/${domain}/privkey.pem
`;
await ssh.execCommand(`echo '${dovecotConfig}' > /etc/dovecot/dovecot.conf`);
// Создание пользователя для почты
await ssh.execCommand(`useradd -m -s /bin/bash ${adminEmail.split('@')[0]}`);
await ssh.execCommand(`echo '${adminEmail.split('@')[0]}:${adminPassword}' | chpasswd`);
// Настройка Roundcube если включен
if (config.enableWebmail) {
const roundcubeConfig = `
server {
listen 80;
server_name ${mailDomain};
location / {
proxy_pass http://localhost/roundcube/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
`;
await ssh.execCommand(`echo '${roundcubeConfig}' > /etc/nginx/sites-available/${mailDomain}`);
await ssh.execCommand(`ln -sf /etc/nginx/sites-available/${mailDomain} /etc/nginx/sites-enabled/`);
}
// Получение SSL для почтового домена
await ssh.execCommand(`certbot --nginx -d ${mailDomain} --non-interactive --agree-tos --email ${adminEmail}`);
// Запуск сервисов
await ssh.execCommand('systemctl enable postfix dovecot');
await ssh.execCommand('systemctl restart postfix dovecot nginx');
// Создание DNS записей
const dnsRecords = `
# Добавьте эти DNS записи в ваш домен:
# MX запись: ${domain} -> ${mailDomain} (приоритет 10)
# A запись: ${mailDomain} -> IP вашего сервера
# SPF запись: v=spf1 mx a ip4:IP_СЕРВЕРА ~all
# DKIM запись: (будет создан автоматически)
`;
console.log('DNS записи для настройки:', dnsRecords);
}
/**
* Получение кода агента для установки
*/
getAgentCode() {
return `
const express = require('express');
const cors = require('cors');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const { NodeSSH } = require('node-ssh');
const app = express();
const PORT = 12345;
// Middleware
app.use(cors());
app.use(express.json());
// Состояние туннеля
let tunnelState = {
connected: false,
domain: null,
tunnelId: null,
sshProcess: null
};
// Здоровье агента
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Создание туннеля
app.post('/tunnel/create', async (req, res) => {
try {
const { domain, email, sshHost, sshUser, sshKey, localPort, serverPort, sshPort } = req.body;
// console.log('Создание туннеля для домена:', domain);
// Сохраняем SSH ключ во временный файл
const keyPath = path.join(__dirname, 'temp_ssh_key');
fs.writeFileSync(keyPath, sshKey, { mode: 0o600 });
// Подключаемся к серверу и настраиваем NGINX
const ssh = new NodeSSH();
await ssh.connect({
host: sshHost,
username: sshUser,
privateKey: sshKey,
port: sshPort
});
// Установка NGINX, certbot и почтовых сервисов
const installPackages = 'apt-get update && apt-get install -y nginx certbot python3-certbot-nginx';
const mailPackages = config.enableMail ? 'postfix dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-managesieved dovecot-sieve dovecot-mysql mysql-server roundcube roundcube-mysql' : '';
await ssh.execCommand(\`\${installPackages} \${mailPackages}\`);
// Создание конфигурации NGINX с полной защитой
const nginxConfig = \`# Rate limiting для защиты от DDoS
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_limit_per_ip:10m rate=50r/s;
# Блокировка известных сканеров и вредоносных ботов
map $http_user_agent $bad_bot {
default 0;
~*bot 1;
~*crawler 1;
~*spider 1;
~*scanner 1;
~*sqlmap 1;
~*nikto 1;
~*dirb 1;
~*gobuster 1;
~*wfuzz 1;
~*burp 1;
~*zap 1;
~*nessus 1;
~*openvas 1;
}
server {
listen 80;
server_name \${domain};
# Блокировка подозрительных ботов
if ($bad_bot = 1) {
return 403;
}
# Защита от path traversal
if ($request_uri ~* "(\\\\.\\\\.|\\\\.\\\\./|\\\\.\\\\.\\\\.\\\\|\\\\.\\\\.%2f|\\\\.\\\\.%5c)") {
return 404;
}
# Защита от опасных расширений
location ~* \\\\.(zip|rar|7z|tar|gz|bz2|xz|sql|sqlite|db|bak|backup|old|csv|php|asp|aspx|jsp|cgi|pl|py|sh|bash|exe|bat|cmd|com|pif|scr|vbs|vbe|jar|war|ear|dll|so|dylib|bin|sys|ini|log|tmp|temp|swp|swo|~)$ {
return 404;
}
# Защита от доступа к чувствительным файлам
location ~* /(\\\\.htaccess|\\\\.htpasswd|\\\\.env|\\\\.git|\\\\.svn|\\\\.DS_Store|Thumbs\\\\.db|web\\\\.config|robots\\\\.txt|sitemap\\\\.xml)$ {
deny all;
return 404;
}
# Основной location для фронтенда
location / {
# Rate limiting для основных страниц
limit_req zone=req_limit_per_ip burst=20 nodelay;
proxy_pass http://localhost:\${serverPort};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Базовые заголовки безопасности
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:;" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
# API проксирование к backend через туннель
location /api/ {
# Rate limiting для API (более строгое)
limit_req zone=api_limit_per_ip burst=100 nodelay;
proxy_pass http://localhost:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Заголовки безопасности для API
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
# WebSocket поддержка
location /ws {
proxy_pass http://localhost:8000/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Скрытие информации о сервере
server_tokens off;
}\`;
await ssh.execCommand(\`echo '\${nginxConfig}' > /etc/nginx/sites-available/\${domain}\`);
await ssh.execCommand(\`ln -sf /etc/nginx/sites-available/\${domain} /etc/nginx/sites-enabled/\`);
await ssh.execCommand('nginx -t && systemctl reload nginx');
// Получение SSL сертификата
await ssh.execCommand(\`certbot --nginx -d \${domain} --non-interactive --agree-tos --email \${email}\`);
// Настройка почты если включена
if (config.enableMail) {
await setupMailServer(ssh, config, domain);
}
ssh.dispose();
// Создание SSH туннелей для frontend и backend
const tunnelId = Date.now().toString();
// SSH туннель для frontend (порт 9000)
const frontendSshArgs = [
'-i', keyPath,
'-p', sshPort.toString(),
'-R', \`\${serverPort}:localhost:\${localPort}\`,
'-N',
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'ServerAliveInterval=60',
'-o', 'ServerAliveCountMax=3',
\`\${sshUser}@\${sshHost}\`
];
// SSH туннель для backend (порт 8000)
const backendSshArgs = [
'-i', keyPath,
'-p', sshPort.toString(),
'-R', '8000:localhost:8000',
'-N',
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'ServerAliveInterval=60',
'-o', 'ServerAliveCountMax=3',
\`\${sshUser}@\${sshHost}\`
];
// Запускаем оба SSH туннеля
const frontendSshProcess = spawn('ssh', frontendSshArgs);
const backendSshProcess = spawn('ssh', backendSshArgs);
frontendSshProcess.on('error', (error) => {
// console.error('Frontend SSH процесс ошибка:', error);
});
backendSshProcess.on('error', (error) => {
// console.error('Backend SSH процесс ошибка:', error);
});
frontendSshProcess.on('close', (code) => {
// console.log('Frontend SSH процесс завершен с кодом:', code);
tunnelState.connected = false;
});
backendSshProcess.on('close', (code) => {
// console.log('Backend SSH процесс завершен с кодом:', code);
tunnelState.connected = false;
});
// Обновляем состояние
tunnelState = {
connected: true,
domain,
tunnelId,
frontendSshProcess,
backendSshProcess
};
res.json({
success: true,
message: 'Туннель успешно создан',
tunnelId,
domain
});
} catch (error) {
// console.error('Ошибка создания туннеля:', error);
res.status(500).json({
success: false,
message: error.message
});
}
});
// Отключение туннелей
app.post('/tunnel/disconnect', (req, res) => {
try {
if (tunnelState.frontendSshProcess) {
tunnelState.frontendSshProcess.kill();
}
if (tunnelState.backendSshProcess) {
tunnelState.backendSshProcess.kill();
}
tunnelState = {
connected: false,
domain: null,
tunnelId: null,
frontendSshProcess: null,
backendSshProcess: null
};
res.json({
success: true,
message: 'Туннель отключен'
});
} catch (error) {
// console.error('Ошибка отключения туннеля:', error);
res.status(500).json({
success: false,
message: error.message
});
}
});
// Статус туннеля
app.get('/tunnel/status', (req, res) => {
res.json({
connected: tunnelState.connected,
domain: tunnelState.domain,
tunnelId: tunnelState.tunnelId
});
});
// Запуск сервера
app.listen(PORT, 'localhost', () => {
// console.log(\`WebSSH Agent запущен на порту \${PORT}\`);
});
`;
}
}
// Создаем композабл для использования в компонентах
export function useWebSshService() {
const service = new WebSshService();
return {
checkAgentStatus: () => service.checkAgentStatus(),
installAndStartAgent: () => service.installAndStartAgent(),
setupVDS: (config) => service.setupVDS(config)
};
}
export default WebSshService;