VNC setup
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Remote Desktop</title>
|
||||||
|
<style>
|
||||||
|
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #000; }
|
||||||
|
#screen { width: 100%; height: 100%; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="screen"></div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import RFB from '/vnc/novnc/core/rfb.js';
|
||||||
|
|
||||||
|
const screen = document.querySelector('#screen');
|
||||||
|
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
const rfb = new RFB(screen, `${protocol}://${location.host}/ws/vnc`);
|
||||||
|
|
||||||
|
rfb.scaleViewport = true;
|
||||||
|
rfb.resizeSession = false;
|
||||||
|
rfb.showDotCursor = true;
|
||||||
|
|
||||||
|
rfb.addEventListener('connect', () => console.log('RFB connected'));
|
||||||
|
rfb.addEventListener('disconnect', event => console.log('RFB disconnected', event.detail));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -502,7 +502,7 @@
|
|||||||
{ name: 'AI Assistant', url: ':1000', icon: 'fa-robot' },
|
{ name: 'AI Assistant', url: ':1000', icon: 'fa-robot' },
|
||||||
{ name: 'ATAK', url: ':1100', icon: 'fa-shield-halved', badge: 'fa-download' },
|
{ 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: '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: 'File Browser', url: ':1300', icon: 'fa-folder' },
|
||||||
{ name: 'Library', url: ':1400', icon: 'fa-book', badge: 'fa-cog', badgeAction: 'openKiwix' },
|
{ name: 'Library', url: ':1400', icon: 'fa-book', badge: 'fa-cog', badgeAction: 'openKiwix' },
|
||||||
{ name: 'Maps', url: ':1500', icon: 'fa-map' },
|
{ name: 'Maps', url: ':1500', icon: 'fa-map' },
|
||||||
|
|||||||
+7
-4
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import {environment} from './services/environment.mjs';
|
import { environment } from './services/environment.mjs';
|
||||||
import { statusRouter } from './services/status.mjs';
|
import { statusRouter } from './services/status.mjs';
|
||||||
|
import { vncRouter } from './services/vnc.mjs';
|
||||||
|
import { attachUpgrade } from './services/websocket.mjs';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -15,10 +16,9 @@ import { statusRouter } from './services/status.mjs';
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
console.log(join(environment.root, '../public'));
|
|
||||||
app.use(express.static(join(environment.root, '../public')));
|
app.use(express.static(join(environment.root, '../public')));
|
||||||
|
|
||||||
app.use('/api', statusRouter);
|
app.use('/api', statusRouter);
|
||||||
|
app.use('/vnc', vncRouter);
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
@@ -34,6 +34,9 @@ import { statusRouter } from './services/status.mjs';
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Single upgrade dispatcher for all namespaced websockets (/ws/vnc, /ws/whatever...)
|
||||||
|
attachUpgrade(server);
|
||||||
|
|
||||||
// Shutdown
|
// Shutdown
|
||||||
const gracefulShutdown = (signal) => {
|
const gracefulShutdown = (signal) => {
|
||||||
console.log(`Received ${signal}, shutting down gracefully`);
|
console.log(`Received ${signal}, shutting down gracefully`);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user