mirror of
https://github.com/sailor-sh/CK-X.git
synced 2026-05-06 00:26:41 +00:00
Refactor ssh connection for remote-terminal
This commit is contained in:
@@ -10,6 +10,7 @@ RUN npm install --production
|
||||
|
||||
# Copy the application files
|
||||
COPY server.js ./
|
||||
COPY services/ ./services/
|
||||
COPY public/ ./public/
|
||||
|
||||
# Ensure the public directory exists
|
||||
|
||||
239
app/server.js
239
app/server.js
@@ -1,11 +1,12 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const http = require('http');
|
||||
const socketio = require('socket.io');
|
||||
const { Client } = require('ssh2');
|
||||
const SSHTerminal = require('./services/ssh-terminal');
|
||||
const PublicService = require('./services/public-service');
|
||||
const RouteService = require('./services/route-service');
|
||||
const VNCService = require('./services/vnc-service');
|
||||
|
||||
// Server configuration
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -25,211 +26,47 @@ const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = socketio(server);
|
||||
|
||||
// SSH terminal namespace
|
||||
const sshIO = io.of('/ssh');
|
||||
|
||||
// Handle SSH connections
|
||||
sshIO.on('connection', (socket) => {
|
||||
console.log('New SSH terminal connection established');
|
||||
|
||||
let ssh = new Client();
|
||||
|
||||
// Connect to the SSH server
|
||||
ssh.on('ready', () => {
|
||||
console.log('SSH connection established');
|
||||
|
||||
// Create shell session
|
||||
ssh.shell((err, stream) => {
|
||||
if (err) {
|
||||
console.error('SSH shell error:', err);
|
||||
socket.emit('data', `Error: ${err.message}\r\n`);
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle incoming data from SSH server
|
||||
stream.on('data', (data) => {
|
||||
socket.emit('data', data.toString('utf-8'));
|
||||
});
|
||||
|
||||
// Handle errors on stream
|
||||
stream.on('close', () => {
|
||||
console.log('SSH stream closed');
|
||||
ssh.end();
|
||||
socket.disconnect();
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
console.error('SSH stream error:', err);
|
||||
socket.emit('data', `Error: ${err.message}\r\n`);
|
||||
});
|
||||
|
||||
// Handle incoming data from browser
|
||||
socket.on('data', (data) => {
|
||||
stream.write(data);
|
||||
});
|
||||
|
||||
// Handle resize events
|
||||
socket.on('resize', (dimensions) => {
|
||||
if (dimensions && dimensions.cols && dimensions.rows) {
|
||||
stream.setWindow(dimensions.rows, dimensions.cols, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle socket disconnection
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected from SSH terminal');
|
||||
stream.close();
|
||||
ssh.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle SSH connection errors
|
||||
ssh.on('error', (err) => {
|
||||
console.error('SSH connection error:', err);
|
||||
socket.emit('data', `SSH connection error: ${err.message}\r\n`);
|
||||
socket.disconnect();
|
||||
});
|
||||
|
||||
// Connect to SSH server
|
||||
ssh.connect({
|
||||
host: SSH_HOST,
|
||||
port: SSH_PORT,
|
||||
username: SSH_USER,
|
||||
password: SSH_PASSWORD,
|
||||
readyTimeout: 30000,
|
||||
keepaliveInterval: 10000
|
||||
});
|
||||
// Initialize SSH Terminal
|
||||
const sshTerminal = new SSHTerminal({
|
||||
host: SSH_HOST,
|
||||
port: SSH_PORT,
|
||||
username: SSH_USER,
|
||||
password: SSH_PASSWORD
|
||||
});
|
||||
|
||||
// Create the public directory if it doesn't exist
|
||||
const publicDir = path.join(__dirname, 'public');
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
console.log('Created public directory');
|
||||
}
|
||||
// Initialize Public Service
|
||||
const publicService = new PublicService(path.join(__dirname, 'public'));
|
||||
publicService.initialize();
|
||||
|
||||
// Copy index.html to public directory if it doesn't exist
|
||||
const indexHtmlSrc = path.join(__dirname, 'index.html');
|
||||
const indexHtmlDest = path.join(publicDir, 'index.html');
|
||||
if (fs.existsSync(indexHtmlSrc) && !fs.existsSync(indexHtmlDest)) {
|
||||
fs.copyFileSync(indexHtmlSrc, indexHtmlDest);
|
||||
console.log('Copied index.html to public directory');
|
||||
}
|
||||
|
||||
// Initialize VNC Service
|
||||
const vncService = new VNCService({
|
||||
host: VNC_SERVICE_HOST,
|
||||
port: VNC_SERVICE_PORT,
|
||||
password: VNC_PASSWORD
|
||||
});
|
||||
|
||||
// SSH terminal namespace
|
||||
const sshIO = io.of('/ssh');
|
||||
sshIO.on('connection', (socket) => {
|
||||
sshTerminal.handleConnection(socket);
|
||||
});
|
||||
|
||||
// Initialize Route Service
|
||||
const routeService = new RouteService(publicService, vncService);
|
||||
|
||||
// Serve static files from the public directory
|
||||
app.use(express.static(publicService.getPublicDir()));
|
||||
|
||||
// Setup VNC proxy
|
||||
vncService.setupVNCProxy(app);
|
||||
|
||||
// Setup routes
|
||||
routeService.setupRoutes(app);
|
||||
|
||||
// Enable CORS
|
||||
app.use(cors());
|
||||
|
||||
// Serve static files from the public directory
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Configure VNC proxy middleware
|
||||
const vncProxyConfig = {
|
||||
target: `http://${VNC_SERVICE_HOST}:${VNC_SERVICE_PORT}`,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
secure: false,
|
||||
pathRewrite: {
|
||||
'^/vnc-proxy': ''
|
||||
},
|
||||
onProxyReq: (proxyReq, req, res) => {
|
||||
// Log HTTP requests being proxied
|
||||
console.log(`Proxying HTTP request to VNC server: ${req.url}`);
|
||||
},
|
||||
onProxyReqWs: (proxyReq, req, socket, options, head) => {
|
||||
// Log WebSocket connections
|
||||
console.log(`WebSocket connection established to VNC server: ${req.url}`);
|
||||
},
|
||||
onProxyRes: (proxyRes, req, res) => {
|
||||
// Log the responses from VNC server
|
||||
console.log(`Received response from VNC server for: ${req.url}`);
|
||||
},
|
||||
onError: (err, req, res) => {
|
||||
console.error(`Proxy error: ${err.message}`);
|
||||
if (res && res.writeHead) {
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
res.end(`Proxy error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Middleware to enhance VNC URLs with authentication if needed
|
||||
app.use('/vnc-proxy', (req, res, next) => {
|
||||
// Check if the URL already has a password parameter
|
||||
if (!req.query.password) {
|
||||
// If no password provided, add default password
|
||||
console.log('Adding default VNC password to request');
|
||||
const separator = req.url.includes('?') ? '&' : '?';
|
||||
req.url = `${req.url}${separator}password=${VNC_PASSWORD}`;
|
||||
}
|
||||
next();
|
||||
}, createProxyMiddleware(vncProxyConfig));
|
||||
|
||||
// Direct WebSocket proxy to handle the websockify endpoint
|
||||
app.use('/websockify', createProxyMiddleware({
|
||||
...vncProxyConfig,
|
||||
pathRewrite: {
|
||||
'^/websockify': '/websockify'
|
||||
},
|
||||
ws: true,
|
||||
onProxyReqWs: (proxyReq, req, socket, options, head) => {
|
||||
// Log WebSocket connections to websockify
|
||||
console.log(`WebSocket connection to websockify established: ${req.url}`);
|
||||
|
||||
// Add additional headers if needed
|
||||
proxyReq.setHeader('Origin', `http://${VNC_SERVICE_HOST}:${VNC_SERVICE_PORT}`);
|
||||
},
|
||||
onError: (err, req, res) => {
|
||||
console.error(`Websockify proxy error: ${err.message}`);
|
||||
if (res && res.writeHead) {
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
res.end(`Websockify proxy error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// API endpoint to get VNC server info
|
||||
app.get('/api/vnc-info', (req, res) => {
|
||||
res.json({
|
||||
host: VNC_SERVICE_HOST,
|
||||
port: VNC_SERVICE_PORT,
|
||||
wsUrl: `/websockify`,
|
||||
defaultPassword: VNC_PASSWORD,
|
||||
status: 'connected'
|
||||
});
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({ status: 'ok', message: 'Service is healthy' });
|
||||
});
|
||||
|
||||
// Catch-all route to serve index.html for any other requests
|
||||
app.get('*', (req, res) => {
|
||||
// Special handling for exam page
|
||||
if (req.path === '/exam') {
|
||||
res.sendFile(path.join(__dirname, 'public', 'exam.html'));
|
||||
}
|
||||
// Special handling for results page
|
||||
else if (req.path === '/results') {
|
||||
res.sendFile(path.join(__dirname, 'public', 'results.html'));
|
||||
}
|
||||
else {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Server error:', err);
|
||||
res.status(500).sendFile(path.join(__dirname, 'public', '50x.html'));
|
||||
});
|
||||
|
||||
// Start the server
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
|
||||
35
app/services/public-service.js
Normal file
35
app/services/public-service.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class PublicService {
|
||||
constructor(publicDir) {
|
||||
this.publicDir = publicDir;
|
||||
this.indexHtmlSrc = path.join(__dirname, '..', 'index.html');
|
||||
this.indexHtmlDest = path.join(publicDir, 'index.html');
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.createPublicDirectory();
|
||||
this.copyIndexHtml();
|
||||
}
|
||||
|
||||
createPublicDirectory() {
|
||||
if (!fs.existsSync(this.publicDir)) {
|
||||
fs.mkdirSync(this.publicDir, { recursive: true });
|
||||
console.log('Created public directory');
|
||||
}
|
||||
}
|
||||
|
||||
copyIndexHtml() {
|
||||
if (fs.existsSync(this.indexHtmlSrc) && !fs.existsSync(this.indexHtmlDest)) {
|
||||
fs.copyFileSync(this.indexHtmlSrc, this.indexHtmlDest);
|
||||
console.log('Copied index.html to public directory');
|
||||
}
|
||||
}
|
||||
|
||||
getPublicDir() {
|
||||
return this.publicDir;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PublicService;
|
||||
43
app/services/route-service.js
Normal file
43
app/services/route-service.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const path = require('path');
|
||||
|
||||
class RouteService {
|
||||
constructor(publicService, vncService) {
|
||||
this.publicService = publicService;
|
||||
this.vncService = vncService;
|
||||
}
|
||||
|
||||
setupRoutes(app) {
|
||||
// API endpoint to get VNC server info
|
||||
app.get('/api/vnc-info', (req, res) => {
|
||||
res.json(this.vncService.getVNCInfo());
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({ status: 'ok', message: 'Service is healthy' });
|
||||
});
|
||||
|
||||
// Catch-all route to serve index.html for any other requests
|
||||
app.get('*', (req, res) => {
|
||||
// Special handling for exam page
|
||||
if (req.path === '/exam') {
|
||||
res.sendFile(path.join(this.publicService.getPublicDir(), 'exam.html'));
|
||||
}
|
||||
// Special handling for results page
|
||||
else if (req.path === '/results') {
|
||||
res.sendFile(path.join(this.publicService.getPublicDir(), 'results.html'));
|
||||
}
|
||||
else {
|
||||
res.sendFile(path.join(this.publicService.getPublicDir(), 'index.html'));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Server error:', err);
|
||||
res.status(500).sendFile(path.join(this.publicService.getPublicDir(), '50x.html'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RouteService;
|
||||
81
app/services/ssh-terminal.js
Normal file
81
app/services/ssh-terminal.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const { Client } = require('ssh2');
|
||||
|
||||
class SSHTerminal {
|
||||
constructor(config) {
|
||||
this.config = {
|
||||
host: config.host || 'remote-terminal',
|
||||
port: config.port || 22,
|
||||
username: config.username || 'candidate',
|
||||
password: config.password || 'password',
|
||||
readyTimeout: 30000,
|
||||
keepaliveInterval: 10000
|
||||
};
|
||||
}
|
||||
|
||||
handleConnection(socket) {
|
||||
console.log('New SSH terminal connection established');
|
||||
|
||||
let ssh = new Client();
|
||||
|
||||
ssh.on('ready', () => {
|
||||
console.log('SSH connection established');
|
||||
this.createShellSession(ssh, socket);
|
||||
});
|
||||
|
||||
ssh.on('error', (err) => {
|
||||
console.error('SSH connection error:', err);
|
||||
socket.emit('data', `SSH connection error: ${err.message}\r\n`);
|
||||
socket.disconnect();
|
||||
});
|
||||
|
||||
ssh.connect(this.config);
|
||||
}
|
||||
|
||||
createShellSession(ssh, socket) {
|
||||
ssh.shell((err, stream) => {
|
||||
if (err) {
|
||||
console.error('SSH shell error:', err);
|
||||
socket.emit('data', `Error: ${err.message}\r\n`);
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupStreamHandlers(stream, socket, ssh);
|
||||
});
|
||||
}
|
||||
|
||||
setupStreamHandlers(stream, socket, ssh) {
|
||||
stream.on('data', (data) => {
|
||||
socket.emit('data', data.toString('utf-8'));
|
||||
});
|
||||
|
||||
stream.on('close', () => {
|
||||
console.log('SSH stream closed');
|
||||
ssh.end();
|
||||
socket.disconnect();
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
console.error('SSH stream error:', err);
|
||||
socket.emit('data', `Error: ${err.message}\r\n`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
stream.write(data);
|
||||
});
|
||||
|
||||
socket.on('resize', (dimensions) => {
|
||||
if (dimensions && dimensions.cols && dimensions.rows) {
|
||||
stream.setWindow(dimensions.rows, dimensions.cols, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected from SSH terminal');
|
||||
stream.close();
|
||||
ssh.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SSHTerminal;
|
||||
93
app/services/vnc-service.js
Normal file
93
app/services/vnc-service.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
|
||||
class VNCService {
|
||||
constructor(config) {
|
||||
this.config = {
|
||||
host: config.host || 'remote-desktop-service',
|
||||
port: config.port || 6901,
|
||||
password: config.password || 'bakku-the-wizard'
|
||||
};
|
||||
|
||||
this.vncProxyConfig = {
|
||||
target: `http://${this.config.host}:${this.config.port}`,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
secure: false,
|
||||
pathRewrite: {
|
||||
'^/vnc-proxy': ''
|
||||
},
|
||||
onProxyReq: (proxyReq, req, res) => {
|
||||
// Log HTTP requests being proxied
|
||||
console.log(`Proxying HTTP request to VNC server: ${req.url}`);
|
||||
},
|
||||
onProxyReqWs: (proxyReq, req, socket, options, head) => {
|
||||
// Log WebSocket connections
|
||||
console.log(`WebSocket connection established to VNC server: ${req.url}`);
|
||||
},
|
||||
onProxyRes: (proxyRes, req, res) => {
|
||||
// Log the responses from VNC server
|
||||
console.log(`Received response from VNC server for: ${req.url}`);
|
||||
},
|
||||
onError: (err, req, res) => {
|
||||
console.error(`Proxy error: ${err.message}`);
|
||||
if (res && res.writeHead) {
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
res.end(`Proxy error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setupVNCProxy(app) {
|
||||
// Middleware to enhance VNC URLs with authentication if needed
|
||||
app.use('/vnc-proxy', (req, res, next) => {
|
||||
// Check if the URL already has a password parameter
|
||||
if (!req.query.password) {
|
||||
// If no password provided, add default password
|
||||
console.log('Adding default VNC password to request');
|
||||
const separator = req.url.includes('?') ? '&' : '?';
|
||||
req.url = `${req.url}${separator}password=${this.config.password}`;
|
||||
}
|
||||
next();
|
||||
}, createProxyMiddleware(this.vncProxyConfig));
|
||||
|
||||
// Direct WebSocket proxy to handle the websockify endpoint
|
||||
app.use('/websockify', createProxyMiddleware({
|
||||
...this.vncProxyConfig,
|
||||
pathRewrite: {
|
||||
'^/websockify': '/websockify'
|
||||
},
|
||||
ws: true,
|
||||
onProxyReqWs: (proxyReq, req, socket, options, head) => {
|
||||
// Log WebSocket connections to websockify
|
||||
console.log(`WebSocket connection to websockify established: ${req.url}`);
|
||||
|
||||
// Add additional headers if needed
|
||||
proxyReq.setHeader('Origin', `http://${this.config.host}:${this.config.port}`);
|
||||
},
|
||||
onError: (err, req, res) => {
|
||||
console.error(`Websockify proxy error: ${err.message}`);
|
||||
if (res && res.writeHead) {
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
res.end(`Websockify proxy error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
getVNCInfo() {
|
||||
return {
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
wsUrl: `/websockify`,
|
||||
defaultPassword: this.config.password,
|
||||
status: 'connected'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VNCService;
|
||||
@@ -212,7 +212,7 @@ services:
|
||||
- LOG_LEVEL=info
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- TRACK_METRICS=true # based on the environment variable TRACK_METRICS, the facilitator will send metrics to the metric server
|
||||
- TRACK_METRICS=false # based on the environment variable TRACK_METRICS, the facilitator will send metrics to the metric server
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- jumphost
|
||||
|
||||
@@ -20,6 +20,18 @@ server {
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /ssh/ {
|
||||
proxy_pass http://webapp:3000/ssh/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
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_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Facilitator API endpoint
|
||||
location /facilitator/api/ {
|
||||
proxy_pass http://facilitator:3000/api/;
|
||||
|
||||
Reference in New Issue
Block a user