diff --git a/install.sh b/install.sh index 795fd40c86..a058ae33c5 100755 --- a/install.sh +++ b/install.sh @@ -219,6 +219,7 @@ fi SHARE_DIRS="$SHARE_LOCATION/examples \ $SHARE_LOCATION/websockify \ + $SHARE_LOCATION/websockify/websockify \ $SHARE_LOCATION/esx-fw-vnc \ $SHARE_LOCATION/oneprovision" @@ -477,7 +478,8 @@ INSTALL_FILES=( NETWORK_OVSWITCH_VXLAN_FILES:$VAR_LOCATION/remotes/vnm/ovswitch_vxlan NETWORK_VCENTER_FILES:$VAR_LOCATION/remotes/vnm/vcenter EXAMPLE_SHARE_FILES:$SHARE_LOCATION/examples - WEBSOCKIFY_SHARE_FILES:$SHARE_LOCATION/websockify + WEBSOCKIFY_SHARE_RUN_FILES:$SHARE_LOCATION/websockify + WEBSOCKIFY_SHARE_MODULE_FILES:$SHARE_LOCATION/websockify/websockify ESX_FW_VNC_SHARE_FILES:$SHARE_LOCATION/esx-fw-vnc INSTALL_GEMS_SHARE_FILES:$SHARE_LOCATION ONETOKEN_SHARE_FILE:$SHARE_LOCATION @@ -1530,9 +1532,12 @@ EXAMPLE_SHARE_FILES="share/examples/vm.template \ # Files required to interact with the websockify server #------------------------------------------------------------------------------- -WEBSOCKIFY_SHARE_FILES="share/websockify/websocketproxy.py \ - share/websockify/websocket.py \ - share/websockify/websockify" +WEBSOCKIFY_SHARE_RUN_FILES="share/websockify/run" +WEBSOCKIFY_SHARE_MODULE_FILES="share/websockify/websockify/__init__.py \ + share/websockify/websockify/auth_plugins.py \ + share/websockify/websockify/token_plugins.py \ + share/websockify/websockify/websocket.py \ + share/websockify/websockify/websocketproxy.py" #------------------------------------------------------------------------------- # Installation packages for ESX hosts to enable VNC ports diff --git a/share/websockify/run b/share/websockify/run new file mode 100755 index 0000000000..9ad217c04c --- /dev/null +++ b/share/websockify/run @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +import websockify + +websockify.websocketproxy.websockify_init() diff --git a/share/websockify/websockify b/share/websockify/websockify deleted file mode 120000 index 9558147b09..0000000000 --- a/share/websockify/websockify +++ /dev/null @@ -1 +0,0 @@ -websocketproxy.py \ No newline at end of file diff --git a/share/websockify/websockify/__init__.py b/share/websockify/websockify/__init__.py new file mode 100644 index 0000000000..37a6f47b45 --- /dev/null +++ b/share/websockify/websockify/__init__.py @@ -0,0 +1,2 @@ +from websockify.websocket import * +from websockify.websocketproxy import * diff --git a/share/websockify/websockify/auth_plugins.py b/share/websockify/websockify/auth_plugins.py new file mode 100644 index 0000000000..93f1385504 --- /dev/null +++ b/share/websockify/websockify/auth_plugins.py @@ -0,0 +1,83 @@ +class BasePlugin(object): + def __init__(self, src=None): + self.source = src + + def authenticate(self, headers, target_host, target_port): + pass + + +class AuthenticationError(Exception): + def __init__(self, log_msg=None, response_code=403, response_headers={}, response_msg=None): + self.code = response_code + self.headers = response_headers + self.msg = response_msg + + if log_msg is None: + log_msg = response_msg + + super(AuthenticationError, self).__init__('%s %s' % (self.code, log_msg)) + + +class InvalidOriginError(AuthenticationError): + def __init__(self, expected, actual): + self.expected_origin = expected + self.actual_origin = actual + + super(InvalidOriginError, self).__init__( + response_msg='Invalid Origin', + log_msg="Invalid Origin Header: Expected one of " + "%s, got '%s'" % (expected, actual)) + + +class BasicHTTPAuth(object): + """Verifies Basic Auth headers. Specify src as username:password""" + + def __init__(self, src=None): + self.src = src + + def authenticate(self, headers, target_host, target_port): + import base64 + auth_header = headers.get('Authorization') + if auth_header: + if not auth_header.startswith('Basic '): + raise AuthenticationError(response_code=403) + + try: + user_pass_raw = base64.b64decode(auth_header[6:]) + except TypeError: + raise AuthenticationError(response_code=403) + + try: + # http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication + user_pass_as_text = user_pass_raw.decode('ISO-8859-1') + except UnicodeDecodeError: + raise AuthenticationError(response_code=403) + + user_pass = user_pass_as_text.split(':', 1) + if len(user_pass) != 2: + raise AuthenticationError(response_code=403) + + if not self.validate_creds(*user_pass): + raise AuthenticationError(response_code=403) + + else: + raise AuthenticationError(response_code=401, + response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'}) + + def validate_creds(self, username, password): + if '%s:%s' % (username, password) == self.src: + return True + else: + return False + +class ExpectOrigin(object): + def __init__(self, src=None): + if src is None: + self.source = [] + else: + self.source = src.split() + + def authenticate(self, headers, target_host, target_port): + origin = headers.get('Origin', None) + if origin is None or origin not in self.source: + raise InvalidOriginError(expected=self.source, actual=origin) diff --git a/share/websockify/websockify/token_plugins.py b/share/websockify/websockify/token_plugins.py new file mode 100644 index 0000000000..92494eb03f --- /dev/null +++ b/share/websockify/websockify/token_plugins.py @@ -0,0 +1,83 @@ +import os + +class BasePlugin(object): + def __init__(self, src): + self.source = src + + def lookup(self, token): + return None + + +class ReadOnlyTokenFile(BasePlugin): + # source is a token file with lines like + # token: host:port + # or a directory of such files + def __init__(self, *args, **kwargs): + super(ReadOnlyTokenFile, self).__init__(*args, **kwargs) + self._targets = None + + def _load_targets(self): + if os.path.isdir(self.source): + cfg_files = [os.path.join(self.source, f) for + f in os.listdir(self.source)] + else: + cfg_files = [self.source] + + self._targets = {} + for f in cfg_files: + for line in [l.strip() for l in open(f).readlines()]: + if line and not line.startswith('#'): + tok, target = line.split(': ') + self._targets[tok] = target.strip().rsplit(':', 1) + + def lookup(self, token): + if self._targets is None: + self._load_targets() + + if token in self._targets: + return self._targets[token] + else: + return None + + +# the above one is probably more efficient, but this one is +# more backwards compatible (although in most cases +# ReadOnlyTokenFile should suffice) +class TokenFile(ReadOnlyTokenFile): + # source is a token file with lines like + # token: host:port + # or a directory of such files + def lookup(self, token): + self._load_targets() + + return super(TokenFile, self).lookup(token) + + +class BaseTokenAPI(BasePlugin): + # source is a url with a '%s' in it where the token + # should go + + # we import things on demand so that other plugins + # in this file can be used w/o unecessary dependencies + + def process_result(self, resp): + return resp.text.split(':') + + def lookup(self, token): + import requests + + resp = requests.get(self.source % token) + + if resp.ok: + return self.process_result(resp) + else: + return None + + +class JSONTokenApi(BaseTokenAPI): + # source is a url with a '%s' in it where the token + # should go + + def process_result(self, resp): + resp_json = resp.json() + return (resp_json['host'], resp_json['port']) diff --git a/share/websockify/websocket.py b/share/websockify/websockify/websocket.py similarity index 89% rename from share/websockify/websocket.py rename to share/websockify/websockify/websocket.py index d161f648c5..274a0047fc 100644 --- a/share/websockify/websocket.py +++ b/share/websockify/websockify/websocket.py @@ -104,6 +104,8 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): self.handler_id = getattr(server, "handler_id", False) self.file_only = getattr(server, "file_only", False) self.traffic = getattr(server, "traffic", False) + self.auto_pong = getattr(server, "auto_pong", False) + self.strict_mode = getattr(server, "strict_mode", True) self.logger = getattr(server, "logger", None) if self.logger is None: @@ -111,6 +113,9 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): SimpleHTTPRequestHandler.__init__(self, req, addr, server) + def log_message(self, format, *args): + self.logger.info("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), format % args)) + @staticmethod def unmask(buf, hlen, plen): pstart = hlen + 4 @@ -118,20 +123,24 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): if numpy: b = c = s2b('') if plen >= 4: - mask = numpy.frombuffer(buf, dtype=numpy.dtype('') + mask = numpy.frombuffer(buf, dtype, offset=hlen, count=1) + data = numpy.frombuffer(buf, dtype, offset=pstart, + count=int(plen / 4)) #b = numpy.bitwise_xor(data, mask).data b = numpy.bitwise_xor(data, mask).tostring() if plen % 4: #self.msg("Partial unmask") - mask = numpy.frombuffer(buf, dtype=numpy.dtype('B'), - offset=hlen, count=(plen % 4)) - data = numpy.frombuffer(buf, dtype=numpy.dtype('B'), - offset=pend - (plen % 4), + dtype=numpy.dtype('B') + if sys.byteorder == 'big': + dtype = dtype.newbyteorder('>') + mask = numpy.frombuffer(buf, dtype, offset=hlen, count=(plen % 4)) + data = numpy.frombuffer(buf, dtype, + offset=pend - (plen % 4), count=(plen % 4)) c = numpy.bitwise_xor(data, mask).tostring() return b + c else: @@ -172,7 +181,7 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): return header + buf, len(header), 0 @staticmethod - def decode_hybi(buf, base64=False, logger=None): + def decode_hybi(buf, base64=False, logger=None, strict=True): """ Decode HyBi style WebSocket packets. Returns: {'fin' : 0_or_1, @@ -238,6 +247,10 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): f['length']) else: logger.debug("Unmasked frame: %s" % repr(buf)) + + if strict: + raise WebSocketRequestHandler.CClose(1002, "The client sent an unmasked frame.") + f['payload'] = buf[(f['hlen'] + f['masked'] * 4):full_len] if base64 and f['opcode'] in [1, 2]: @@ -346,7 +359,8 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): while buf: frame = self.decode_hybi(buf, base64=self.base64, - logger=self.logger) + logger=self.logger, + strict=self.strict_mode) #self.msg("Received buf: %s, frame: %s", repr(buf), frame) if frame['payload'] == None: @@ -360,6 +374,15 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): closed = {'code': frame['close_code'], 'reason': frame['close_reason']} break + elif self.auto_pong and frame['opcode'] == 0x9: # ping + self.print_traffic("} ping %s\n" % + repr(frame['payload'])) + self.send_pong(frame['payload']) + return [], False + elif frame['opcode'] == 0xA: # pong + self.print_traffic("} pong %s\n" % + repr(frame['payload'])) + return [], False self.print_traffic("}") @@ -388,10 +411,20 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): def send_close(self, code=1000, reason=''): """ Send a WebSocket orderly close frame. """ - msg = pack(">H%ds" % len(reason), code, reason) + msg = pack(">H%ds" % len(reason), code, s2b(reason)) buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False) self.request.send(buf) + def send_pong(self, data=''): + """ Send a WebSocket pong frame. """ + buf, h, t = self.encode_hybi(s2b(data), opcode=0x0A, base64=False) + self.request.send(buf) + + def send_ping(self, data=''): + """ Send a WebSocket ping frame. """ + buf, h, t = self.encode_hybi(s2b(data), opcode=0x09, base64=False) + self.request.send(buf) + def do_websocket_handshake(self): h = self.headers @@ -444,9 +477,13 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): """Upgrade a connection to Websocket, if requested. If this succeeds, new_websocket_client() will be called. Otherwise, False is returned. """ + if (self.headers.get('upgrade') and self.headers.get('upgrade').lower() == 'websocket'): + # ensure connection is authorized, and determine the target + self.validate_connection() + if not self.do_websocket_handshake(): return False @@ -519,6 +556,10 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): """ Do something with a WebSockets client connection. """ raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded") + def validate_connection(self): + """ Ensure that the connection is a valid connection, and set the target. """ + pass + def do_HEAD(self): if self.only_upgrade: self.send_error(405, "Method Not Allowed") @@ -567,7 +608,7 @@ class WebSocketServer(object): file_only=False, run_once=False, timeout=0, idle_timeout=0, traffic=False, tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None, - tcp_keepintvl=None): + tcp_keepintvl=None, auto_pong=False, strict_mode=True): # settings self.RequestHandlerClass = RequestHandlerClass @@ -581,6 +622,8 @@ class WebSocketServer(object): self.timeout = timeout self.idle_timeout = idle_timeout self.traffic = traffic + self.file_only = file_only + self.strict_mode = strict_mode self.launch_time = time.time() self.ws_connection = False @@ -592,6 +635,7 @@ class WebSocketServer(object): self.tcp_keepidle = tcp_keepidle self.tcp_keepintvl = tcp_keepintvl + self.auto_pong = auto_pong # Make paths settings absolute self.cert = os.path.abspath(cert) self.key = self.web = self.record = '' @@ -618,7 +662,10 @@ class WebSocketServer(object): self.listen_host, self.listen_port) self.msg(" - Flash security policy server") if self.web: - self.msg(" - Web server. Web root: %s", self.web) + if self.file_only: + self.msg(" - Web server (no directory listings). Web root: %s", self.web) + else: + self.msg(" - Web server. Web root: %s", self.web) if ssl: if os.path.exists(self.cert): self.msg(" - SSL/TLS support") @@ -701,6 +748,10 @@ class WebSocketServer(object): @staticmethod def daemonize(keepfd=None, chdir='/'): + + if keepfd is None: + keepfd = [] + os.umask(0) if chdir: os.chdir(chdir) @@ -723,7 +774,7 @@ class WebSocketServer(object): if maxfd == resource.RLIM_INFINITY: maxfd = 256 for fd in reversed(range(maxfd)): try: - if fd != keepfd: + if fd not in keepfd: os.close(fd) except OSError: _, exc, _ = sys.exc_info() @@ -753,7 +804,7 @@ class WebSocketServer(object): """ ready = select.select([sock], [], [], 3)[0] - + if not ready: raise self.EClose("ignoring socket not ready") # Peek, but do not read the data so that we have a opportunity @@ -761,7 +812,7 @@ class WebSocketServer(object): handshake = sock.recv(1024, socket.MSG_PEEK) #self.msg("Handshake [%s]" % handshake) - if handshake == "": + if not handshake: raise self.EClose("ignoring empty handshake") elif handshake.startswith(s2b("")): @@ -844,11 +895,14 @@ class WebSocketServer(object): raise self.Terminate() def multiprocessing_SIGCHLD(self, sig, stack): - self.vmsg('Reaping zombies, active child count is %s', len(multiprocessing.active_children())) + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers + multiprocessing.active_children() def fallback_SIGCHLD(self, sig, stack): # Reap zombies when using os.fork() (python 2.4) - self.vmsg("Got SIGCHLD, reaping zombies") + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers try: result = os.waitpid(-1, os.WNOHANG) while result[0]: @@ -858,16 +912,18 @@ class WebSocketServer(object): pass def do_SIGINT(self, sig, stack): - self.msg("Got SIGINT, exiting") + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers self.terminate() def do_SIGTERM(self, sig, stack): - self.msg("Got SIGTERM, exiting") + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers self.terminate() def top_new_client(self, startsock, address): """ Do something with a WebSockets client connection. """ - # handler process + # handler process client = None try: try: @@ -890,6 +946,18 @@ class WebSocketServer(object): # Original socket closed by caller client.close() + def get_log_fd(self): + """ + Get file descriptors for the loggers. + They should not be closed when the process is forked. + """ + descriptors = [] + for handler in self.logger.parent.handlers: + if isinstance(handler, logging.FileHandler): + descriptors.append(handler.stream.fileno()) + + return descriptors + def start_server(self): """ Daemonize if requested. Listen for for connections. Run @@ -905,7 +973,9 @@ class WebSocketServer(object): tcp_keepintvl=self.tcp_keepintvl) if self.daemon: - self.daemonize(keepfd=lsock.fileno(), chdir=self.web) + keepfd = self.get_log_fd() + keepfd.append(lsock.fileno()) + self.daemonize(keepfd=keepfd, chdir=self.web) self.started() # Some things need to happen after daemonizing @@ -1009,8 +1079,17 @@ class WebSocketServer(object): except (self.Terminate, SystemExit, KeyboardInterrupt): self.msg("In exit") + # terminate all child processes + if multiprocessing and not self.run_once: + children = multiprocessing.active_children() + + for child in children: + self.msg("Terminating child %s" % child.pid) + child.terminate() + break except Exception: + exc = sys.exc_info()[1] self.msg("handler exception: %s", str(exc)) self.vmsg("exception", exc_info=True) diff --git a/share/websockify/websocketproxy.py b/share/websockify/websockify/websocketproxy.py old mode 100644 new mode 100755 similarity index 72% rename from share/websockify/websocketproxy.py rename to share/websockify/websockify/websocketproxy.py index 7b3ec111df..81e2248f21 --- a/share/websockify/websocketproxy.py +++ b/share/websockify/websockify/websocketproxy.py @@ -11,13 +11,14 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates ''' -import signal, socket, optparse, time, os, sys, subprocess, logging +import signal, socket, optparse, time, os, sys, subprocess, logging, errno try: from socketserver import ForkingMixIn except: from SocketServer import ForkingMixIn try: from http.server import HTTPServer except: from BaseHTTPServer import HTTPServer -from select import select -import websocket +import select +from websockify import websocket +from websockify import auth_plugins as auth try: from urllib.parse import parse_qs, urlparse except: @@ -38,14 +39,33 @@ Traffic Legend: <. - Client send partial """ + def send_auth_error(self, ex): + self.send_response(ex.code, ex.msg) + self.send_header('Content-Type', 'text/html') + for name, val in ex.headers.items(): + self.send_header(name, val) + + self.end_headers() + + def validate_connection(self): + if self.server.token_plugin: + (self.server.target_host, self.server.target_port) = self.get_target(self.server.token_plugin, self.path) + + if self.server.auth_plugin: + try: + self.server.auth_plugin.authenticate( + headers=self.headers, target_host=self.server.target_host, + target_port=self.server.target_port) + except auth.AuthenticationError: + ex = sys.exc_info()[1] + self.send_auth_error(ex) + raise + def new_websocket_client(self): """ Called after a new WebSocket connection has been established. """ - # Checks if we receive a token, and look - # for a valid target for it then - if self.server.target_cfg: - (self.server.target_host, self.server.target_port) = self.get_target(self.server.target_cfg, self.path) + # Checking for a token is done in validate_connection() # Connect to the target if self.server.wrap_cmd: @@ -73,15 +93,15 @@ Traffic Legend: if tsock: tsock.shutdown(socket.SHUT_RDWR) tsock.close() - if self.verbose: + if self.verbose: self.log_message("%s:%s: Closed target", self.server.target_host, self.server.target_port) raise - def get_target(self, target_cfg, path): + def get_target(self, target_plugin, path): """ - Parses the path, extracts a token, and looks for a valid - target for that token in the configuration file(s). Sets + Parses the path, extracts a token, and looks up a target + for that token using the token plugin. Sets target_host and target_port if successful """ # The files in targets contain the lines @@ -90,32 +110,17 @@ Traffic Legend: # Extract the token parameter from url args = parse_qs(urlparse(path)[4]) # 4 is the query from url - if not args.has_key('token') or not len(args['token']): - raise self.EClose("Token not present") + if not 'token' in args or not len(args['token']): + raise self.server.EClose("Token not present") token = args['token'][0].rstrip('\n') - # target_cfg can be a single config file or directory of - # config files - if os.path.isdir(target_cfg): - cfg_files = [os.path.join(target_cfg, f) - for f in os.listdir(target_cfg)] + result_pair = target_plugin.lookup(token) + + if result_pair is not None: + return result_pair else: - cfg_files = [target_cfg] - - targets = {} - for f in cfg_files: - for line in [l.strip() for l in file(f).readlines()]: - if line and not line.startswith('#'): - ttoken, target = line.split(': ') - targets[ttoken] = target.strip() - - self.vmsg("Target config: %s" % repr(targets)) - - if targets.has_key(token): - return targets[token].split(':') - else: - raise self.EClose("Token '%s' not found" % token) + raise self.server.EClose("Token '%s' not found" % token) def do_proxy(self, target): """ @@ -126,12 +131,37 @@ Traffic Legend: tqueue = [] rlist = [self.request, target] + if self.server.heartbeat: + now = time.time() + self.heartbeat = now + self.server.heartbeat + else: + self.heartbeat = None + while True: wlist = [] + if self.heartbeat is not None: + now = time.time() + if now > self.heartbeat: + self.heartbeat = now + self.server.heartbeat + self.send_ping() + if tqueue: wlist.append(target) if cqueue or c_pend: wlist.append(self.request) - ins, outs, excepts = select(rlist, wlist, [], 1) + try: + ins, outs, excepts = select.select(rlist, wlist, [], 1) + except (select.error, OSError): + exc = sys.exc_info()[1] + if hasattr(exc, 'errno'): + err = exc.errno + else: + err = exc[0] + + if err != errno.EINTR: + raise + else: + continue + if excepts: raise Exception("Socket exception") if self.request in outs: @@ -147,7 +177,7 @@ Traffic Legend: if closed: # TODO: What about blocking on client socket? - if self.verbose: + if self.verbose: self.log_message("%s:%s: Client closed connection", self.server.target_host, self.server.target_port) raise self.CClose(closed['code'], closed['reason']) @@ -195,7 +225,11 @@ class WebSocketProxy(websocket.WebSocketServer): self.wrap_mode = kwargs.pop('wrap_mode', None) self.unix_target = kwargs.pop('unix_target', None) self.ssl_target = kwargs.pop('ssl_target', None) - self.target_cfg = kwargs.pop('target_cfg', None) + self.heartbeat = kwargs.pop('heartbeat', None) + + self.token_plugin = kwargs.pop('token_plugin', None) + self.auth_plugin = kwargs.pop('auth_plugin', None) + # Last 3 timestamps command was run self.wrap_times = [0, 0, 0] @@ -251,9 +285,9 @@ class WebSocketProxy(websocket.WebSocketServer): else: dst_string = "%s:%s" % (self.target_host, self.target_port) - if self.target_cfg: - msg = " - proxying from %s:%s to targets in %s" % ( - self.listen_host, self.listen_port, self.target_cfg) + if self.token_plugin: + msg = " - proxying from %s:%s to targets generated by %s" % ( + self.listen_host, self.listen_port, type(self.token_plugin).__name__) else: msg = " - proxying from %s:%s to %s" % ( self.listen_host, self.listen_port, dst_string) @@ -352,20 +386,69 @@ def websockify_init(): parser.add_option("--prefer-ipv6", "-6", action="store_true", dest="source_is_ipv6", help="prefer IPv6 when resolving source_addr") + parser.add_option("--libserver", action="store_true", + help="use Python library SocketServer engine") parser.add_option("--target-config", metavar="FILE", dest="target_cfg", help="Configuration file containing valid targets " "in the form 'token: host:port' or, alternatively, a " - "directory containing configuration files of this form") - parser.add_option("--libserver", action="store_true", - help="use Python library SocketServer engine") + "directory containing configuration files of this form " + "(DEPRECATED: use `--token-plugin TokenFile --token-source " + " path/to/token/file` instead)") + parser.add_option("--token-plugin", default=None, metavar="PLUGIN", + help="use the given Python class to process tokens " + "into host:port pairs") + parser.add_option("--token-source", default=None, metavar="ARG", + help="an argument to be passed to the token plugin" + "on instantiation") + parser.add_option("--auth-plugin", default=None, metavar="PLUGIN", + help="use the given Python class to determine if " + "a connection is allowed") + parser.add_option("--auth-source", default=None, metavar="ARG", + help="an argument to be passed to the auth plugin" + "on instantiation") + parser.add_option("--auto-pong", action="store_true", + help="Automatically respond to ping frames with a pong") + parser.add_option("--heartbeat", type=int, default=0, + help="send a ping to the client every HEARTBEAT seconds") + parser.add_option("--log-file", metavar="FILE", + dest="log_file", + help="File where logs will be saved") + + (opts, args) = parser.parse_args() + if opts.log_file: + opts.log_file = os.path.abspath(opts.log_file) + handler = logging.FileHandler(opts.log_file) + handler.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter("%(message)s")) + logging.getLogger(WebSocketProxy.log_prefix).addHandler(handler) + + del opts.log_file + if opts.verbose: logging.getLogger(WebSocketProxy.log_prefix).setLevel(logging.DEBUG) + if opts.token_source and not opts.token_plugin: + parser.error("You must use --token-plugin to use --token-source") + + if opts.auth_source and not opts.auth_plugin: + parser.error("You must use --auth-plugin to use --auth-source") + + + # Transform to absolute path as daemon may chdir + if opts.target_cfg: + opts.target_cfg = os.path.abspath(opts.target_cfg) + + if opts.target_cfg: + opts.token_plugin = 'TokenFile' + opts.token_source = opts.target_cfg + + del opts.target_cfg + # Sanity checks - if len(args) < 2 and not (opts.target_cfg or opts.unix_target): + if len(args) < 2 and not (opts.token_plugin or opts.unix_target): parser.error("Too few arguments") if sys.argv.count('--'): opts.wrap_cmd = args[1:] @@ -390,7 +473,7 @@ def websockify_init(): try: opts.listen_port = int(opts.listen_port) except: parser.error("Error parsing listen port") - if opts.wrap_cmd or opts.unix_target or opts.target_cfg: + if opts.wrap_cmd or opts.unix_target or opts.token_plugin: opts.target_host = None opts.target_port = None else: @@ -402,9 +485,32 @@ def websockify_init(): try: opts.target_port = int(opts.target_port) except: parser.error("Error parsing target port") - # Transform to absolute path as daemon may chdir - if opts.target_cfg: - opts.target_cfg = os.path.abspath(opts.target_cfg) + if opts.token_plugin is not None: + if '.' not in opts.token_plugin: + opts.token_plugin = ( + 'websockify.token_plugins.%s' % opts.token_plugin) + + token_plugin_module, token_plugin_cls = opts.token_plugin.rsplit('.', 1) + + __import__(token_plugin_module) + token_plugin_cls = getattr(sys.modules[token_plugin_module], token_plugin_cls) + + opts.token_plugin = token_plugin_cls(opts.token_source) + + del opts.token_source + + if opts.auth_plugin is not None: + if '.' not in opts.auth_plugin: + opts.auth_plugin = 'websockify.auth_plugins.%s' % opts.auth_plugin + + auth_plugin_module, auth_plugin_cls = opts.auth_plugin.rsplit('.', 1) + + __import__(auth_plugin_module) + auth_plugin_cls = getattr(sys.modules[auth_plugin_module], auth_plugin_cls) + + opts.auth_plugin = auth_plugin_cls(opts.auth_source) + + del opts.auth_source # Create and start the WebSockets proxy libserver = opts.libserver @@ -433,9 +539,13 @@ class LibProxyServer(ForkingMixIn, HTTPServer): self.wrap_mode = kwargs.pop('wrap_mode', None) self.unix_target = kwargs.pop('unix_target', None) self.ssl_target = kwargs.pop('ssl_target', None) - self.target_cfg = kwargs.pop('target_cfg', None) + self.token_plugin = kwargs.pop('token_plugin', None) + self.auth_plugin = kwargs.pop('auth_plugin', None) + self.heartbeat = kwargs.pop('heartbeat', None) + + self.token_plugin = None + self.auth_plugin = None self.daemon = False - self.target_cfg = None # Server configuration listen_host = kwargs.pop('listen_host', '') @@ -456,8 +566,8 @@ class LibProxyServer(ForkingMixIn, HTTPServer): if web: os.chdir(web) - - HTTPServer.__init__(self, (listen_host, listen_port), + + HTTPServer.__init__(self, (listen_host, listen_port), RequestHandlerClass) diff --git a/src/sunstone/OpenNebulaVNC.rb b/src/sunstone/OpenNebulaVNC.rb index f3908f6b2e..120f2a1745 100644 --- a/src/sunstone/OpenNebulaVNC.rb +++ b/src/sunstone/OpenNebulaVNC.rb @@ -109,7 +109,7 @@ class OpenNebulaVNC @pipe = nil @token_folder = File.join(VAR_LOCATION, opts[:token_folder_name]) - @proxy_path = File.join(SHARE_LOCATION, "websockify/websocketproxy.py") + @proxy_path = File.join(SHARE_LOCATION, "websockify/run") @proxy_port = config[:vnc_proxy_port] @proxy_ipv6 = config[:vnc_proxy_ipv6] @@ -151,10 +151,7 @@ class OpenNebulaVNC proxy_options << " -6" end - system("which python2 >/dev/null 2>&1") - python = $?.success? ? "python2" : "python" - - cmd ="#{python} #{@proxy_path} #{proxy_options} #{@proxy_port}" + cmd ="python #{@proxy_path} #{proxy_options} #{@proxy_port}" begin @logger.info { "Starting VNC proxy: #{cmd}" }