mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
- The lite VNC page took the proxy's host, port and path from its link. A link naming another host made the page send that host a fresh signed console token, which opened the VM's console for two minutes. The page now uses only what the server gives it, like the full page. - On a local-socket host, a VM whose console listens on 127.0.0.1 is reached directly instead of being refused for want of an SSH tunnel. - A shut-off VM's console says that the VM is not running instead of failing with no reason.
349 lines
No EOL
12 KiB
HTML
Executable file
349 lines
No EOL
12 KiB
HTML
Executable file
{% extends "console-base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
|
|
{% block head %}
|
|
<!--
|
|
noVNC example: lightweight example using minimal UI and features
|
|
This is a self-contained file which doesn't import WebUtil or external CSS.
|
|
Copyright (C) 2019 The noVNC Authors
|
|
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
|
|
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
|
|
Connect parameters are provided in query string:
|
|
http://example.com/?scale=true (the proxy host, port and path come from the server)
|
|
-->
|
|
<title>WebVirtCloud - noVNC - Lite</title>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<!-- Always force latest IE rendering engine (even in intranet) &
|
|
Chrome Frame. Remove this if you use the .htaccess -->
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
|
|
<!-- Stylesheets -->
|
|
<link rel="stylesheet" href='{% static "js/novnc/app/styles/lite.css" %}'>
|
|
|
|
<!-- actual script modules -->
|
|
<script type="module" crossorigin="anonymous">
|
|
// RFB holds the API to connect and communicate with a VNC server
|
|
import RFB from '{% static "js/novnc/core/rfb.js" %}';
|
|
import KeyTable from '{% static "js/novnc/core/input/keysym.js" %}';
|
|
|
|
let rfb;
|
|
let desktopName;
|
|
|
|
// When this function is called we have
|
|
// successfully connected to a server
|
|
function connectedToServer(e) {
|
|
status("Connected to " + desktopName);
|
|
}
|
|
|
|
// This function is called when we are disconnected
|
|
function disconnectedFromServer(e) {
|
|
if (e.detail.clean) {
|
|
status("Disconnected");
|
|
} else {
|
|
status("Something went wrong, connection is closed");
|
|
}
|
|
}
|
|
|
|
// When this function is called, the server requires
|
|
// credentials to authenticate
|
|
function credentialsAreRequired(e) {
|
|
const password = prompt("Password Required:");
|
|
rfb.sendCredentials({ password: password });
|
|
}
|
|
|
|
// When this function is called we have received
|
|
// a desktop name from the server
|
|
function updateDesktopName(e) {
|
|
desktopName = e.detail.name;
|
|
}
|
|
|
|
function isConnected() {
|
|
return rfb && (!rfb._rfbConnectionState || rfb._rfbConnectionState === 'connected');
|
|
}
|
|
|
|
function sendSingleKey(keysym, code) {
|
|
if (!isConnected()) { return false; }
|
|
rfb.sendKey(keysym, code, true);
|
|
setTimeout(() => {
|
|
if (isConnected()) {
|
|
rfb.sendKey(keysym, code, false);
|
|
}
|
|
}, 20);
|
|
return false;
|
|
}
|
|
|
|
function sendKeyCombo(keys) {
|
|
if (!isConnected()) { return false; }
|
|
for (let i = 0; i < keys.length; i++) {
|
|
rfb.sendKey(keys[i].keysym, keys[i].code, true);
|
|
}
|
|
setTimeout(() => {
|
|
if (isConnected()) {
|
|
for (let i = keys.length - 1; i >= 0; i--) {
|
|
rfb.sendKey(keys[i].keysym, keys[i].code, false);
|
|
}
|
|
}
|
|
}, 20);
|
|
return false;
|
|
}
|
|
|
|
// Since most operating systems will catch Ctrl+Alt+Del
|
|
// before they get a chance to be intercepted by the browser,
|
|
// we provide a way to emulate this key sequence.
|
|
function sendCtrlAltDel() {
|
|
if (isConnected() && typeof rfb.sendCtrlAltDel === 'function') {
|
|
rfb.sendCtrlAltDel();
|
|
} else {
|
|
sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Control_L, code: 'ControlLeft' },
|
|
{ keysym: KeyTable.XK_Alt_L, code: 'AltLeft' },
|
|
{ keysym: KeyTable.XK_Delete, code: 'Delete' }
|
|
]);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const fnKeys = [
|
|
{ keysym: KeyTable.XK_F1, code: 'F1' },
|
|
{ keysym: KeyTable.XK_F2, code: 'F2' },
|
|
{ keysym: KeyTable.XK_F3, code: 'F3' },
|
|
{ keysym: KeyTable.XK_F4, code: 'F4' },
|
|
{ keysym: KeyTable.XK_F5, code: 'F5' },
|
|
{ keysym: KeyTable.XK_F6, code: 'F6' },
|
|
{ keysym: KeyTable.XK_F7, code: 'F7' },
|
|
{ keysym: KeyTable.XK_F8, code: 'F8' },
|
|
{ keysym: KeyTable.XK_F9, code: 'F9' },
|
|
{ keysym: KeyTable.XK_F10, code: 'F10' },
|
|
{ keysym: KeyTable.XK_F11, code: 'F11' },
|
|
{ keysym: KeyTable.XK_F12, code: 'F12' }
|
|
];
|
|
|
|
function sendCtrlAltFN(f) {
|
|
if (f < 0 || f >= fnKeys.length) { return false; }
|
|
return sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Control_L, code: 'ControlLeft' },
|
|
{ keysym: KeyTable.XK_Alt_L, code: 'AltLeft' },
|
|
fnKeys[f]
|
|
]);
|
|
}
|
|
|
|
function fullscreen() {
|
|
if (document.fullscreenElement || // alternative standard method
|
|
document.mozFullScreenElement || // currently working methods
|
|
document.webkitFullscreenElement ||
|
|
document.msFullscreenElement) {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen();
|
|
} else if (document.mozCancelFullScreen) {
|
|
document.mozCancelFullScreen();
|
|
} else if (document.webkitExitFullscreen) {
|
|
document.webkitExitFullscreen();
|
|
} else if (document.msExitFullscreen) {
|
|
document.msExitFullscreen();
|
|
}
|
|
} else {
|
|
const el = document.documentElement;
|
|
const request = el.requestFullscreen ||
|
|
el.mozRequestFullScreen ||
|
|
el.webkitRequestFullscreen ||
|
|
el.msRequestFullscreen;
|
|
if (request) {
|
|
request.call(el).catch(() => {});
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
document.addEventListener('fullscreenchange', () => {
|
|
if (document.fullscreenElement) {
|
|
if ('keyboard' in navigator && typeof navigator.keyboard.lock === 'function') {
|
|
navigator.keyboard.lock(['AltLeft', 'AltRight', 'Tab', 'Escape']).catch(() => {});
|
|
}
|
|
} else {
|
|
if ('keyboard' in navigator && typeof navigator.keyboard.unlock === 'function') {
|
|
navigator.keyboard.unlock();
|
|
}
|
|
}
|
|
});
|
|
|
|
function machineShutdown() {
|
|
if (isConnected()) { rfb.machineShutdown(); }
|
|
return false;
|
|
}
|
|
function machineReboot() {
|
|
if (isConnected()) { rfb.machineReboot(); }
|
|
return false;
|
|
}
|
|
function machineReset() {
|
|
if (isConnected()) { rfb.machineReset(); }
|
|
return false;
|
|
}
|
|
|
|
// Show a status text in the top bar
|
|
function status(text) {
|
|
document.getElementById('noVNC_status').textContent = text;
|
|
}
|
|
|
|
function bindKeyAction(id, handler) {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
handler();
|
|
});
|
|
}
|
|
}
|
|
|
|
// This function extracts the value of one variable from the
|
|
// query string. If the variable isn't defined in the URL
|
|
// it returns the default value instead.
|
|
function readQueryVariable(name, defaultValue) {
|
|
// A URL with a query parameter can look like this:
|
|
// https://www.example.com?myqueryparam=myvalue
|
|
//
|
|
// Note that we use location.href instead of location.search
|
|
// because Firefox < 53 has a bug w.r.t location.search
|
|
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
|
|
match = document.location.href.match(re);
|
|
|
|
if (match) {
|
|
// We have to decode the URL since want the cleartext value
|
|
return decodeURIComponent(match[1]);
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
bindKeyAction('sendCtrlAltDelButton', sendCtrlAltDel);
|
|
bindKeyAction('machineShutdownButton', machineShutdown);
|
|
bindKeyAction('machineRebootButton', machineReboot);
|
|
bindKeyAction('machineResetButton', machineReset);
|
|
bindKeyAction('fullscreen_button', fullscreen);
|
|
// fit the remote screen to the window, or show it 1:1 with scrollbars
|
|
bindKeyAction('scale_button', () => {
|
|
rfb.scaleViewport = !rfb.scaleViewport;
|
|
const link = document.querySelector('#scale_button a');
|
|
link.classList.toggle('active', rfb.scaleViewport);
|
|
link.setAttribute('aria-pressed', rfb.scaleViewport);
|
|
});
|
|
|
|
bindKeyAction('ctrlaltdel', sendCtrlAltDel);
|
|
bindKeyAction('ctrlaltbackspace', () => sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Control_L, code: 'ControlLeft' },
|
|
{ keysym: KeyTable.XK_Alt_L, code: 'AltLeft' },
|
|
{ keysym: KeyTable.XK_BackSpace, code: 'Backspace' }
|
|
]));
|
|
bindKeyAction('alttab', () => sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Alt_L, code: 'AltLeft' },
|
|
{ keysym: KeyTable.XK_Tab, code: 'Tab' }
|
|
]));
|
|
bindKeyAction('superkey', () => sendSingleKey(KeyTable.XK_Super_L, 'MetaLeft'));
|
|
bindKeyAction('sendesc', () => sendSingleKey(KeyTable.XK_Escape, 'Escape'));
|
|
bindKeyAction('sendtab', () => sendSingleKey(KeyTable.XK_Tab, 'Tab'));
|
|
|
|
bindKeyAction('ctrlc', () => sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Control_L, code: 'ControlLeft' },
|
|
{ keysym: KeyTable.XK_c, code: 'KeyC' }
|
|
]));
|
|
bindKeyAction('ctrlz', () => sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Control_L, code: 'ControlLeft' },
|
|
{ keysym: KeyTable.XK_z, code: 'KeyZ' }
|
|
]));
|
|
bindKeyAction('ctrld', () => sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Control_L, code: 'ControlLeft' },
|
|
{ keysym: KeyTable.XK_d, code: 'KeyD' }
|
|
]));
|
|
bindKeyAction('ctrlbackslash', () => sendKeyCombo([
|
|
{ keysym: KeyTable.XK_Control_L, code: 'ControlLeft' },
|
|
{ keysym: KeyTable.XK_backslash, code: 'Backslash' }
|
|
]));
|
|
|
|
for (let i = 0; i < 12; i++) {
|
|
bindKeyAction('ctrlaltf' + (i + 1), () => sendCtrlAltFN(i));
|
|
}
|
|
|
|
// The proxy the server names, never one from the link: the page sends
|
|
// it a console token
|
|
const host = '{{ ws_host|escapejs }}';
|
|
let port = '{{ ws_port|escapejs }}';
|
|
|
|
{% if perms.instances.passwordless_console and console_passwd %}
|
|
const password = '{{ console_passwd|escapejs }}';
|
|
{% else %}
|
|
const password = readQueryVariable('password');
|
|
{% endif %}
|
|
|
|
const path = '{{ ws_path|escapejs }}';
|
|
|
|
// | | | | | |
|
|
// | | | Connect | | |
|
|
// v v v v v v
|
|
|
|
status("Connecting");
|
|
|
|
// Build the websocket URL used to connect
|
|
let url;
|
|
if (window.location.protocol === "https:") {
|
|
url = 'wss';
|
|
} else {
|
|
url = 'ws';
|
|
}
|
|
url += '://' + host;
|
|
if (port) {
|
|
url += ':' + port;
|
|
}
|
|
if (path) {
|
|
url += path.startsWith('/') ? path : ('/' + path);
|
|
} else {
|
|
url += '/';
|
|
}
|
|
const token = '{{ token|escapejs }}';
|
|
if (token) {
|
|
url += (url.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token);
|
|
}
|
|
|
|
// Creating a new RFB object will start a new connection
|
|
rfb = new RFB(document.getElementById('noVNC_container'), url,
|
|
{ credentials: { password: password } });
|
|
|
|
// Add listeners to important events from the RFB module
|
|
rfb.addEventListener("connect", connectedToServer);
|
|
rfb.addEventListener("disconnect", disconnectedFromServer);
|
|
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
|
|
rfb.addEventListener("desktopname", updateDesktopName);
|
|
|
|
// Set parameters that can be changed on an active connection
|
|
rfb.scaleViewport = {{ scale|yesno:"true,false" }};
|
|
rfb.viewOnly = {{ view_only|yesno:"true,false" }};
|
|
rfb.resizeSession = {{ resize_session|yesno:"true,false" }};
|
|
rfb.clipViewport = {{ clip_viewport|yesno:"true,false" }};
|
|
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block navbarmenu %}
|
|
<li class="nav-item" id="scale_button">
|
|
<a class="nav-link{% if scale %} active{% endif %}" href="#" role="button" aria-pressed="{{ scale|yesno:"true,false" }}">{% trans "Scale" %}</a>
|
|
</li>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="noVNC_status_bar">
|
|
<div id="noVNC_left_dummy_elem"></div>
|
|
<div id="noVNC_status">{% trans 'Loading' %}</div>
|
|
<div id="noVNC_buttons">
|
|
<input type="button" value="Send CtrlAltDel" id="sendCtrlAltDelButton" class="noVNC_shown">
|
|
<span id="noVNC_power_buttons" class="noVNC_hidden">
|
|
<input type="button" value="Shutdown" id="machineShutdownButton">
|
|
<input type="button" value="Reboot" id="machineRebootButton">
|
|
<input type="button" value="Reset" id="machineResetButton">
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div id="noVNC_container">
|
|
<!-- This is where the remote screen will appear -->
|
|
</div>
|
|
{% endblock %} |