- Added SFTP file management capabilities including list, upload, download, delete, and directory operations. - Integrated SFTP progress callbacks to provide real-time feedback during file transfers. - Updated the UI to include a dedicated SFTP browser and host information panel. - Enhanced the sidebar and title bar with improved styling and animations for a cyberpunk theme. - Refactored host management to support editing and connecting to hosts with a more intuitive interface. - Updated package dependencies to support new features and improve performance.
472 lines
13 KiB
JavaScript
472 lines
13 KiB
JavaScript
/**
|
|
* EasyShell - 后端服务器
|
|
* 提供 SSH 代理、SFTP 服务和 WebSocket 通信
|
|
*/
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const { Server } = require('socket.io');
|
|
const cors = require('cors');
|
|
const { Client } = require('ssh2');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
|
|
// Socket.IO 配置
|
|
const io = new Server(server, {
|
|
cors: {
|
|
origin: '*',
|
|
methods: ['GET', 'POST'],
|
|
},
|
|
pingTimeout: 60000,
|
|
pingInterval: 25000,
|
|
});
|
|
|
|
// 中间件
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// 存储活动的 SSH 连接
|
|
const sshConnections = new Map();
|
|
const sftpConnections = new Map();
|
|
|
|
// 健康检查
|
|
app.get('/health', (req, res) => {
|
|
res.json({ status: 'ok', timestamp: Date.now() });
|
|
});
|
|
|
|
// 获取服务器信息
|
|
app.get('/info', (req, res) => {
|
|
res.json({
|
|
name: 'EasyShell Server',
|
|
version: '1.0.0',
|
|
connections: sshConnections.size,
|
|
});
|
|
});
|
|
|
|
// ========== Socket.IO 事件处理 ==========
|
|
|
|
io.on('connection', (socket) => {
|
|
console.log(`✅ 客户端连接: ${socket.id}`);
|
|
|
|
// SSH 连接
|
|
socket.on('ssh:connect', async (hostConfig, callback) => {
|
|
const connectionId = `${socket.id}-${Date.now()}`;
|
|
|
|
try {
|
|
const conn = new Client();
|
|
|
|
conn.on('ready', () => {
|
|
console.log(`✅ SSH 连接成功: ${hostConfig.host}`);
|
|
|
|
// 创建 shell
|
|
conn.shell({ term: 'xterm-256color' }, (err, stream) => {
|
|
if (err) {
|
|
callback({ success: false, error: err.message });
|
|
return;
|
|
}
|
|
|
|
// 存储连接
|
|
sshConnections.set(connectionId, { conn, stream, hostConfig });
|
|
|
|
// 数据传输
|
|
stream.on('data', (data) => {
|
|
socket.emit(`ssh:data:${connectionId}`, data.toString());
|
|
});
|
|
|
|
stream.stderr.on('data', (data) => {
|
|
socket.emit(`ssh:data:${connectionId}`, data.toString());
|
|
});
|
|
|
|
stream.on('close', () => {
|
|
console.log(`📤 SSH 会话关闭: ${hostConfig.host}`);
|
|
socket.emit(`ssh:close:${connectionId}`);
|
|
sshConnections.delete(connectionId);
|
|
});
|
|
|
|
callback({ success: true, connectionId });
|
|
});
|
|
});
|
|
|
|
conn.on('error', (err) => {
|
|
console.error(`❌ SSH 连接错误: ${err.message}`);
|
|
socket.emit(`ssh:error:${connectionId}`, err.message);
|
|
callback({ success: false, error: err.message });
|
|
});
|
|
|
|
// 连接配置
|
|
const connectConfig = {
|
|
host: hostConfig.host,
|
|
port: hostConfig.port || 22,
|
|
username: hostConfig.username,
|
|
readyTimeout: 20000,
|
|
keepaliveInterval: 10000,
|
|
};
|
|
|
|
if (hostConfig.privateKey && hostConfig.privateKey.trim()) {
|
|
connectConfig.privateKey = hostConfig.privateKey;
|
|
}
|
|
if (hostConfig.password && hostConfig.password.trim()) {
|
|
connectConfig.password = hostConfig.password;
|
|
}
|
|
|
|
conn.connect(connectConfig);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// SSH 写入数据
|
|
socket.on('ssh:write', ({ connectionId, data }) => {
|
|
const connection = sshConnections.get(connectionId);
|
|
if (connection?.stream) {
|
|
connection.stream.write(data);
|
|
}
|
|
});
|
|
|
|
// SSH 调整窗口大小
|
|
socket.on('ssh:resize', ({ connectionId, cols, rows }) => {
|
|
const connection = sshConnections.get(connectionId);
|
|
if (connection?.stream) {
|
|
connection.stream.setWindow(rows, cols, 0, 0);
|
|
}
|
|
});
|
|
|
|
// SSH 断开连接
|
|
socket.on('ssh:disconnect', (connectionId) => {
|
|
const connection = sshConnections.get(connectionId);
|
|
if (connection) {
|
|
connection.conn.end();
|
|
sshConnections.delete(connectionId);
|
|
console.log(`📤 SSH 连接已断开: ${connectionId}`);
|
|
}
|
|
});
|
|
|
|
// SSH 执行命令
|
|
socket.on('ssh:exec', async ({ hostConfig, command }, callback) => {
|
|
const conn = new Client();
|
|
let output = '';
|
|
let errorOutput = '';
|
|
|
|
conn.on('ready', () => {
|
|
conn.exec(command, (err, stream) => {
|
|
if (err) {
|
|
conn.end();
|
|
callback({ success: false, error: err.message });
|
|
return;
|
|
}
|
|
|
|
stream.on('close', (code) => {
|
|
conn.end();
|
|
callback({
|
|
success: true,
|
|
code,
|
|
stdout: output,
|
|
stderr: errorOutput,
|
|
});
|
|
});
|
|
|
|
stream.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
stream.stderr.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
});
|
|
});
|
|
});
|
|
|
|
conn.on('error', (err) => {
|
|
callback({ success: false, error: err.message });
|
|
});
|
|
|
|
const connectConfig = {
|
|
host: hostConfig.host,
|
|
port: hostConfig.port || 22,
|
|
username: hostConfig.username,
|
|
};
|
|
|
|
if (hostConfig.privateKey) {
|
|
connectConfig.privateKey = hostConfig.privateKey;
|
|
} else if (hostConfig.password) {
|
|
connectConfig.password = hostConfig.password;
|
|
}
|
|
|
|
conn.connect(connectConfig);
|
|
});
|
|
|
|
// ========== SFTP 操作 ==========
|
|
|
|
// SFTP 列出目录
|
|
socket.on('sftp:list', async ({ hostConfig, remotePath }, callback) => {
|
|
try {
|
|
const result = await sftpOperation(hostConfig, async (sftp) => {
|
|
return new Promise((resolve, reject) => {
|
|
sftp.readdir(remotePath, (err, list) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
const files = list.map(item => ({
|
|
filename: item.filename,
|
|
longname: item.longname,
|
|
attrs: {
|
|
size: item.attrs.size,
|
|
mtime: item.attrs.mtime,
|
|
atime: item.attrs.atime,
|
|
mode: item.attrs.mode,
|
|
isDirectory: (item.attrs.mode & 0o40000) === 0o40000,
|
|
isFile: (item.attrs.mode & 0o100000) === 0o100000,
|
|
}
|
|
}));
|
|
|
|
resolve({ success: true, files });
|
|
});
|
|
});
|
|
});
|
|
callback(result);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// SFTP 创建目录
|
|
socket.on('sftp:mkdir', async ({ hostConfig, remotePath }, callback) => {
|
|
try {
|
|
const result = await sftpOperation(hostConfig, async (sftp) => {
|
|
return new Promise((resolve, reject) => {
|
|
sftp.mkdir(remotePath, (err) => {
|
|
if (err) reject(err);
|
|
else resolve({ success: true });
|
|
});
|
|
});
|
|
});
|
|
callback(result);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// SFTP 删除文件
|
|
socket.on('sftp:delete', async ({ hostConfig, remotePath }, callback) => {
|
|
try {
|
|
const result = await sftpOperation(hostConfig, async (sftp) => {
|
|
return new Promise((resolve, reject) => {
|
|
sftp.unlink(remotePath, (err) => {
|
|
if (err) reject(err);
|
|
else resolve({ success: true });
|
|
});
|
|
});
|
|
});
|
|
callback(result);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// SFTP 删除目录(递归)
|
|
socket.on('sftp:rmdir', async ({ hostConfig, remotePath }, callback) => {
|
|
try {
|
|
const result = await sftpOperation(hostConfig, async (sftp) => {
|
|
const deleteRecursive = async (dirPath) => {
|
|
return new Promise((resolve, reject) => {
|
|
sftp.readdir(dirPath, async (err, list) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
for (const item of list) {
|
|
const itemPath = `${dirPath}/${item.filename}`;
|
|
const isDir = (item.attrs.mode & 0o40000) === 0o40000;
|
|
|
|
if (isDir) {
|
|
await deleteRecursive(itemPath);
|
|
} else {
|
|
await new Promise((res, rej) => {
|
|
sftp.unlink(itemPath, (err) => {
|
|
if (err) rej(err);
|
|
else res();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
sftp.rmdir(dirPath, (err) => {
|
|
if (err) reject(err);
|
|
else resolve();
|
|
});
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
await deleteRecursive(remotePath);
|
|
return { success: true };
|
|
});
|
|
callback(result);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// SFTP 重命名
|
|
socket.on('sftp:rename', async ({ hostConfig, oldPath, newPath }, callback) => {
|
|
try {
|
|
const result = await sftpOperation(hostConfig, async (sftp) => {
|
|
return new Promise((resolve, reject) => {
|
|
sftp.rename(oldPath, newPath, (err) => {
|
|
if (err) reject(err);
|
|
else resolve({ success: true });
|
|
});
|
|
});
|
|
});
|
|
callback(result);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// SFTP 读取文件
|
|
socket.on('sftp:readFile', async ({ hostConfig, remotePath }, callback) => {
|
|
try {
|
|
const result = await sftpOperation(hostConfig, async (sftp) => {
|
|
return new Promise((resolve, reject) => {
|
|
let content = '';
|
|
const readStream = sftp.createReadStream(remotePath);
|
|
|
|
readStream.on('data', (chunk) => {
|
|
content += chunk.toString();
|
|
});
|
|
|
|
readStream.on('error', reject);
|
|
|
|
readStream.on('end', () => {
|
|
resolve({ success: true, content });
|
|
});
|
|
});
|
|
});
|
|
callback(result);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// SFTP 写入文件
|
|
socket.on('sftp:writeFile', async ({ hostConfig, remotePath, content }, callback) => {
|
|
try {
|
|
const result = await sftpOperation(hostConfig, async (sftp) => {
|
|
return new Promise((resolve, reject) => {
|
|
const writeStream = sftp.createWriteStream(remotePath);
|
|
|
|
writeStream.on('error', reject);
|
|
writeStream.on('close', () => resolve({ success: true }));
|
|
|
|
writeStream.end(content);
|
|
});
|
|
});
|
|
callback(result);
|
|
} catch (error) {
|
|
callback({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// 客户端断开连接时清理
|
|
socket.on('disconnect', () => {
|
|
console.log(`📤 客户端断开: ${socket.id}`);
|
|
|
|
// 清理该客户端的所有 SSH 连接
|
|
for (const [id, connection] of sshConnections.entries()) {
|
|
if (id.startsWith(socket.id)) {
|
|
connection.conn.end();
|
|
sshConnections.delete(id);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// SFTP 操作辅助函数
|
|
async function sftpOperation(hostConfig, operation) {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = new Client();
|
|
|
|
conn.on('ready', () => {
|
|
conn.sftp(async (err, sftp) => {
|
|
if (err) {
|
|
conn.end();
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await operation(sftp);
|
|
conn.end();
|
|
resolve(result);
|
|
} catch (error) {
|
|
conn.end();
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
|
|
conn.on('error', reject);
|
|
|
|
const connectConfig = {
|
|
host: hostConfig.host,
|
|
port: hostConfig.port || 22,
|
|
username: hostConfig.username,
|
|
readyTimeout: 20000,
|
|
};
|
|
|
|
if (hostConfig.privateKey && hostConfig.privateKey.trim()) {
|
|
connectConfig.privateKey = hostConfig.privateKey;
|
|
}
|
|
if (hostConfig.password && hostConfig.password.trim()) {
|
|
connectConfig.password = hostConfig.password;
|
|
}
|
|
|
|
conn.connect(connectConfig);
|
|
});
|
|
}
|
|
|
|
// 启动服务器
|
|
const PORT = process.env.PORT || 3001;
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`
|
|
╔═══════════════════════════════════════════════════╗
|
|
║ ║
|
|
║ ⚡ EasyShell Server v1.0.0 ║
|
|
║ ║
|
|
║ 🌐 HTTP: http://0.0.0.0:${PORT} ║
|
|
║ 🔌 Socket: ws://0.0.0.0:${PORT} ║
|
|
║ ║
|
|
║ Ready for connections... ║
|
|
║ ║
|
|
╚═══════════════════════════════════════════════════╝
|
|
`);
|
|
});
|
|
|
|
// 优雅关闭
|
|
process.on('SIGTERM', () => {
|
|
console.log('正在关闭服务器...');
|
|
|
|
// 关闭所有 SSH 连接
|
|
for (const [id, connection] of sshConnections.entries()) {
|
|
connection.conn.end();
|
|
}
|
|
|
|
server.close(() => {
|
|
console.log('服务器已关闭');
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
module.exports = { app, server, io };
|
|
|