1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 11:46:49 +00:00
webvirtcloud/console/novncd
catborise 1a6dcc3e54 refactor(console): novncd keeps only the websockify handler it runs
- The fallbacks for websockify releases without ProxyRequestHandler and
  for noVNC's old wsproxy are gone (websockify 0.13 is pinned), with the
  Nova names, the per-connection mode switch and the traffic legend.
- The certificate option is gone: nginx ends TLS and novncd speaks plain
  websockets on the loopback.
- The mixin is ConsoleClient and the handler ConsoleProxyHandler; the
  CLI test that tested a copy of the parser instead of novncd's is
  removed (novncd's own parser is tested).
2026-10-26 20:41:46 +03:00

182 lines
6.5 KiB
Python
Executable file

#!/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 ipaddress
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 close_tunnel, open_tunnel, tunnel_target # noqa: E402
from vrtManager.connection import CONN_SOCKET, CONN_SSH # 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
try:
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()
except Exception as e:
logging.error("Fail to retrieve console connection infos: %s", e)
raise
compute = instance.compute
return compute.hostname, compute.login, compute.type, console_host, console_port, console_socket
WILDCARD = (None, "", "0.0.0.0", "::")
def is_loopback(address):
"""127.0.0.0/8, ::1 or localhost: reachable only on the compute itself."""
try:
return ipaddress.ip_address(address).is_loopback
except ValueError:
return address == "localhost"
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:
self.msg("No console token in the websocket URL")
return
try:
connhost, connuser, conntype, console_host, console_port, console_socket = get_connection_infos(token)
except Exception as e:
self.msg("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 = close_tunnel(tunnel)
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()
FORMAT = "%(asctime)s - %(name)s - %(levelname)s : %(message)s"
if options.debug:
logging.basicConfig(level=logging.DEBUG, format=FORMAT)
options.verbose = True
elif options.verbose:
logging.basicConfig(level=logging.INFO, format=FORMAT)
else:
logging.basicConfig(level=logging.WARNING, format=FORMAT)
# 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",
).start_server()