ваше сообщение коммита

This commit is contained in:
2025-11-25 02:21:51 +03:00
parent da69f198e6
commit 90ffc445ee

View File

@@ -295,7 +295,7 @@ async function execDockerCommand(command) {
* Выполнить SSH команду на VDS * Выполнить SSH команду на VDS
*/ */
async function execSshCommandOnVds(command, settings) { async function execSshCommandOnVds(command, settings) {
const { sshHost, sshPort = 22, sshUser, sshPassword } = settings; const { sshHost, sshPort = 22, sshUser } = settings;
// Экранируем команду для SSH // Экранируем команду для SSH
// Экранируем двойные кавычки и знаки доллара для правильной передачи через SSH // Экранируем двойные кавычки и знаки доллара для правильной передачи через SSH
@@ -304,19 +304,12 @@ async function execSshCommandOnVds(command, settings) {
.replace(/\$/g, '\\$') // Экранируем знаки доллара .replace(/\$/g, '\\$') // Экранируем знаки доллара
.replace(/"/g, '\\"'); // Экранируем двойные кавычки .replace(/"/g, '\\"'); // Экранируем двойные кавычки
// Базовые опции SSH // Базовые опции SSH - используем только SSH ключи, пароли не поддерживаются
const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR`; const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o PasswordAuthentication=no`;
// Строим SSH команду // Строим SSH команду - всегда используем SSH ключи
let sshCommand; // SSH автоматически найдет ключ в ~/.ssh/id_rsa или ~/.ssh/id_ed25519
if (sshPassword && sshPassword.trim()) { const sshCommand = `ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
// Используем sshpass для подключения с паролем (если пароль указан)
sshCommand = `sshpass -p "${sshPassword.replace(/"/g, '\\"')}" ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
} else {
// Используем SSH ключи (по умолчанию из ~/.ssh/id_rsa или ~/.ssh/id_ed25519)
// SSH автоматически найдет ключ в ~/.ssh/
sshCommand = `ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
}
try { try {
const { stdout, stderr } = await execAsync(sshCommand); const { stdout, stderr } = await execAsync(sshCommand);
@@ -1096,12 +1089,59 @@ router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
// Пытаемся обновить сертификат через Docker certbot // Пытаемся обновить сертификат через Docker certbot
logger.info('[VDS] Обновление SSL сертификата...'); logger.info('[VDS] Обновление SSL сертификата...');
const renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot renew --force-renewal --non-interactive 2>&1 || certbot renew --force-renewal --non-interactive 2>&1`); // Сначала пробуем renew --force-renewal для обновления существующего сертификата
// Это не создает новый сертификат и не попадает под лимит Let's Encrypt
let renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot renew --force-renewal --non-interactive 2>&1 || certbot renew --force-renewal --non-interactive 2>&1`);
// Если renew не сработал (сертификат не найден или другая ошибка), создаем новый
if (renewResult.code !== 0 || renewResult.stdout.includes('No renewals were attempted') || renewResult.stdout.includes('No certs found')) {
logger.info('[VDS] Renew не сработал, создаем новый сертификат...');
// Удаляем только сертификаты с суффиксами, основной оставляем
const certListResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
if (certListResult.stdout) {
const lines = certListResult.stdout.split('\n');
const certNames = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('Certificate Name:')) {
const certName = lines[i].split('Certificate Name:')[1]?.trim();
// Удаляем только сертификаты с суффиксами (например, hb3-accelerator.com-0001, hb3-accelerator.com-0002)
if (certName && certName !== domain && certName.startsWith(domain + '-')) {
certNames.push(certName);
}
}
}
// Удаляем только сертификаты с суффиксами
for (const certName of certNames) {
logger.info(`[VDS] Удаление старого сертификата с суффиксом: ${certName}`);
await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`);
}
}
// Создаем новый сертификат только если его нет
const email = vdsSettings.email || 'admin@example.com';
renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1 || certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1`);
}
if (renewResult.code === 0) { if (renewResult.code === 0) {
// Перезапускаем nginx для применения нового сертификата // Перезапускаем nginx для применения нового сертификата
const reloadResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || systemctl reload nginx 2>&1`); const reloadResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || systemctl reload nginx 2>&1`);
// Очищаем старые сертификаты с суффиксами, чтобы они не накапливались
logger.info('[VDS] Очистка старых сертификатов с суффиксами...');
const certListAfter = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
if (certListAfter.stdout) {
const lines = certListAfter.stdout.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('Certificate Name:')) {
const certName = lines[i].split('Certificate Name:')[1]?.trim();
// Удаляем сертификаты с суффиксами (например, hb3-accelerator.com-0001, hb3-accelerator.com-0002)
if (certName && certName !== domain && certName.startsWith(domain + '-')) {
logger.info(`[VDS] Удаление старого сертификата с суффиксом: ${certName}`);
await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`);
}
}
}
}
logger.info('[VDS] SSL сертификат обновлен'); logger.info('[VDS] SSL сертификат обновлен');
res.json({ res.json({
success: true, success: true,
@@ -1143,26 +1183,97 @@ router.get('/ssl/status', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
// Проверяем срок действия сертификата // Проверяем срок действия сертификата
let certInfo = null; let certInfo = null;
if (domain) { if (domain && checkResult.stdout) {
const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`; // Парсим вывод certbot certificates для поиска сертификата по домену
const certCheckResult = await execDockerCommand(`openssl x509 -in ${certPath} -noout -dates -subject 2>&1 || echo "Certificate not found"`); // Ищем строку с "Domains:" содержащую наш домен, затем ищем "Certificate Path:"
const certLines = checkResult.stdout.split('\n');
let certPath = null;
let certName = null;
if (certCheckResult.code === 0 && !certCheckResult.stdout.includes('not found')) { for (let i = 0; i < certLines.length; i++) {
certInfo = { const line = certLines[i];
exists: true, // Ищем сертификат по домену
details: certCheckResult.stdout if (line.includes('Domains:') && line.includes(domain)) {
}; // Нашли сертификат для нашего домена, ищем путь в следующих строках
for (let j = i + 1; j < Math.min(i + 10, certLines.length); j++) {
if (certLines[j].includes('Certificate Path:')) {
certPath = certLines[j].split('Certificate Path:')[1]?.trim();
// Заменяем fullchain.pem на cert.pem для проверки
certPath = certPath.replace('/fullchain.pem', '/cert.pem');
break;
}
if (certLines[j].includes('Certificate Name:')) {
certName = certLines[j].split('Certificate Name:')[1]?.trim();
}
}
break;
}
}
// Если нашли путь, проверяем сертификат
if (certPath) {
const certCheckResult = await execDockerCommand(`openssl x509 -in ${certPath} -noout -dates -subject -issuer 2>&1 || echo "Certificate not found"`);
if (certCheckResult.code === 0 && !certCheckResult.stdout.includes('not found') && !certCheckResult.stdout.includes('No such file')) {
certInfo = {
exists: true,
details: certCheckResult.stdout,
certName: certName,
certPath: certPath
};
} else {
certInfo = {
exists: false,
error: certCheckResult.stdout || 'Сертификат не найден'
};
}
} else { } else {
certInfo = { certInfo = {
exists: false, exists: false,
error: certCheckResult.stdout error: 'Сертификат не найден для домена ' + domain + ' в выводе certbot certificates'
}; };
} }
} }
// Парсим все сертификаты из вывода certbot certificates
const allCertificates = [];
if (checkResult.stdout) {
const lines = checkResult.stdout.split('\n');
let currentCert = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('Certificate Name:')) {
if (currentCert) {
allCertificates.push(currentCert);
}
currentCert = {
name: line.split('Certificate Name:')[1]?.trim(),
domains: [],
expiryDate: null,
certPath: null,
keyPath: null
};
} else if (currentCert) {
if (line.includes('Domains:')) {
currentCert.domains = line.split('Domains:')[1]?.trim().split(/\s+/);
} else if (line.includes('Expiry Date:')) {
currentCert.expiryDate = line.split('Expiry Date:')[1]?.trim();
} else if (line.includes('Certificate Path:')) {
currentCert.certPath = line.split('Certificate Path:')[1]?.trim();
} else if (line.includes('Private Key Path:')) {
currentCert.keyPath = line.split('Private Key Path:')[1]?.trim();
}
}
}
if (currentCert) {
allCertificates.push(currentCert);
}
}
res.json({ res.json({
success: true, success: true,
certificates: checkResult.stdout, certificates: checkResult.stdout,
allCertificates: allCertificates,
domain: domain, domain: domain,
certInfo: certInfo certInfo: certInfo
}); });