VNC setup

This commit is contained in:
2026-09-15 00:36:20 -04:00
parent 6f83981242
commit 4c8e18b682
4 changed files with 439 additions and 32 deletions
+8 -10
View File
@@ -9,14 +9,20 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@novnc/novnc": "^1.7.0",
"dotenv": "^17.4.1", "dotenv": "^17.4.1",
"express": "^5.2.1", "express": "^5.2.1"
"node-gpsd": "^0.3.4"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
} }
}, },
"node_modules/@novnc/novnc": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@novnc/novnc/-/novnc-1.7.0.tgz",
"integrity": "sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==",
"license": "MPL-2.0"
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -723,14 +729,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-gpsd": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/node-gpsd/-/node-gpsd-0.3.4.tgz",
"integrity": "sha512-sI9hPfHiaWDmhjE1oJZnhMo7UF2vQVGl3qk0K4HbN1L8BkS0I+rd6V687eHOj/6ervXiHrhwXzYYsWdXxGy0Qg==",
"engines": {
"node": ">=v0.8.0"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
+1
View File
@@ -11,6 +11,7 @@
"license": "MIT", "license": "MIT",
"description": "Dashboard", "description": "Dashboard",
"dependencies": { "dependencies": {
"@novnc/novnc": "^1.7.0",
"dotenv": "^17.4.1", "dotenv": "^17.4.1",
"express": "^5.2.1" "express": "^5.2.1"
}, },
+423 -20
View File
@@ -1,30 +1,433 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en" class="noVNC_loading">
<head> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!--
<title>Remote Desktop</title> noVNC example: simple example using default UI
<style> Copyright (C) 2019 The noVNC authors
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #000; } noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
#screen { width: 100%; height: 100%; } This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
</style>
</head> Connect parameters are provided in query string:
<body> http://example.com/?host=HOST&port=PORT&encrypt=1
<div id="screen"></div> or the fragment:
http://example.com/#host=HOST&port=PORT&encrypt=1
-->
<title>noVNC</title>
<link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico">
<meta name="theme-color" content="#313131">
<!-- Apple iOS Safari settings -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- @2x -->
<link rel="apple-touch-icon" sizes="40x40" type="image/png" href="app/images/icons/novnc-ios-40.png">
<link rel="apple-touch-icon" sizes="58x58" type="image/png" href="app/images/icons/novnc-ios-58.png">
<link rel="apple-touch-icon" sizes="80x80" type="image/png" href="app/images/icons/novnc-ios-80.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
<link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-ios-152.png">
<link rel="apple-touch-icon" sizes="167x167" type="image/png" href="app/images/icons/novnc-ios-167.png">
<!-- @3x -->
<link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-ios-60.png">
<link rel="apple-touch-icon" sizes="87x87" type="image/png" href="app/images/icons/novnc-ios-87.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png">
<!-- Stylesheets -->
<link rel="stylesheet" href="app/styles/constants.css">
<link rel="stylesheet" href="app/styles/base.css">
<link rel="stylesheet" href="app/styles/input.css">
<!-- Images that will later appear via CSS -->
<link rel="preload" as="image" href="app/images/info.svg">
<link rel="preload" as="image" href="app/images/error.svg">
<link rel="preload" as="image" href="app/images/warning.svg">
<script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
<script type="module"> <script type="module">
import RFB from '/vnc/novnc/core/rfb.js'; import UI from "./app/ui.js";
import * as Log from './core/util/logging.js';
const screen = document.querySelector('#screen'); let response;
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
const rfb = new RFB(screen, `${protocol}://${location.host}/ws/vnc`);
rfb.scaleViewport = true; let defaults = {};
rfb.resizeSession = false; let mandatory = {};
rfb.showDotCursor = true;
rfb.addEventListener('connect', () => console.log('RFB connected')); // Default settings will be loaded from defaults.json. Mandatory
rfb.addEventListener('disconnect', event => console.log('RFB disconnected', event.detail)); // settings will be loaded from mandatory.json, which the user
// cannot change.
try {
response = await fetch('./defaults.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
defaults = await response.json();
} catch (err) {
Log.Error("Couldn't fetch defaults.json: " + err);
}
try {
response = await fetch('./mandatory.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
mandatory = await response.json();
} catch (err) {
Log.Error("Couldn't fetch mandatory.json: " + err);
}
// You can also override any defaults you need here:
//
// defaults['host'] = 'vnc.example.com';
// Or force a specific setting, preventing the user from
// changing it:
//
// mandatory['view_only'] = true;
// See docs/EMBEDDING.md for a list of possible settings.
UI.start({ settings: { defaults: defaults,
mandatory: mandatory } });
</script> </script>
</head>
<body>
<div id="noVNC_fallback_error" class="noVNC_center">
<div>
<div>noVNC encountered an error:</div>
<br>
<div id="noVNC_fallback_errormsg"></div>
</div>
</div>
<!-- noVNC control bar -->
<div id="noVNC_control_bar_anchor" class="noVNC_crosscenter">
<div id="noVNC_control_bar">
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
<div class="noVNC_scroll">
<img class="noVNC_logo" src="app/images/icons/novnc-icon-35x21.svg"
alt="noVNC">
<hr>
<!-- Drag/Pan the viewport -->
<input type="image" alt="Drag" src="app/images/drag.svg"
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
title="Move/Drag viewport">
<!--noVNC touch device only buttons-->
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
<!-- Extra manual keys -->
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
title="Show extra keys">
<div class="noVNC_crosscenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
id="noVNC_toggle_ctrl_button" class="noVNC_button"
title="Toggle Ctrl">
<input type="image" alt="Alt" src="app/images/alt.svg"
id="noVNC_toggle_alt_button" class="noVNC_button"
title="Toggle Alt">
<input type="image" alt="Windows" src="app/images/windows.svg"
id="noVNC_toggle_windows_button" class="noVNC_button"
title="Toggle Windows">
<input type="image" alt="Tab" src="app/images/tab.svg"
id="noVNC_send_tab_button" class="noVNC_button"
title="Send Tab">
<input type="image" alt="Esc" src="app/images/esc.svg"
id="noVNC_send_esc_button" class="noVNC_button"
title="Send Escape">
<input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
title="Send Ctrl-Alt-Del">
</div>
</div>
<!-- Shutdown/Reboot -->
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
id="noVNC_power_button" class="noVNC_button"
title="Shutdown/Reboot...">
<div class="noVNC_crosscenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/power.svg"> Power
</div>
<input type="button" id="noVNC_shutdown_button" value="Shutdown">
<input type="button" id="noVNC_reboot_button" value="Reboot">
<input type="button" id="noVNC_reset_button" value="Reset">
</div>
</div>
<!-- Clipboard -->
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
id="noVNC_clipboard_button" class="noVNC_button"
title="Clipboard">
<div class="noVNC_crosscenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
</div>
<p class="noVNC_subheading">
Edit clipboard content in the textbox below.
</p>
<textarea id="noVNC_clipboard_text" rows=5></textarea>
</div>
</div>
<!-- Toggle fullscreen -->
<input type="image" alt="Full screen" src="app/images/fullscreen.svg"
id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
title="Full screen">
<!-- Settings -->
<input type="image" alt="Settings" src="app/images/settings.svg"
id="noVNC_settings_button" class="noVNC_button"
title="Settings">
<div class="noVNC_crosscenter">
<div id="noVNC_settings" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
</div>
<ul>
<li>
<label>
<input id="noVNC_setting_shared" type="checkbox"
class="toggle">
Shared mode
</label>
</li>
<li>
<label>
<input id="noVNC_setting_view_only" type="checkbox"
class="toggle">
View only
</label>
</li>
<li><hr></li>
<li>
<label>
<input id="noVNC_setting_view_clip" type="checkbox"
class="toggle">
Clip to window
</label>
</li>
<li>
<label for="noVNC_setting_resize">Scaling mode:</label>
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local scaling</option>
<option value="remote">Remote resizing</option>
</select>
</li>
<li><hr></li>
<li>
<div class="noVNC_expander">Advanced</div>
<div><ul>
<li>
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li>
<label for="noVNC_setting_compression">Compression level:</label>
<input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
</li>
<li><hr></li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="text" value="">
</li>
<li>
<div class="noVNC_expander">WebSocket</div>
<div><ul>
<li>
<label>
<input id="noVNC_setting_encrypt" type="checkbox"
class="toggle">
Encrypt
</label>
</li>
<li>
<label for="noVNC_setting_host">Host:</label>
<input id="noVNC_setting_host">
</li>
<li>
<label for="noVNC_setting_port">Port:</label>
<input id="noVNC_setting_port" type="number">
</li>
<li>
<label for="noVNC_setting_path">Path:</label>
<input id="noVNC_setting_path" type="text" value="websockify">
</li>
</ul></div>
</li>
<li><hr></li>
<li>
<label>
<input id="noVNC_setting_reconnect" type="checkbox"
class="toggle">
Automatic reconnect
</label>
</li>
<li>
<label for="noVNC_setting_reconnect_delay">Reconnect delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number">
</li>
<li><hr></li>
<li>
<label>
<input id="noVNC_setting_show_dot" type="checkbox"
class="toggle">
Show dot when no cursor
</label>
</li>
<li>
<label>
<input id="noVNC_setting_keep_device_awake" type="checkbox"
class="toggle">
Keep client display awake while connected
</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown -->
<li>
<label>Logging:
<select id="noVNC_setting_logging" name="vncLogging">
</select>
</label>
</li>
</ul></div>
</li>
<li class="noVNC_version_separator"><hr></li>
<li class="noVNC_version_wrapper">
<span>Version:</span>
<span class="noVNC_version"></span>
</li>
</ul>
</div>
</div>
<!-- Connection controls -->
<input type="image" alt="Disconnect" src="app/images/disconnect.svg"
id="noVNC_disconnect_button" class="noVNC_button"
title="Disconnect">
</div>
</div>
</div> <!-- End of noVNC_control_bar -->
<div class="noVNC_hint_anchor noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>
<div class="noVNC_hint_anchor noVNC_right noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>
<div class="noVNC_hint_anchor noVNC_top noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>
<div class="noVNC_hint_anchor noVNC_bottom noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>
<!-- Status dialog -->
<div id="noVNC_status"></div>
<!-- Connect button -->
<div class="noVNC_center">
<div id="noVNC_connect_dlg">
<p class="noVNC_logo" translate="no"><span>no</span>VNC</p>
<div>
<button id="noVNC_connect_button">
<img alt="" src="app/images/connect.svg"> Connect
</button>
</div>
</div>
</div>
<!-- Server key verification dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
Server identity
</div>
<div>
The server has provided the following identifying information:
</div>
<div id="noVNC_fingerprint_block">
Fingerprint:
<span id="noVNC_fingerprint"></span>
</div>
<div>
Please verify that the information is correct and press
"Approve". Otherwise press "Reject".
</div>
<div class="button_row">
<input id="noVNC_approve_server_button" type="submit" value="Approve">
<input id="noVNC_reject_server_button" type="button" value="Reject">
</div>
</form></div>
</div>
<!-- Password dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
Credentials
</div>
<div id="noVNC_username_block">
<label for="noVNC_username_input">Username:</label>
<input id="noVNC_username_input">
</div>
<div id="noVNC_password_block">
<label for="noVNC_password_input">Password:</label>
<input id="noVNC_password_input" type="password">
</div>
<div class="button_row">
<input id="noVNC_credentials_button" type="submit" value="Send credentials">
</div>
</form></div>
</div>
<!-- Transition screens -->
<div id="noVNC_transition">
<div id="noVNC_transition_text"></div>
<div>
<input type="button" id="noVNC_cancel_reconnect_button" value="Cancel">
</div>
<div class="noVNC_spinner"></div>
</div>
<!-- This is where the RFB elements will attach -->
<div id="noVNC_container">
<!-- Note that Google Chrome on Android doesn't respect any of these,
html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example -->
<textarea id="noVNC_keyboardinput" autocapitalize="off"
autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
</div>
<audio id="noVNC_bell">
<source src="app/sounds/bell.oga" type="audio/ogg">
<source src="app/sounds/bell.mp3" type="audio/mpeg">
</audio>
</body> </body>
</html> </html>
+7 -2
View File
@@ -5,13 +5,18 @@ import { fileURLToPath } from 'node:url';
import { registerSocket } from './websocket.mjs'; import { registerSocket } from './websocket.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const NOVNC = path.join(__dirname, '../node_modules/@novnc/novnc'); const NOVNC = path.join(__dirname, '../../node_modules/@novnc/novnc');
const VNC_HOST = process.env.VNC_HOST || '127.0.0.1'; const VNC_HOST = process.env.VNC_HOST || '127.0.0.1';
const VNC_PORT = process.env.VNC_PORT || 5900; const VNC_PORT = process.env.VNC_PORT || 5900;
export const vncRouter = express.Router(); export const vncRouter = express.Router();
vncRouter.get('/novnc/desktop.html', (req, res) =>
res.sendFile(path.join(__dirname, '../../public/desktop.html'))
);
vncRouter.use('/novnc', express.static(NOVNC)); vncRouter.use('/novnc', express.static(NOVNC));
vncRouter.get('/', (req, res) => res.sendFile(path.join(__dirname, '../public/vnc.html'))); vncRouter.get('/', (req, res) =>
res.redirect('/vnc/novnc/vnc.html?path=ws/vnc&autoconnect=true&resize=scale')
);
// Namespaced so other websocket features can register alongside this one // Namespaced so other websocket features can register alongside this one
const wss = registerSocket('/ws/vnc', { const wss = registerSocket('/ws/vnc', {