diff --git a/client/public/desktop.html b/client/public/desktop.html new file mode 100644 index 0000000..5456373 --- /dev/null +++ b/client/public/desktop.html @@ -0,0 +1,30 @@ + + + + + + Remote Desktop + + + +
+ + + + diff --git a/client/public/index.html b/client/public/index.html index 48343cf..db9f66c 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -502,7 +502,7 @@ { name: 'AI Assistant', url: ':1000', icon: 'fa-robot' }, { name: 'ATAK', url: ':1100', icon: 'fa-shield-halved', badge: 'fa-download' }, { name: 'Code Book', url: '/code-book.html', icon: 'fa-lock', badge: 'fa-download', badgeAction: 'saveCodeBook' }, - { name: 'Desktop', url: ':1200', icon: 'fa-desktop', badge: 'fa-download' }, + { name: 'Desktop', url: '/desktop.html', icon: 'fa-desktop', badge: 'fa-download' }, { name: 'File Browser', url: ':1300', icon: 'fa-folder' }, { name: 'Library', url: ':1400', icon: 'fa-book', badge: 'fa-cog', badgeAction: 'openKiwix' }, { name: 'Maps', url: ':1500', icon: 'fa-map' }, diff --git a/client/src/main.mjs b/client/src/main.mjs index 6877a30..59faaa6 100644 --- a/client/src/main.mjs +++ b/client/src/main.mjs @@ -1,8 +1,9 @@ - import express from "express"; import { join } from 'path'; -import {environment} from './services/environment.mjs'; +import { environment } from './services/environment.mjs'; import { statusRouter } from './services/status.mjs'; +import { vncRouter } from './services/vnc.mjs'; +import { attachUpgrade } from './services/websocket.mjs'; (async () => { const app = express(); @@ -15,10 +16,9 @@ import { statusRouter } from './services/status.mjs'; }); // Routes - console.log(join(environment.root, '../public')); app.use(express.static(join(environment.root, '../public'))); - app.use('/api', statusRouter); + app.use('/vnc', vncRouter); // Error handler app.use((err, req, res, next) => { @@ -34,6 +34,9 @@ import { statusRouter } from './services/status.mjs'; process.exit(1); }); + // Single upgrade dispatcher for all namespaced websockets (/ws/vnc, /ws/whatever...) + attachUpgrade(server); + // Shutdown const gracefulShutdown = (signal) => { console.log(`Received ${signal}, shutting down gracefully`); diff --git a/client/src/services/vnc.mjs b/client/src/services/vnc.mjs new file mode 100644 index 0000000..041dd87 --- /dev/null +++ b/client/src/services/vnc.mjs @@ -0,0 +1,53 @@ +import express from 'express'; +import net from 'node:net'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { registerSocket } from './websocket.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const NOVNC = path.join(__dirname, '../node_modules/@novnc/novnc'); +const VNC_HOST = process.env.VNC_HOST || '127.0.0.1'; +const VNC_PORT = process.env.VNC_PORT || 5900; + +export const vncRouter = express.Router(); +vncRouter.use('/novnc', express.static(NOVNC)); +vncRouter.get('/', (req, res) => res.sendFile(path.join(__dirname, '../public/vnc.html'))); + +// Namespaced so other websocket features can register alongside this one +const wss = registerSocket('/ws/vnc', { + handleProtocols: protocols => (protocols.has('binary') ? 'binary' : false) +}); + +wss.on('connection', ws => { + console.log('VNC client connected'); + const socket = net.createConnection(VNC_PORT, VNC_HOST); + + socket.on('data', data => { + if (ws.readyState === ws.OPEN) ws.send(data, { binary: true }); + }); + + ws.on('message', data => { + const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data); + if (socket.writable) socket.write(buffer); + }); + + ws.on('error', error => { + console.error('VNC websocket error:', error.message); + socket.destroy(); + }); + + ws.on('close', (code, reason) => { + console.log(`VNC browser disconnected: ${code}${reason?.length ? ` ${reason}` : ''}`); + socket.destroy(); + }); + + socket.on('error', error => { + console.error('VNC socket error:', error.message); + if (ws.readyState === ws.OPEN) ws.close(); + }); + + socket.on('close', () => { + console.log('VNC socket closed'); + if (ws.readyState === ws.OPEN) ws.close(); + }); +}); diff --git a/client/src/services/websocket.mjs b/client/src/services/websocket.mjs new file mode 100644 index 0000000..fc64472 --- /dev/null +++ b/client/src/services/websocket.mjs @@ -0,0 +1,31 @@ +import { WebSocketServer } from 'ws'; + +/** Registry of path -> WebSocketServer, letting multiple namespaced sockets share one HTTP server. */ +const sockets = new Map(); + +/** + * Register a WebSocketServer under an exact path (e.g. '/ws/vnc'). + * @param {string} path + * @param {object} [options] - extra ws.Server options (handleProtocols, etc.) + * @returns {WebSocketServer} + */ +export function registerSocket(path, options = {}) { + if (sockets.has(path)) throw new Error(`Socket already registered on ${path}`); + const wss = new WebSocketServer({ noServer: true, ...options }); + sockets.set(path, wss); + return wss; +} + +/** + * Attach one 'upgrade' listener to the HTTP server that routes to the + * registered WebSocketServer matching the request path. + * @param {import('http').Server} server + */ +export function attachUpgrade(server) { + server.on('upgrade', (req, socket, head) => { + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const wss = sockets.get(pathname); + if (!wss) return socket.destroy(); + wss.handleUpgrade(req, socket, head, ws => wss.emit('connection', ws, req)); + }); +}