diff --git a/console/templates/console-vnc-lite.html b/console/templates/console-vnc-lite.html
index 7da37b2a..63d10f70 100755
--- a/console/templates/console-vnc-lite.html
+++ b/console/templates/console-vnc-lite.html
@@ -12,14 +12,6 @@
Connect parameters are provided in query string:
http://example.com/?scale=true (the proxy host, port and path come from the server)
-->
-
WebVirtCloud - noVNC - Lite
-
-
-
-
-
-
@@ -114,26 +106,6 @@
document.getElementById('noVNC_status').textContent = text;
}
- // 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('machineShutdownButton', machineShutdown);
bindKeyAction('machineRebootButton', machineReboot);
bindKeyAction('machineResetButton', machineReset);
@@ -153,10 +125,12 @@
const host = window.location.hostname;
let port = window.location.port;
+ // the VNC password, for a user who may open the console without it;
+ // otherwise the server asks for it (credentialsrequired)
{% if perms.instances.passwordless_console and console_passwd %}
- const password = '{{ console_passwd|escapejs }}';
+ const credentials = { password: '{{ console_passwd|escapejs }}' };
{% else %}
- const password = readQueryVariable('password');
+ const credentials = {};
{% endif %}
const path = 'novncd/';
@@ -190,7 +164,7 @@
// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('noVNC_container'), url,
- { credentials: { password: password } });
+ { credentials: credentials });
// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
@@ -208,6 +182,7 @@
{% endblock %}
{% block navbarmenu %}
+{% include "console-menu.html" %}
{% trans "Scale" %}
diff --git a/console/templates/console-xterm.html b/console/templates/console-xterm.html
index 8bac9384..ec3047c8 100644
--- a/console/templates/console-xterm.html
+++ b/console/templates/console-xterm.html
@@ -3,7 +3,6 @@
{% load static %}
{% block head %}
-
WebVirtCloud - XTerm
@@ -14,6 +13,8 @@
{% endblock %}
+{% block navbarmenu %}{% include "console-menu.html" with serial=True %}{% endblock %}
+
{% block content %}
Status: connecting...
diff --git a/console/tests/test_views.py b/console/tests/test_views.py
index 10c08dec..9be41e6b 100644
--- a/console/tests/test_views.py
+++ b/console/tests/test_views.py
@@ -131,7 +131,7 @@ class ConsoleViewsTestCase(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, "console-vnc-lite.html")
+ self.assertTemplateUsed(response, "console-error.html")
self.assertIn("'rdp' is not supported", str(response.context["console_error"]))
@override_settings(SERIAL_CONSOLE_ENABLED=True)
@@ -204,9 +204,32 @@ class ConsoleViewsTestCase(TestCase):
url = self.url
response = self.client.get(url)
- # Non-permitted user attempting to view other user's instance
+ # Non-permitted user attempting to view other user's instance: the
+ # console's own error page, without the VM's name or a console token
self.assertEqual(response.status_code, 403)
+ self.assertTemplateUsed(response, "console-error.html")
self.assertIn("permission", response.content.decode("utf-8").lower())
+ self.assertNotContains(response, "vm-alpha", status_code=403)
+ self.assertNotIn("token", response.context)
+
+ @override_settings(SERIAL_CONSOLE_ENABLED=False)
+ @patch("console.views.wvmInstance")
+ def test_a_console_that_cannot_open_gets_no_console_client(self, mock_wvm):
+ # the page says why; it neither connects to novncd nor offers keys
+ conn = mock_wvm.return_value
+ conn.instance.isActive.return_value = 1
+ self.client.force_login(self.admin_user)
+ for console_type, running in (("vnc", 0), ("pty", 1), ("rdp", 1), (None, 1)):
+ with self.subTest(console_type=console_type, running=running):
+ conn.get_console_type.return_value = console_type
+ conn.instance.isActive.return_value = running
+ response = self.client.get(self.url)
+ self.assertTemplateUsed(response, "console-error.html")
+ self.assertNotIn("token", response.context)
+ page = html.fromstring(response.content)
+ self.assertEqual(page.xpath("//script[@type='module'] | //*[@id='dropdownMenuLink']"), [])
+ self.assertEqual(len(page.xpath("//div[@role='alert']")), 1)
+ self.assertEqual(page.findtext(".//title"), "vm-alpha - Console")
@patch("console.views.wvmInstance")
def test_console_lite_uses_the_signed_token(self, mock_wvm):
@@ -220,7 +243,6 @@ class ConsoleViewsTestCase(TestCase):
signed = response.context["token"]
self.assertEqual(TimestampSigner(salt="console.novnc").unsign(signed),
f"{self.compute.id}:{self.instance.uuid}:{self.admin_user.id}")
- self.assertNotContains(response, "readQueryVariable('token'")
self.assertContains(response, f"const token = '{escapejs(signed)}'")
@patch("console.views.wvmInstance")
@@ -238,8 +260,8 @@ class ConsoleViewsTestCase(TestCase):
self.client.force_login(self.alice)
response = self.client.get(self.url + "?host=attacker.example&port=443&path=x")
self.assertNotContains(response, "attacker.example")
- for name in ("host", "port", "path"):
- self.assertNotContains(response, f"readQueryVariable('{name}'")
+ # nor a password from it: a link would leave it in history and logs
+ self.assertNotContains(response, "readQueryVariable")
self.assertContains(response, "const host = window.location.hostname;")
@patch("console.views.wvmInstance")
@@ -394,7 +416,7 @@ class ConsoleViewsTestCase(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, "console-vnc-lite.html")
+ self.assertTemplateUsed(response, "console-error.html")
self.assertIsNotNone(response.context["console_error"])
def test_an_unknown_vm_gives_the_superuser_an_error_page(self):
@@ -402,10 +424,19 @@ class ConsoleViewsTestCase(TestCase):
url = reverse("console", args=[self.compute.id, "no-such-uuid"])
response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, "console-vnc-lite.html")
+ self.assertEqual(response.status_code, 403)
+ self.assertTemplateUsed(response, "console-error.html")
self.assertIsNotNone(response.context["console_error"])
+ @override_settings(SERIAL_CONSOLE_ENABLED=True)
+ @patch("console.views.wvmInstance")
+ def test_the_serial_console_offers_only_the_keys_a_terminal_takes(self, mock_wvm):
+ mock_wvm.return_value.get_console_type.return_value = "pty"
+ self.client.force_login(self.admin_user)
+ page = html.fromstring(self.client.get(self.url).content)
+ keys = page.xpath("//*[@aria-labelledby='dropdownMenuLink']//a/@id")
+ self.assertEqual(sorted(keys), ["ctrlbackslash", "ctrlc", "ctrld", "ctrlz", "sendesc", "sendtab"])
+
@patch("console.views.wvmInstance")
def test_console_send_keys_menu_elements(self, mock_wvm):
mock_conn = MagicMock()
diff --git a/console/views.py b/console/views.py
index 007bf093..a3f7a223 100644
--- a/console/views.py
+++ b/console/views.py
@@ -3,7 +3,6 @@
from appsettings.settings import app_settings
from console import tokens
from django.conf import settings
-from django.http.response import HttpResponseForbidden
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from instances.models import Instance
@@ -22,30 +21,22 @@ def to_bool(val, default=False):
def console(request, compute_id, uuid):
"""The console page of the VM with this UUID on this compute. The page's
- query gives the view (lite or full) and its display options."""
- console_error = None
- token = ""
+ query gives the view (lite or full) and its display options. A console
+ that cannot open gets a page that says why, and no console client."""
view_type = "full" if request.GET.get("view", "").strip().lower() == "full" else "lite"
view_only = to_bool(request.GET.get("view_only"), default=to_bool(app_settings.CONSOLE_VIEW_ONLY))
scale = to_bool(request.GET.get("scale"), default=to_bool(app_settings.CONSOLE_SCALE))
resize_session = to_bool(request.GET.get("resize_session"), default=to_bool(app_settings.CONSOLE_RESIZE_SESSION))
clip_viewport = to_bool(request.GET.get("clip_viewport"), default=to_bool(app_settings.CONSOLE_CLIP_VIEWPORT))
+ instance = Instance.objects.filter(compute_id=compute_id, uuid=uuid).first()
+ # Same answer for "no such VM" and "not allowed", so the response
+ # does not reveal whether a VM exists.
+ if instance is None or not can_open_console(request.user, instance):
+ console_error = _("User does not have permission to access console or host/instance not exist")
+ return render(request, "console-error.html", {"console_error": console_error}, status=403)
+
try:
- instance = Instance.objects.filter(compute_id=compute_id, uuid=uuid).first()
- if instance is None and request.user.is_superuser:
- raise Instance.DoesNotExist
- # Same answer for "no such VM" and "not allowed", so the response
- # does not reveal whether a VM exists.
- if instance is None or not can_open_console(request.user, instance):
- console_error = _(
- "User does not have permission to access console or host/instance not exist"
- )
- return HttpResponseForbidden(console_error)
-
- # Generate secure signed token for noVNC daemon
- token = tokens.SIGNER.sign(f"{compute_id}:{uuid}:{request.user.id}")
-
conn = wvmInstance(
instance.compute.hostname,
instance.compute.login,
@@ -62,40 +53,36 @@ def console(request, compute_id, uuid):
unreachable = instance.compute.type in (CONN_TCP, CONN_TLS) and bool(
conn.get_console_socket() or is_loopback(conn.get_console_listener_addr())
)
- except (libvirtError, ValueError, IndexError, Instance.DoesNotExist):
+ except (libvirtError, ValueError, IndexError):
running = unreachable = False
console_type = None
console_passwd = None
- if console_type and not running: # a shut-off VM has no console to connect to
+ if console_type is None:
+ console_error = _("Fail to get console. Please check the console configuration of your VM.")
+ elif 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())
elif console_type == "vnc" and unreachable:
console_error = _(
"This console listens only on its host, which the panel reaches over TCP or TLS. "
"Let the console listen on all interfaces in the VM's console settings, "
"or add the host as an SSH host."
)
- response = render(request, "console-vnc-lite.html", locals())
- elif console_type == "vnc":
- console_page = "console-vnc-" + view_type + ".html"
- response = render(request, console_page, locals())
elif console_type == "pty" and not settings.SERIAL_CONSOLE_ENABLED:
console_error = _(
"Serial console is disabled on this server. "
"Please switch VM graphics to VNC or ask an administrator."
)
- response = render(request, "console-vnc-lite.html", locals())
- elif console_type == "pty":
- response = render(request, "console-xterm.html", locals())
+ elif console_type not in ("vnc", "pty"):
+ console_error = _(
+ "Console type '%(type)s' is not supported. Please switch the VM graphics to VNC."
+ ) % {"type": console_type}
else:
- if console_type is None:
- console_error = _(
- "Fail to get console. Please check the console configuration of your VM."
- )
- else:
- console_error = _(
- "Console type '%(type)s' is not supported. Please switch the VM graphics to VNC."
- ) % {"type": console_type}
- response = render(request, "console-vnc-lite.html", locals())
- return response
+ console_error = None
+ if console_error:
+ return render(request, "console-error.html", {"instance": instance, "console_error": console_error})
+
+ # the console daemons' credential for this page
+ token = tokens.SIGNER.sign(f"{compute_id}:{uuid}:{request.user.id}")
+ console_page = "console-xterm.html" if console_type == "pty" else "console-vnc-" + view_type + ".html"
+ return render(request, console_page, locals())