mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 04:06:47 +00:00
feat(console): the VNC console uses the panel's own address
The console pages connect to /novncd/ on the address, scheme and port the page itself was loaded from; nginx proxies it to novncd, which listens on the loopback only. There is nothing to configure: the console works on HTTPS (wss), behind another proxy and on any published port. Before, the page connected to WS_PUBLIC_HOST/PORT/PATH, port 6080 by default: straight to novncd, past nginx and without TLS, so an HTTPS panel and a Docker container published on another port had no console until the public port was set by hand, and novncd listened on every interface. Gone: WS_PUBLIC_PORT, WS_PUBLIC_HOST, WS_PUBLIC_PATH and WS_CERT, the installer's two public console questions and its 6080 firewall rule, the Docker image's WS_PUBLIC_PORT and its init script lines, the manual install's firewall rule for 6080 and nginx's unused /websockify location. WS_HOST defaults to 127.0.0.1. A VM with a QEMU websocket port is still reached on that port of the panel's host name (left for a separate decision).
This commit is contained in:
parent
82af555d45
commit
68b104dfe4
14 changed files with 43 additions and 164 deletions
|
|
@ -22,10 +22,10 @@ from urllib.parse import parse_qs, urlparse
|
|||
import argparse
|
||||
from console.sshtunnels import close_tunnel, open_tunnel, tunnel_target
|
||||
from vrtManager.connection import CONN_SOCKET, CONN_SSH
|
||||
from webvirtcloud.settings import WS_CERT, WS_HOST, WS_PORT
|
||||
from webvirtcloud.settings import WS_HOST, WS_PORT
|
||||
|
||||
|
||||
def get_parser(default_host=WS_HOST, default_port=WS_PORT or 6080, default_cert=WS_CERT or CERT):
|
||||
def get_parser(default_host=WS_HOST, default_port=WS_PORT or 6080, default_cert=CERT):
|
||||
parser = argparse.ArgumentParser(description="WebVirtCloud noVNC daemon")
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
|
|
|
|||
|
|
@ -176,10 +176,9 @@
|
|||
bindKeyAction('ctrlaltf' + (i + 1), () => sendCtrlAltFN(i));
|
||||
}
|
||||
|
||||
let fullPath = '{{ ws_path|escapejs }}';
|
||||
if (fullPath.startsWith('/')) {
|
||||
fullPath = fullPath.substring(1);
|
||||
}
|
||||
// novncd behind the panel's own address (nginx's /novncd/); a VM with a
|
||||
// QEMU websocket port is reached on that port of the panel's host name
|
||||
let fullPath = 'novncd/';
|
||||
const token = '{{ token|escapejs }}';
|
||||
if (token) {
|
||||
fullPath += (fullPath.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token);
|
||||
|
|
@ -195,10 +194,12 @@
|
|||
{% endif %}
|
||||
};
|
||||
|
||||
const https = window.location.protocol === 'https:';
|
||||
const mandatory = {
|
||||
host: '{{ ws_host|escapejs }}',
|
||||
port: {{ ws_port|default:6080 }},
|
||||
host: window.location.hostname,
|
||||
port: Number('{{ console_websocket_port|default_if_none:""|escapejs }}' || window.location.port || (https ? 443 : 80)),
|
||||
path: fullPath,
|
||||
encrypt: https,
|
||||
};
|
||||
|
||||
UI.start({ settings: { defaults, mandatory } });
|
||||
|
|
@ -345,16 +346,16 @@
|
|||
</li>
|
||||
<li>
|
||||
<label for="noVNC_setting_host">Host:</label>
|
||||
<input id="noVNC_setting_host" type="text" value="{{ ws_host }}">
|
||||
<input id="noVNC_setting_host" type="text">
|
||||
</li>
|
||||
<li>
|
||||
<label for="noVNC_setting_port">Port:</label>
|
||||
<input id="noVNC_setting_port" type="number" value="{{ ws_port }}">
|
||||
<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"> -->
|
||||
<input id="noVNC_setting_path" type="text" value="{{ ws_path }}">
|
||||
<input id="noVNC_setting_path" type="text">
|
||||
</li>
|
||||
</ul></div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -265,10 +265,11 @@
|
|||
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 }}';
|
||||
// novncd behind the panel's own address (nginx's /novncd/), never a host
|
||||
// from the link: the page sends it a console token. A VM with a QEMU
|
||||
// websocket port is reached on that port of the panel's host name.
|
||||
const host = window.location.hostname;
|
||||
let port = '{{ console_websocket_port|default_if_none:""|escapejs }}' || window.location.port;
|
||||
|
||||
{% if perms.instances.passwordless_console and console_passwd %}
|
||||
const password = '{{ console_passwd|escapejs }}';
|
||||
|
|
@ -276,7 +277,7 @@
|
|||
const password = readQueryVariable('password');
|
||||
{% endif %}
|
||||
|
||||
const path = '{{ ws_path|escapejs }}';
|
||||
const path = 'novncd/';
|
||||
|
||||
// | | | | | |
|
||||
// | | | Connect | | |
|
||||
|
|
|
|||
|
|
@ -89,8 +89,6 @@ class ConsoleViewsTestCase(TestCase):
|
|||
self.assertEqual(
|
||||
unsigned, f"{self.compute.id}:{self.instance.uuid}:{self.admin_user.id}"
|
||||
)
|
||||
self.assertEqual(response.context["ws_path"], "novncd/")
|
||||
self.assertEqual(response.context["ws_port"], 6080)
|
||||
self.assertEqual(response.context["console_passwd"], "vncsecret")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
|
|
@ -106,7 +104,7 @@ class ConsoleViewsTestCase(TestCase):
|
|||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "console-vnc-full.html")
|
||||
self.assertEqual(response.context["ws_port"], 5900)
|
||||
self.assertEqual(response.context["console_websocket_port"], 5900)
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_the_lite_console_switches_scaling_from_its_menu(self, mock_wvm):
|
||||
|
|
@ -247,7 +245,7 @@ class ConsoleViewsTestCase(TestCase):
|
|||
self.assertNotContains(response, "attacker.example")
|
||||
for name in ("host", "port", "path"):
|
||||
self.assertNotContains(response, f"readQueryVariable('{name}'")
|
||||
self.assertContains(response, "const host = 'testserver';")
|
||||
self.assertContains(response, "const host = window.location.hostname;")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_the_page_sets_no_token_cookie(self, mock_wvm):
|
||||
|
|
@ -278,60 +276,18 @@ class ConsoleViewsTestCase(TestCase):
|
|||
self.assertTemplateNotUsed(response, "console-vnc-lite.html")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
@override_settings(WS_PUBLIC_PATH="/novncd/")
|
||||
def test_console_ws_path_normalization_leading_slash(self, mock_wvm):
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.get_console_type.return_value = "vnc"
|
||||
mock_wvm.return_value = mock_conn
|
||||
|
||||
def test_both_pages_connect_to_novncd_behind_the_panels_own_address(self, mock_wvm):
|
||||
# nginx proxies /novncd/ on the panel's own scheme, host and port: no
|
||||
# console address to configure, HTTPS gives wss
|
||||
mock_wvm.return_value.get_console_type.return_value = "vnc"
|
||||
mock_wvm.return_value.get_console_websocket_port.return_value = None
|
||||
self.client.force_login(self.admin_user)
|
||||
url = reverse("console") + f"?token={self.token}"
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context["ws_path"], "novncd/")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
@override_settings(WS_PUBLIC_PATH="novncd/")
|
||||
def test_console_ws_path_normalization_clean(self, mock_wvm):
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.get_console_type.return_value = "vnc"
|
||||
mock_wvm.return_value = mock_conn
|
||||
|
||||
self.client.force_login(self.admin_user)
|
||||
url = reverse("console") + f"?token={self.token}"
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context["ws_path"], "novncd/")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
@override_settings(WS_PUBLIC_PATH="/")
|
||||
def test_console_ws_path_root_slash(self, mock_wvm):
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.get_console_type.return_value = "vnc"
|
||||
mock_wvm.return_value = mock_conn
|
||||
|
||||
self.client.force_login(self.admin_user)
|
||||
url = reverse("console") + f"?token={self.token}"
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context["ws_path"], "")
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["panel.example.com"])
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_console_ws_host_strip_port(self, mock_wvm):
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.get_console_type.return_value = "vnc"
|
||||
mock_wvm.return_value = mock_conn
|
||||
|
||||
self.client.force_login(self.admin_user)
|
||||
url = reverse("console") + f"?token={self.token}"
|
||||
response = self.client.get(url, HTTP_HOST="panel.example.com:8080")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context["ws_host"], "panel.example.com")
|
||||
lite = self.client.get(reverse("console") + f"?token={self.token}&view=lite")
|
||||
self.assertContains(lite, "const host = window.location.hostname;")
|
||||
self.assertContains(lite, "const path = 'novncd/';")
|
||||
full = self.client.get(reverse("console") + f"?token={self.token}&view=full")
|
||||
self.assertContains(full, "host: window.location.hostname,")
|
||||
self.assertContains(full, "encrypt: https,")
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_console_xss_protection_query_parameters(self, mock_wvm):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
# pylint: disable=no-name-in-module,no-member
|
||||
import re
|
||||
|
||||
from appsettings.settings import app_settings
|
||||
from console import tokens
|
||||
|
|
@ -112,20 +111,6 @@ def console(request):
|
|||
console_websocket_port = None
|
||||
console_passwd = None
|
||||
|
||||
ws_public_port = getattr(settings, "WS_PUBLIC_PORT", 6080)
|
||||
ws_public_host = getattr(settings, "WS_PUBLIC_HOST", None)
|
||||
ws_public_path = getattr(settings, "WS_PUBLIC_PATH", "/")
|
||||
|
||||
ws_port = console_websocket_port if console_websocket_port else ws_public_port
|
||||
ws_host = ws_public_host if ws_public_host else request.get_host()
|
||||
ws_path = ws_public_path if ws_public_path else "/"
|
||||
|
||||
if ":" in ws_host:
|
||||
ws_host = re.sub(":[0-9]+", "", ws_host)
|
||||
|
||||
if ws_path:
|
||||
ws_path = ws_path.strip("/") + "/" if ws_path.strip("/") else ""
|
||||
|
||||
if console_type and not running: # a shut-off VM has no console to connect to
|
||||
console_error = _("The VM is not running: start it to open its console.")
|
||||
response = render(request, "console-vnc-lite.html", locals())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue