#!/usr/bin/env python3 # The VNC console proxy: nginx passes /novncd/ websockets to it; each one # carries a console token and is joined to that VM's VNC server, directly, # through an SSH tunnel or a local Unix socket. import argparse import logging import os import socket import sys from urllib.parse import parse_qs, urlparse import django ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) if ROOT_PATH not in sys.path: sys.path.append(ROOT_PATH) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webvirtcloud.settings") django.setup() from websockify import ProxyRequestHandler, WebSocketProxy # noqa: E402 from console.sshtunnels import open_tunnel, tunnel_target # noqa: E402 from vrtManager.connection import CONN_SOCKET, CONN_SSH # noqa: E402 from vrtManager.util import is_loopback # noqa: E402 from webvirtcloud.settings import WS_HOST, WS_PORT # noqa: E402 def get_parser(default_host=WS_HOST, default_port=WS_PORT or 6080): parser = argparse.ArgumentParser(description="WebVirtCloud noVNC daemon") parser.add_argument( "-v", "--verbose", dest="verbose", action="store_true", help="Verbose mode", default=False, ) parser.add_argument( "-d", "--debug", dest="debug", action="store_true", help="Debug mode", default=False ) parser.add_argument( "-H", "--host", dest="host", action="store", help="Listen host", default=default_host ) parser.add_argument( "-p", "--port", dest="port", action="store", type=int, help="Listen port", default=default_port, ) return parser def get_connection_infos(token): from console.tokens import authorize from vrtManager.instance import wvmInstance instance, _user = authorize(token) conn = wvmInstance( instance.compute.hostname, instance.compute.login, instance.compute.password, instance.compute.type, instance.name, uuid=instance.uuid, # the VM access was checked for, whatever its name now ) console_host = conn.get_console_listener_addr() console_port = conn.get_console_port() console_socket = conn.get_console_socket() compute = instance.compute return compute.hostname, compute.login, compute.type, console_host, console_port, console_socket WILDCARD = (None, "", "0.0.0.0", "::") class ConsoleClient: """What a console websocket is joined to; ConsoleProxyHandler is the websockify handler that runs it.""" def _new_client(self, socket_factory): # The page puts its own console's token on its websocket URL: a cookie # is shared by the browser's tabs and would name the last console opened token = parse_qs(urlparse(self.path).query).get("token", [None])[0] if not token: logging.warning("Console connection without a token in its websocket URL") return try: connhost, connuser, conntype, console_host, console_port, console_socket = get_connection_infos(token) except Exception as e: logging.warning("Console connection denied: %s", e) return logging.debug( "Console of %s@%s (type %s): %s:%s, socket %s", connuser, connhost, conntype, console_host, console_port, console_socket, ) tunnel = None loopback = is_loopback(console_host) if conntype == CONN_SSH: # the console travels inside the SSH connection, encrypted, # whatever address it listens on target = tunnel_target(console_host, console_port, console_socket) self.msg("Opening an SSH tunnel to %s@%s, console %s" % (connuser, connhost, target)) tsock, tunnel = open_tunnel(connhost, connuser, target) elif 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 loopback: # 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 loopback: message = "The console listens only on its compute; it is reached through SSH computes only" self.msg(message) raise Exception(message) else: # the address the console listens on, or the compute's for a wildcard address = connhost if console_host in WILDCARD else console_host self.msg("connecting to: %s:%s" % (address, console_port)) tsock = socket_factory(address, console_port, connect=True) try: self.msg("Start proxying") self.do_proxy(tsock) finally: tsock.close() if tunnel: error = tunnel.close() if error: logging.warning("SSH tunnel to %s@%s failed: %s", connuser, connhost, error) class ConsoleProxyHandler(ProxyRequestHandler, ConsoleClient): def msg(self, *args, **kwargs): self.log_message(*args, **kwargs) def new_websocket_client(self): self._new_client(self.server.socket) if __name__ == "__main__": options = get_parser().parse_args() # to stderr (the journal, docker logs), not the panel's own log file: # each process rotating that file would lose lines. force: Django's # settings already gave the root logger that file. level = logging.DEBUG if options.debug else logging.INFO if options.verbose else logging.WARNING logging.basicConfig(level=level, format="%(levelname)s %(name)s: %(message)s", force=True) options.verbose = options.verbose or options.debug # nginx ends TLS: novncd speaks plain websockets on the loopback WebSocketProxy( RequestHandlerClass=ConsoleProxyHandler, listen_host=options.host, listen_port=options.port, verbose=options.verbose, daemon=False, web=False, target_host="ignore", target_port="ignore", # a ping while the VM's screen stays the same: nginx, and a proxy on # the way, end a websocket that sends nothing for 60 seconds heartbeat=30, ).start_server()