mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
The xterm console never connected: socketiod expected a different token format, its worker had no compute credentials, one global console was shared by every client and its output broadcast to all of them, and the Docker image did not run socketiod at all. - console/tokens.py: one authorize() for noVNC and socketiod; the signed token is the only credential, checked at connect time. - socketiod: one pty worker per client (consolecallback with the instance id), one session per VM, a session cap, input and resize checked; the parent never opens libvirt. The engine.io same-origin check is on. - consolecallback: opens the console with the compute's credentials and exits with a message when it cannot. - Docker runs socketiod as a runit service; nginx proxies /socket.io/ with Host and the scheme for the origin check. Supervisor and systemd start it as well; WEBVIRTCLOUD_SERIAL_CONSOLE=1 enables it. - Tests: token and session unit tests, socketiod as a process, and a live test that reads SeaBIOS from a wvc-test VM on the TCP and SSH hosts.
340 lines
11 KiB
Python
Executable file
340 lines
11 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
import django
|
|
|
|
DIR_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT_PATH = os.path.abspath(os.path.join(DIR_PATH, "..", ""))
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webvirtcloud.settings")
|
|
CERT = DIR_PATH + "/cert.pem"
|
|
|
|
if ROOT_PATH not in sys.path:
|
|
sys.path.append(ROOT_PATH)
|
|
|
|
django.setup()
|
|
|
|
import re
|
|
import socket
|
|
|
|
# from six.moves import http_cookies as Cookie
|
|
from http import cookies as Cookie
|
|
import argparse
|
|
from console.sshtunnels import SSHTunnels
|
|
from vrtManager.connection import CONN_SOCKET, CONN_SSH
|
|
from webvirtcloud.settings import WS_CERT, WS_HOST, WS_PORT
|
|
|
|
|
|
def get_parser(default_host=WS_HOST, default_port=WS_PORT or 6080, default_cert=WS_CERT or CERT):
|
|
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,
|
|
)
|
|
parser.add_argument(
|
|
"-c",
|
|
"--cert",
|
|
dest="cert",
|
|
action="store",
|
|
help="Certificate file path",
|
|
default=default_cert,
|
|
)
|
|
return parser
|
|
|
|
|
|
|
|
try:
|
|
from websockify import WebSocketProxy
|
|
|
|
try:
|
|
from websockify import ProxyRequestHandler
|
|
except ImportError:
|
|
USE_HANDLER = False
|
|
else:
|
|
USE_HANDLER = True
|
|
except ImportError:
|
|
try:
|
|
from novnc.wsproxy import WebSocketProxy
|
|
except ImportError:
|
|
print("Unable to import a websockify implementation,\n please install one")
|
|
sys.exit(1)
|
|
else:
|
|
USE_HANDLER = False
|
|
|
|
|
|
def get_connection_infos(token):
|
|
from console.tokens import authorize
|
|
from vrtManager.instance import wvmInstance
|
|
|
|
try:
|
|
instance = authorize(token)
|
|
conn = wvmInstance(
|
|
instance.compute.hostname,
|
|
instance.compute.login,
|
|
instance.compute.password,
|
|
instance.compute.type,
|
|
instance.name,
|
|
)
|
|
if instance.compute.hostname.count(":"):
|
|
connhost = instance.compute.hostname.split(":")[0]
|
|
connport = instance.compute.hostname.split(":")[1]
|
|
else:
|
|
connhost = instance.compute.hostname
|
|
connport = 22
|
|
connuser = instance.compute.login
|
|
conntype = instance.compute.type
|
|
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
|
|
return (
|
|
connhost,
|
|
connport,
|
|
connuser,
|
|
conntype,
|
|
console_host,
|
|
console_port,
|
|
console_socket,
|
|
)
|
|
|
|
|
|
class CompatibilityMixIn(object):
|
|
def _new_client(self, daemon, socket_factory):
|
|
# NoVNC uses it's own convention that forward token
|
|
# from the request to a cookie header, we should check
|
|
# also for this behavior
|
|
token = None
|
|
hcookie = self.headers.get("cookie")
|
|
|
|
if hcookie:
|
|
cookie = Cookie.SimpleCookie()
|
|
for hcookie_part in hcookie.split(";"):
|
|
hcookie_part = hcookie_part.lstrip()
|
|
try:
|
|
cookie.load(hcookie_part)
|
|
except Cookie.CookieError:
|
|
# NOTE(stgleb): Do not print out cookie content
|
|
# for security reasons.
|
|
self.msg("Found malformed cookie")
|
|
else:
|
|
if "token" in cookie:
|
|
token = cookie["token"].value
|
|
|
|
# Fallback to query parameter if token not found in cookie
|
|
if not token and hasattr(self, "path"):
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
try:
|
|
parsed_url = urlparse(self.path)
|
|
query_params = parse_qs(parsed_url.query)
|
|
if "token" in query_params:
|
|
token = query_params["token"][0]
|
|
except Exception:
|
|
pass
|
|
|
|
if not token:
|
|
self.msg("No console token provided in cookie or query parameters")
|
|
return
|
|
|
|
try:
|
|
(
|
|
connhost,
|
|
connport,
|
|
connuser,
|
|
conntype,
|
|
console_host,
|
|
console_port,
|
|
console_socket,
|
|
) = get_connection_infos(token)
|
|
except Exception as e:
|
|
self.msg("Console connection denied: %s" % e)
|
|
return
|
|
|
|
cnx_debug_msg = "Connection infos :\n"
|
|
cnx_debug_msg += "- connhost : '%s'\n" % connhost
|
|
cnx_debug_msg += "- connport : '%s'\n" % connport
|
|
cnx_debug_msg += "- connuser : '%s'\n" % connuser
|
|
cnx_debug_msg += "- conntype : '%s'\n" % conntype
|
|
cnx_debug_msg += "- console_host : '%s'\n" % console_host
|
|
cnx_debug_msg += "- console_port : '%s'\n" % console_port
|
|
cnx_debug_msg += "- console_socket : '%s'\n" % console_socket
|
|
logging.debug(cnx_debug_msg)
|
|
|
|
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 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)
|
|
try:
|
|
# generate a string with all placeholders to avoid TypeErrors
|
|
# in sprintf
|
|
# https://github.com/retspen/webvirtmgr/pull/497
|
|
error_msg = "Try to open tunnel on %s@%s:%s on console %s:%s "
|
|
error_msg += "(or socket %s)"
|
|
self.msg(
|
|
error_msg
|
|
% (
|
|
connuser,
|
|
connhost,
|
|
connport,
|
|
console_host,
|
|
console_port,
|
|
console_socket,
|
|
)
|
|
)
|
|
tunnel = SSHTunnels(
|
|
connhost,
|
|
connuser,
|
|
connport,
|
|
console_host,
|
|
console_port,
|
|
console_socket,
|
|
)
|
|
fd = tunnel.open_new()
|
|
tunnel.unlock()
|
|
tsock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
|
|
except Exception as e:
|
|
self.msg("Fail to open tunnel : %s" % e)
|
|
raise
|
|
self.msg("Tunnel opened")
|
|
else:
|
|
# 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)
|
|
|
|
# Start proxying
|
|
try:
|
|
self.msg("Start proxying")
|
|
self.do_proxy(tsock)
|
|
except Exception:
|
|
if tunnel:
|
|
self.vmsg(
|
|
"%s:%s (via %s@%s:%s) : Websocket client or Target closed"
|
|
% (console_host, console_port, connuser, connhost, connport)
|
|
)
|
|
if tsock:
|
|
tsock.shutdown(socket.SHUT_RDWR)
|
|
tsock.close()
|
|
tunnel.close_all()
|
|
raise
|
|
|
|
|
|
if USE_HANDLER:
|
|
|
|
class NovaProxyRequestHandler(ProxyRequestHandler, CompatibilityMixIn):
|
|
def msg(self, *args, **kwargs):
|
|
self.log_message(*args, **kwargs)
|
|
|
|
def vmsg(self, *args, **kwargs):
|
|
if self.verbose:
|
|
self.msg(*args, **kwargs)
|
|
|
|
def new_websocket_client(self):
|
|
"""
|
|
Called after a new WebSocket connection has been established.
|
|
"""
|
|
# Setup variable for compatibility
|
|
daemon = self.server.daemon
|
|
socket_factory = self.server.socket
|
|
|
|
self._new_client(daemon, socket_factory)
|
|
|
|
else:
|
|
|
|
class NovaWebSocketProxy(WebSocketProxy, CompatibilityMixIn):
|
|
def new_client(self):
|
|
"""
|
|
Called after a new WebSocket connection has been established.
|
|
"""
|
|
# Setup variable for compatibility
|
|
daemon = self.daemon
|
|
socket_factory = self.socket
|
|
|
|
self._new_client(daemon, socket_factory)
|
|
|
|
|
|
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)
|
|
|
|
if USE_HANDLER:
|
|
# Create the WebSocketProxy with NovaProxyRequestHandler handler
|
|
server = WebSocketProxy(
|
|
RequestHandlerClass=NovaProxyRequestHandler,
|
|
listen_host=options.host,
|
|
listen_port=options.port,
|
|
source_is_ipv6=False,
|
|
verbose=options.verbose,
|
|
cert=options.cert,
|
|
key=None,
|
|
ssl_only=False,
|
|
daemon=False,
|
|
record=False,
|
|
web=False,
|
|
traffic=False,
|
|
target_host="ignore",
|
|
target_port="ignore",
|
|
wrap_mode="exit",
|
|
wrap_cmd=None,
|
|
)
|
|
else:
|
|
# Create the NovaWebSockets proxy
|
|
server = NovaWebSocketProxy(
|
|
listen_host=options.host,
|
|
listen_port=options.port,
|
|
source_is_ipv6=False,
|
|
verbose=options.verbose,
|
|
cert=options.cert,
|
|
key=None,
|
|
ssl_only=False,
|
|
daemon=False,
|
|
record=False,
|
|
web=False,
|
|
target_host="ignore",
|
|
target_port="ignore",
|
|
wrap_mode="exit",
|
|
wrap_cmd=None,
|
|
)
|
|
server.start_server()
|