Files
DLE/webssh-agent/agent.js

359 lines
11 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.

const express = require('express');
const cors = require('cors');
const { spawn } = require('child_process');
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const app = express();
const PORT = 12345;
// Middleware
app.use(cors({
origin: ['http://localhost:5173', 'http://localhost:8000'],
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
// Состояние туннеля
let tunnelState = {
connected: false,
domain: null,
tunnelId: null,
sshProcess: null,
config: null
};
// Логирование
const log = {
info: (message) => console.log(chalk.blue('[INFO]'), message),
success: (message) => console.log(chalk.green('[SUCCESS]'), message),
error: (message) => console.log(chalk.red('[ERROR]'), message),
warn: (message) => console.log(chalk.yellow('[WARN]'), message)
};
// Проверка здоровья агента
app.get('/health', (req, res) => {
log.info('Health check requested');
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: '1.0.0',
tunnelConnected: tunnelState.connected
});
});
// Создание туннеля
app.post('/tunnel/create', async (req, res) => {
try {
const { domain, email, sshHost, sshUser, sshKey, localPort, serverPort, sshPort } = req.body;
log.info(`Создание туннеля для домена: ${domain}`);
// Валидация входных данных
if (!domain || !email || !sshHost || !sshUser || !sshKey) {
return res.status(400).json({
success: false,
message: 'Все обязательные поля должны быть заполнены'
});
}
// Если туннель уже подключен, отключаем его
if (tunnelState.connected) {
log.warn('Отключаем существующий туннель');
await disconnectTunnel();
}
// Создаем временную директорию для SSH ключей
const tempDir = path.join(__dirname, 'temp');
await fs.ensureDir(tempDir);
// Сохраняем SSH ключ во временный файл
const keyPath = path.join(tempDir, `ssh_key_${Date.now()}`);
let normalizedKey = sshKey
.replace(/[ \t]+$/gm, '') // убираем пробелы и табы в конце каждой строки
.replace(/\r\n/g, '\n'); // нормализуем переносы строк
// Гарантируем, что после END нет ничего, кроме перевода строки
normalizedKey = normalizedKey.replace(
/(-----END OPENSSH PRIVATE KEY-----)[^\n]*$/m,
'$1\n'
);
await fs.writeFile(keyPath, normalizedKey, { mode: 0o600 });
log.info('SSH ключ сохранен, подключаемся к серверу...');
// Функция для выполнения SSH команд
const execSshCommand = (command) => {
return new Promise((resolve, reject) => {
const sshArgs = [
'-i', keyPath,
'-p', (sshPort || 22).toString(),
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
`${sshUser}@${sshHost}`,
command
];
const sshProcess = spawn('ssh', sshArgs);
let stdout = '';
let stderr = '';
sshProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
sshProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
sshProcess.on('close', (code) => {
resolve({ code, stdout, stderr });
});
sshProcess.on('error', (error) => {
reject(error);
});
});
};
log.info('Подключение к серверу установлено');
// Проверяем и устанавливаем NGINX и certbot
log.info('Проверка и установка NGINX...');
const installResult = await execSshCommand('which nginx || (apt-get update && apt-get install -y nginx certbot python3-certbot-nginx)');
if (installResult.code !== 0) {
log.error('Ошибка установки NGINX: ' + installResult.stderr);
}
// Создание конфигурации NGINX
const nginxConfig = `
server {
listen 80;
server_name ${domain};
location / {
proxy_pass http://localhost:${serverPort || 9000};
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;
proxy_set_header Upgrade \\$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass \\$http_upgrade;
}
}
`;
log.info('Создание конфигурации NGINX...');
await execSshCommand(`echo '${nginxConfig}' > /etc/nginx/sites-available/${domain}`);
await execSshCommand(`ln -sf /etc/nginx/sites-available/${domain} /etc/nginx/sites-enabled/`);
// Проверка и перезагрузка NGINX
const nginxTestResult = await execSshCommand('nginx -t');
if (nginxTestResult.code !== 0) {
log.error('Ошибка конфигурации NGINX: ' + nginxTestResult.stderr);
throw new Error('Ошибка конфигурации NGINX: ' + nginxTestResult.stderr);
}
await execSshCommand('systemctl reload nginx');
log.success('NGINX настроен и перезагружен');
// Получение SSL сертификата
log.info('Получение SSL сертификата...');
const certbotResult = await execSshCommand(
`certbot --nginx -d ${domain} --non-interactive --agree-tos --email ${email} --redirect`
);
if (certbotResult.code !== 0) {
log.warn('Предупреждение при получении SSL: ' + certbotResult.stderr);
// Продолжаем, даже если SSL не получен
} else {
log.success('SSL сертификат получен');
}
// Создание SSH туннеля
log.info('Создание SSH туннеля...');
const tunnelId = Date.now().toString();
const sshArgs = [
'-i', keyPath,
'-p', (sshPort || 22).toString(),
'-R', `${serverPort || 9000}:localhost:${localPort || 5173}`,
'-N',
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'ServerAliveInterval=60',
'-o', 'ServerAliveCountMax=3',
`${sshUser}@${sshHost}`
];
const sshProcess = spawn('ssh', sshArgs);
sshProcess.stdout.on('data', (data) => {
log.info('SSH stdout: ' + data.toString());
});
sshProcess.stderr.on('data', (data) => {
log.warn('SSH stderr: ' + data.toString());
});
sshProcess.on('error', (error) => {
log.error('SSH процесс ошибка: ' + error.message);
tunnelState.connected = false;
});
sshProcess.on('close', (code) => {
log.info(`SSH процесс завершен с кодом: ${code}`);
tunnelState.connected = false;
});
// Даем время на установку соединения
await new Promise(resolve => setTimeout(resolve, 3000));
// Обновляем состояние
tunnelState = {
connected: true,
domain,
tunnelId,
sshProcess,
config: { domain, email, sshHost, sshUser, localPort, serverPort, sshPort, keyPath }
};
log.success(`Туннель успешно создан для домена: ${domain}`);
res.json({
success: true,
message: 'Туннель успешно создан',
tunnelId,
domain,
url: `https://${domain}`
});
} catch (error) {
log.error('Ошибка создания туннеля: ' + error.message);
res.status(500).json({
success: false,
message: error.message
});
}
});
// Отключение туннеля
app.post('/tunnel/disconnect', async (req, res) => {
try {
const result = await disconnectTunnel();
res.json(result);
} catch (error) {
log.error('Ошибка отключения туннеля: ' + error.message);
res.status(500).json({
success: false,
message: error.message
});
}
});
// Функция отключения туннеля
async function disconnectTunnel() {
try {
if (tunnelState.sshProcess) {
tunnelState.sshProcess.kill('SIGTERM');
// Ждем завершения процесса
await new Promise((resolve) => {
tunnelState.sshProcess.on('close', resolve);
setTimeout(resolve, 5000); // Таймаут 5 сек
});
}
// Удаляем временный SSH ключ
if (tunnelState.config && tunnelState.config.keyPath) {
try {
await fs.remove(tunnelState.config.keyPath);
} catch (err) {
log.warn('Не удалось удалить временный SSH ключ: ' + err.message);
}
}
const previousDomain = tunnelState.domain;
tunnelState = {
connected: false,
domain: null,
tunnelId: null,
sshProcess: null,
config: null
};
log.success('Туннель отключен');
return {
success: true,
message: `Туннель для домена ${previousDomain} отключен`
};
} catch (error) {
log.error('Ошибка при отключении туннеля: ' + error.message);
return {
success: false,
message: error.message
};
}
}
// Статус туннеля
app.get('/tunnel/status', (req, res) => {
res.json({
connected: tunnelState.connected,
domain: tunnelState.domain,
tunnelId: tunnelState.tunnelId,
timestamp: new Date().toISOString()
});
});
// Получение логов
app.get('/tunnel/logs', (req, res) => {
// В реальной реализации здесь можно вернуть логи из файла
res.json({
logs: [
{
timestamp: new Date().toISOString(),
level: 'info',
message: 'Агент запущен и готов к работе'
}
]
});
});
// Graceful shutdown
process.on('SIGTERM', async () => {
log.info('Получен сигнал SIGTERM, завершаем работу...');
if (tunnelState.connected) {
await disconnectTunnel();
}
process.exit(0);
});
process.on('SIGINT', async () => {
log.info('Получен сигнал SIGINT, завершаем работу...');
if (tunnelState.connected) {
await disconnectTunnel();
}
process.exit(0);
});
// Запуск сервера
app.listen(PORT, 'localhost', () => {
log.success(`WebSSH Agent запущен на порту ${PORT}`);
log.info('Агент готов к приему команд от фронтенда');
});
// Обработка необработанных ошибок
process.on('uncaughtException', (error) => {
log.error('Необработанная ошибка: ' + error.message);
// console.error(error);
});
process.on('unhandledRejection', (reason, promise) => {
log.error('Необработанное отклонение промиса: ' + reason);
// console.error(reason);
});