mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
ssh's error output went to a pipe that nobody read while the console ran: once it filled, ssh stopped and the console froze. It now goes to a temporary file, read when the tunnel closes. Closing waits five seconds for ssh to end and then kills it, instead of waiting for ever. The Tunnel holds the ssh process and its error file together.
176 lines
6.4 KiB
Python
Executable file
176 lines
6.4 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 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
|
|
|
|
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", "::")
|
|
|
|
|
|
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:
|
|
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",
|
|
).start_server()
|