1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

fix(console): a VM with a QEMU websocket port opens through novncd

A VM defined with QEMU's own VNC websocket (graphics websocket='...',
which the panel never sets) made the console page connect to that port on
the panel's host name: the port is on the compute, so the console did not
open unless the panel ran on that compute, and then it bypassed novncd and
the panel's console token. Such a VM has its ordinary VNC port too, and
novncd now reaches it there like any other VM.
This commit is contained in:
catborise 2026-10-26 21:17:20 +03:00
parent 3bd9fe63e5
commit 554e3a7995
5 changed files with 6 additions and 31 deletions

View file

@ -66,8 +66,7 @@
bindSendKeys(() => UI.rfb);
// 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
// novncd behind the panel's own address (nginx's /novncd/)
let fullPath = 'novncd/';
const token = '{{ token|escapejs }}';
if (token) {
@ -87,7 +86,7 @@
const https = window.location.protocol === 'https:';
const mandatory = {
host: window.location.hostname,
port: Number('{{ console_websocket_port|default_if_none:""|escapejs }}' || window.location.port || (https ? 443 : 80)),
port: Number(window.location.port || (https ? 443 : 80)),
path: fullPath,
encrypt: https,
};

View file

@ -149,10 +149,9 @@
bindSendKeys(() => rfb);
// 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.
// from the link: the page sends it a console token
const host = window.location.hostname;
let port = '{{ console_websocket_port|default_if_none:""|escapejs }}' || window.location.port;
let port = window.location.port;
{% if perms.instances.passwordless_console and console_passwd %}
const password = '{{ console_passwd|escapejs }}';

View file

@ -75,7 +75,6 @@ class ConsoleViewsTestCase(TestCase):
def test_console_superuser_vnc_lite(self, mock_wvm):
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_conn.get_console_passwd.return_value = "vncsecret"
mock_wvm.return_value = mock_conn
@ -96,7 +95,6 @@ class ConsoleViewsTestCase(TestCase):
def test_console_superuser_vnc_full(self, mock_wvm):
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = 5900
mock_wvm.return_value = mock_conn
self.client.force_login(self.admin_user)
@ -105,12 +103,12 @@ class ConsoleViewsTestCase(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "console-vnc-full.html")
self.assertEqual(response.context["console_websocket_port"], 5900)
# a VM with a QEMU websocket port goes through novncd too, on its VNC port
self.assertContains(response, "port: Number(window.location.port")
@patch("console.views.wvmInstance")
def test_the_lite_console_switches_scaling_from_its_menu(self, mock_wvm):
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)
for query, pressed in (("", "false"), ("&scale=true", "true")):
with self.subTest(query=query):
@ -126,7 +124,6 @@ class ConsoleViewsTestCase(TestCase):
# Only VNC is supported; a VM defined elsewhere may use another type.
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "rdp"
mock_conn.get_console_websocket_port.return_value = None
mock_wvm.return_value = mock_conn
self.client.force_login(self.admin_user)
@ -173,7 +170,6 @@ class ConsoleViewsTestCase(TestCase):
def test_console_user_assigned_instance_access(self, mock_wvm):
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_wvm.return_value = mock_conn
self.client.force_login(self.alice)
@ -189,7 +185,6 @@ class ConsoleViewsTestCase(TestCase):
# (letters and digits only: escapejs keeps them as they are in the lite view)
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_conn.get_console_passwd.return_value = "vncpw7f3a"
mock_wvm.return_value = mock_conn
permission = Permission.objects.get(codename="passwordless_console")
@ -217,7 +212,6 @@ class ConsoleViewsTestCase(TestCase):
def test_console_lite_uses_the_signed_token(self, mock_wvm):
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_wvm.return_value = mock_conn
self.client.force_login(self.admin_user)
@ -241,7 +235,6 @@ class ConsoleViewsTestCase(TestCase):
def test_the_lite_console_connects_only_to_the_servers_proxy(self, mock_wvm):
# a link naming another host would send it the signed console token
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.alice)
response = self.client.get(self.url + "?host=attacker.example&port=443&path=x")
self.assertNotContains(response, "attacker.example")
@ -253,7 +246,6 @@ class ConsoleViewsTestCase(TestCase):
def test_the_page_sets_no_token_cookie(self, mock_wvm):
# one cookie for every tab would name the last console opened
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)
response = self.client.get(self.url)
self.assertNotIn("token", response.cookies)
@ -264,7 +256,6 @@ class ConsoleViewsTestCase(TestCase):
# is for superusers and VM owners only.
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_wvm.return_value = mock_conn
perm = Permission.objects.get(codename="view_instances")
@ -291,7 +282,6 @@ class ConsoleViewsTestCase(TestCase):
# 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)
lite = self.client.get(self.url + "?view=lite")
self.assertContains(lite, "const host = window.location.hostname;")
@ -304,7 +294,6 @@ class ConsoleViewsTestCase(TestCase):
def test_console_xss_protection_query_parameters(self, mock_wvm):
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_wvm.return_value = mock_conn
self.client.force_login(self.admin_user)
@ -329,7 +318,6 @@ class ConsoleViewsTestCase(TestCase):
def test_console_password_escapejs_escaping(self, mock_wvm):
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_conn.get_console_passwd.return_value = "O'Brian<script>&123"
mock_wvm.return_value = mock_conn
@ -362,7 +350,6 @@ class ConsoleViewsTestCase(TestCase):
def test_console_full_resize_syntax_validity(self, mock_wvm):
mock_conn = MagicMock()
mock_conn.get_console_type.return_value = "vnc"
mock_conn.get_console_websocket_port.return_value = None
mock_wvm.return_value = mock_conn
self.client.force_login(self.admin_user)

View file

@ -54,7 +54,6 @@ def console(request, compute_id, uuid):
uuid=instance.uuid,
)
console_type = conn.get_console_type()
console_websocket_port = conn.get_console_websocket_port()
console_passwd = conn.get_console_passwd()
running = conn.instance.isActive()
# novncd reaches a console that listens on its host's loopback only
@ -63,7 +62,6 @@ def console(request, compute_id, uuid):
except (libvirtError, ValueError, IndexError, Instance.DoesNotExist):
running = unreachable = False
console_type = None
console_websocket_port = None
console_passwd = None
if console_type and not running: # a shut-off VM has no console to connect to

View file

@ -1368,14 +1368,6 @@ class wvmInstance(wvmConnect):
)
return port
def get_console_websocket_port(self):
console_type = self.get_console_type()
websocket_port = util.get_xml_path(
self._XMLDesc(0),
"/domain/devices/graphics[@type='%s']/@websocket" % console_type,
)
return websocket_port
def get_console_passwd(self, config=False):
return util.get_xml_path(
self._XMLDesc(PERSISTENT_XML if config else VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@passwd"