ваше сообщение коммита
This commit is contained in:
359
webssh-agent/agent.js
Normal file
359
webssh-agent/agent.js
Normal file
@@ -0,0 +1,359 @@
|
||||
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);
|
||||
});
|
||||
1237
webssh-agent/package-lock.json
generated
Normal file
1237
webssh-agent/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
webssh-agent/package.json
Normal file
33
webssh-agent/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "webssh-agent",
|
||||
"version": "1.0.0",
|
||||
"description": "Local SSH tunnel agent for automatic web publishing",
|
||||
"main": "agent.js",
|
||||
"scripts": {
|
||||
"start": "node agent.js",
|
||||
"dev": "nodemon agent.js",
|
||||
"install-service": "node install-service.js"
|
||||
},
|
||||
"keywords": [
|
||||
"ssh",
|
||||
"tunnel",
|
||||
"nginx",
|
||||
"ssl",
|
||||
"web",
|
||||
"publishing"
|
||||
],
|
||||
"author": "DApp Business",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"fs-extra": "^11.1.1",
|
||||
"chalk": "^4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user