diff --git a/app/Dockerfile b/app/Dockerfile index 705bc77..258ccb1 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -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 diff --git a/app/server.js b/app/server.js index 4a058a5..0380fb5 100644 --- a/app/server.js +++ b/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}`); diff --git a/app/services/public-service.js b/app/services/public-service.js new file mode 100644 index 0000000..4da4e3e --- /dev/null +++ b/app/services/public-service.js @@ -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; \ No newline at end of file diff --git a/app/services/route-service.js b/app/services/route-service.js new file mode 100644 index 0000000..5cf1fe0 --- /dev/null +++ b/app/services/route-service.js @@ -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; \ No newline at end of file diff --git a/app/services/ssh-terminal.js b/app/services/ssh-terminal.js new file mode 100644 index 0000000..024d8ac --- /dev/null +++ b/app/services/ssh-terminal.js @@ -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; \ No newline at end of file diff --git a/app/services/vnc-service.js b/app/services/vnc-service.js new file mode 100644 index 0000000..1e03f0b --- /dev/null +++ b/app/services/vnc-service.js @@ -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; \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index f87bfcf..44eea2f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/nginx/default.conf b/nginx/default.conf index 2b06ab6..0df1635 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -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/;