diff --git a/console/novncd b/console/novncd
index c4f3a9a1..b8c81006 100755
--- a/console/novncd
+++ b/console/novncd
@@ -182,19 +182,22 @@ class CompatibilityMixIn(object):
cnx_debug_msg += "- console_socket : '%s'\n" % console_socket
logging.debug(cnx_debug_msg)
+ tunnel = None
if console_socket and conntype == CONN_SOCKET:
# Local socket on local host
self.msg("Try to open local socket %s" % console_socket)
tsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
tsock.connect(console_socket)
+ elif conntype == CONN_SOCKET and re.match(r"^127\.", console_host):
+ # libvirt runs on this host: its loopback is ours
+ self.msg("connecting to: %s:%s" % (console_host, console_port))
+ tsock = socket_factory(console_host, console_port, connect=True)
elif console_socket or re.match(r"^127\.", console_host):
# Need tunnel to physical host
if conntype != CONN_SSH:
- self.msg(
- "Need a tunnel to access console but can't mount "
- + "one because it's not a SSH host"
- )
- raise Exception(self.msg)
+ message = "Need a tunnel to access console but can't mount one because it's not a SSH host"
+ self.msg(message)
+ raise Exception(message)
try:
# generate a string with all placeholders to avoid TypeErrors
# in sprintf
@@ -231,7 +234,6 @@ class CompatibilityMixIn(object):
# Direct access
self.msg("connecting to: %s:%s" % (connhost, console_port))
tsock = socket_factory(connhost, console_port, connect=True)
- tunnel = None
if self.verbose and not daemon:
print(self.traffic_legend)
diff --git a/console/templates/console-vnc-lite.html b/console/templates/console-vnc-lite.html
index 6de2555f..c42c8c7c 100755
--- a/console/templates/console-vnc-lite.html
+++ b/console/templates/console-vnc-lite.html
@@ -10,7 +10,7 @@
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/?host=HOST&port=PORT&scale=true
+ http://example.com/?scale=true (the proxy host, port and path come from the server)
-->
WebVirtCloud - noVNC - Lite
@@ -265,10 +265,10 @@
bindKeyAction('ctrlaltf' + (i + 1), () => sendCtrlAltFN(i));
}
- // Read parameters specified in the URL query string
- // By default, use the host and port of server that served this file
- const host = readQueryVariable('host', '{{ ws_host|escapejs }}');
- let port = readQueryVariable('port', '{{ ws_port|escapejs }}');
+ // 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 }}';
@@ -276,8 +276,7 @@
const password = readQueryVariable('password');
{% endif %}
- //const path = readQueryVariable('path', 'websockify');
- const path = readQueryVariable('path', '{{ ws_path|escapejs }}');
+ const path = '{{ ws_path|escapejs }}';
// | | | | | |
// | | | Connect | | |
diff --git a/console/tests/test_views.py b/console/tests/test_views.py
index bc08d0bf..03dcd21d 100644
--- a/console/tests/test_views.py
+++ b/console/tests/test_views.py
@@ -229,6 +229,26 @@ class ConsoleViewsTestCase(TestCase):
self.assertNotContains(response, "readQueryVariable('token'")
self.assertContains(response, f"const token = '{escapejs(signed)}'")
+ @patch("console.views.wvmInstance")
+ def test_a_shut_off_vm_says_so(self, mock_wvm):
+ mock_wvm.return_value.get_console_type.return_value = "vnc"
+ mock_wvm.return_value.instance.isActive.return_value = 0
+ self.client.force_login(self.alice)
+ response = self.client.get(reverse("console") + f"?token={self.token}")
+ self.assertContains(response, "The VM is not running")
+
+ @patch("console.views.wvmInstance")
+ 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(reverse("console") + f"?token={self.token}&host=attacker.example&port=443&path=x")
+ self.assertNotContains(response, "attacker.example")
+ for name in ("host", "port", "path"):
+ self.assertNotContains(response, f"readQueryVariable('{name}'")
+ self.assertContains(response, "const host = 'testserver';")
+
@patch("console.views.wvmInstance")
def test_console_token_cookie_flags(self, mock_wvm):
# The token cookie is only read by novncd, never by JS.
@@ -539,6 +559,20 @@ class NovncdDaemonLogicTestCase(TestCase):
mock_get_info.assert_called_once_with("1-uuid-from-query")
socket_factory.assert_called_once_with("host2", 5901, connect=True)
+ @patch.object(novncd_mod, "get_connection_infos")
+ def test_a_local_socket_host_reaches_a_console_on_its_loopback(self, mock_get_info):
+ # libvirt on the panel's host, the VM's console listening on 127.0.0.1
+ mock_get_info.return_value = ("localhost", None, "", novncd_mod.CONN_SOCKET, "127.0.0.1", 5902, None)
+ handler = self.CompatibilityMixIn()
+ handler.headers = {"cookie": "token=1-uuid"}
+ handler.path = "/novncd/"
+ handler.msg = MagicMock()
+ handler.do_proxy = MagicMock()
+ handler.verbose = False
+ socket_factory = MagicMock()
+ handler._new_client(daemon=False, socket_factory=socket_factory)
+ socket_factory.assert_called_once_with("127.0.0.1", 5902, connect=True)
+
@patch.object(novncd_mod, "get_connection_infos")
def test_compatibility_mixin_missing_token_safe_exit(self, mock_get_info):
handler = self.CompatibilityMixIn()
diff --git a/console/views.py b/console/views.py
index 4dc00c5c..e5372e39 100644
--- a/console/views.py
+++ b/console/views.py
@@ -105,7 +105,9 @@ def console(request):
console_type = conn.get_console_type()
console_websocket_port = conn.get_console_websocket_port()
console_passwd = conn.get_console_passwd()
+ running = conn.instance.isActive()
except (libvirtError, ValueError, IndexError, Instance.DoesNotExist):
+ running = False
console_type = None
console_websocket_port = None
console_passwd = None
@@ -124,7 +126,10 @@ def console(request):
if ws_path:
ws_path = ws_path.strip("/") + "/" if ws_path.strip("/") else ""
- if console_type == "vnc":
+ 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())
+ 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: