From 4ab155c4066417ead7ae4e2860ec019c21aeb388 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 13 Nov 2025 11:30:55 +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 --- frontend/docker-entrypoint.sh | 12 +++-- frontend/nginx-local.conf | 6 +++ .../src/services/systemMessagesService.js | 1 + webssh-agent/agent.js | 50 +++++++++++++++---- webssh-agent/docker-compose.prod.yml | 2 +- 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/frontend/docker-entrypoint.sh b/frontend/docker-entrypoint.sh index d73dc5c..ff37686 100644 --- a/frontend/docker-entrypoint.sh +++ b/frontend/docker-entrypoint.sh @@ -8,13 +8,19 @@ echo "🔧 Настройка nginx с параметрами:" echo " DOMAIN: $DOMAIN" echo " BACKEND_CONTAINER: $BACKEND_CONTAINER" -# Выбор конфигурации в зависимости от домена +# Выбор конфигурации +SSL_CERT_PATH="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" +SSL_KEY_PATH="/etc/letsencrypt/live/${DOMAIN}/privkey.pem" + if echo "$DOMAIN" | grep -qE '^localhost(:[0-9]+)?$|^production\.local$'; then echo " Режим: ЛОКАЛЬНАЯ РАЗРАБОТКА (без SSL)" TEMPLATE_FILE="/etc/nginx/nginx-local.conf.template" -else - echo " Режим: ПРОДАКШН (с SSL)" +elif [ -f "$SSL_CERT_PATH" ] && [ -f "$SSL_KEY_PATH" ]; then + echo " Режим: ПРОДАКШН (SSL сертификаты найдены)" TEMPLATE_FILE="/etc/nginx/nginx-ssl.conf.template" +else + echo " Режим: ПРОДАКШН (ожидаем выпуск SSL, работаем по HTTP)" + TEMPLATE_FILE="/etc/nginx/nginx-local.conf.template" fi # Обработка переменных окружения для nginx конфигурации diff --git a/frontend/nginx-local.conf b/frontend/nginx-local.conf index e94a609..eb1b82e 100644 --- a/frontend/nginx-local.conf +++ b/frontend/nginx-local.conf @@ -38,6 +38,12 @@ http { add_header X-XSS-Protection "1; mode=block" always; } + # Certbot webroot для автоматического получения SSL сертификатов + location /.well-known/acme-challenge/ { + root /var/www/certbot; + try_files $uri $uri/ =404; + } + # Статические файлы location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; diff --git a/frontend/src/services/systemMessagesService.js b/frontend/src/services/systemMessagesService.js index 5830f92..92b0af4 100644 --- a/frontend/src/services/systemMessagesService.js +++ b/frontend/src/services/systemMessagesService.js @@ -53,3 +53,4 @@ export default { } }; + diff --git a/webssh-agent/agent.js b/webssh-agent/agent.js index e3d7e7c..9810e4a 100644 --- a/webssh-agent/agent.js +++ b/webssh-agent/agent.js @@ -216,21 +216,21 @@ app.post('/vds/transfer-encryption-key', logRequest, async (req, res) => { sshConnectPassword }; - // 1. Проверяем, что директория для ключа существует на VDS - log.info('🔍 Проверка директории для ключа шифрования...'); - const dirCheckResult = await execSshCommand(`ls -la /home/${dockerUser}/dapp/ssl/keys/`, options); - - if (dirCheckResult.code !== 0) { - log.error('❌ Директория для ключа шифрования не найдена на VDS'); + // 1. Убеждаемся, что директория для ключа существует на VDS + log.info('🔍 Подготовка директории для ключа шифрования на VDS...'); + const ensureDirResult = await execSshCommand(`mkdir -p /home/${dockerUser}/dapp/ssl/keys`, options); + if (ensureDirResult.code !== 0) { + log.error('❌ Не удалось подготовить директорию для ключа шифрования на VDS'); return res.status(500).json({ success: false, - message: 'Директория для ключа шифрования не найдена на VDS. Сначала выполните настройку VDS.' + message: 'Не удалось подготовить директорию для ключа шифрования на VDS' }); } // 2. Читаем ключ шифрования с локальной машины log.info('📖 Чтение ключа шифрования с локальной машины...'); - const encryptionKeyPath = '/home/alex/Digital_Legal_Entity(DLE)/ssl/keys/full_db_encryption.key'; + const encryptionKeyPath = process.env.ENCRYPTION_KEY_PATH + || path.resolve(__dirname, '..', 'ssl', 'keys', 'full_db_encryption.key'); try { const encryptionKeyContent = await fs.readFile(encryptionKeyPath, 'utf8'); @@ -442,12 +442,10 @@ findtime = 3600 log.info('Nginx конфигурация встроена в Docker образ frontend-nginx'); log.info('Конфигурация будет применена автоматически при запуске контейнера'); - // Проверяем наличие необходимых переменных окружения для nginx if (!domain || !email) { log.error('Критическая ошибка: отсутствуют обязательные переменные DOMAIN или EMAIL для nginx'); throw new Error('Необходимы переменные DOMAIN и EMAIL для настройки nginx'); } - log.success(`Nginx будет настроен для домена: ${domain} с email: ${email}`); // 14. 🆕 Создание полного .env файла со всеми переменными окружения @@ -502,6 +500,32 @@ WS_BACKEND_CONTAINER=dapp-backend`; // 16.0. 🆕 Получение реального SSL сертификата через Let's Encrypt (опционально) log.info('Получение реального SSL сертификата через Let\'s Encrypt...'); + // Убеждаемся, что challenge доступен по HTTP + log.info('Проверяем доступность HTTP challenge для Let\'s Encrypt...'); + await execSshCommand('mkdir -p /var/www/certbot/.well-known/acme-challenge', options); + const challengeToken = `agent-challenge-${Date.now()}`; + await execSshCommand(`echo 'challenge-ok' > /var/www/certbot/.well-known/acme-challenge/${challengeToken}`, options); + let tempHttpContainerStarted = false; + let challengeCheck = await execSshCommand(`curl -fsS http://${domain}/.well-known/acme-challenge/${challengeToken}`, options); + + if (challengeCheck.code !== 0) { + log.warn('HTTP challenge недоступен. Запускаю временный nginx на 80 порту...'); + await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml stop frontend-nginx || true`, options); + await execSshCommand('docker rm -f dle-certbot-http 2>/dev/null || true', options); + const tempNginxStart = await execSshCommand('docker run -d --name dle-certbot-http -p 80:80 -v /var/www/certbot:/usr/share/nginx/html:ro nginx:alpine', options); + if (tempNginxStart.code === 0) { + tempHttpContainerStarted = true; + await execSshCommand('sleep 3', options); + challengeCheck = await execSshCommand(`curl -fsS http://${domain}/.well-known/acme-challenge/${challengeToken}`, options); + } else { + log.warn('Не удалось запустить временный nginx для challenge: ' + tempNginxStart.stderr); + } + } else { + log.success('HTTP challenge доступен через frontend-nginx'); + } + + await execSshCommand(`rm -f /var/www/certbot/.well-known/acme-challenge/${challengeToken}`, options); + // Получаем SSL сертификат через certbot const certbotResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot`, options); @@ -512,6 +536,12 @@ WS_BACKEND_CONTAINER=dapp-backend`; log.info('Будет использоваться временный самоподписанный сертификат'); } + if (tempHttpContainerStarted) { + log.info('Останавливаю временный HTTP контейнер для challenge'); + await execSshCommand('docker rm -f dle-certbot-http || true', options); + await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml up -d frontend-nginx`, options); + } + // Настройка автоматического обновления SSL сертификатов log.info('Настройка автоматического обновления SSL сертификатов...'); const renewScript = `#!/bin/bash diff --git a/webssh-agent/docker-compose.prod.yml b/webssh-agent/docker-compose.prod.yml index b306114..9803b80 100644 --- a/webssh-agent/docker-compose.prod.yml +++ b/webssh-agent/docker-compose.prod.yml @@ -224,7 +224,7 @@ services: - WS_BACKEND_CONTAINER=dapp-backend volumes: # SSL сертификаты Let's Encrypt (автоматически обновляются) - - /etc/letsencrypt:/etc/ssl/certs:ro + - /etc/letsencrypt:/etc/letsencrypt:ro # Webroot для certbot - /var/www/certbot:/var/www/certbot depends_on: