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));
+ });
+}