From d85b40fc35dc0847d10b6e9bd219378ed7d331bd Mon Sep 17 00:00:00 2001 From: olevole Date: Sun, 26 Feb 2023 23:47:46 +0300 Subject: [PATCH] re-added websockify --- .../utils/websockify/websockify/__init__.py | 2 + .../utils/websockify/websockify/__main__.py | 4 + .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 239 bytes .../__pycache__/__main__.cpython-39.pyc | Bin 0 -> 262 bytes .../__pycache__/auth_plugins.cpython-39.pyc | Bin 0 -> 4138 bytes .../__pycache__/websocket.cpython-39.pyc | Bin 0 -> 21821 bytes .../__pycache__/websocketproxy.cpython-39.pyc | Bin 0 -> 19405 bytes .../websocketserver.cpython-39.pyc | Bin 0 -> 4252 bytes .../websockifyserver.cpython-39.pyc | Bin 0 -> 22783 bytes .../websockify/websockify/auth_plugins.py | 102 ++ .../websockify/websockify/sysloghandler.py | 118 +++ .../websockify/websockify/token_plugins.py | 316 +++++++ .../utils/websockify/websockify/websocket.py | 874 ++++++++++++++++++ .../websockify/websockify/websocketproxy.py | 800 ++++++++++++++++ .../websockify/websockify/websocketserver.py | 110 +++ .../websockify/websockify/websockifyserver.py | 862 +++++++++++++++++ 16 files changed, 3188 insertions(+) create mode 100644 public/novnc/utils/websockify/websockify/__init__.py create mode 100644 public/novnc/utils/websockify/websockify/__main__.py create mode 100644 public/novnc/utils/websockify/websockify/__pycache__/__init__.cpython-39.pyc create mode 100644 public/novnc/utils/websockify/websockify/__pycache__/__main__.cpython-39.pyc create mode 100644 public/novnc/utils/websockify/websockify/__pycache__/auth_plugins.cpython-39.pyc create mode 100644 public/novnc/utils/websockify/websockify/__pycache__/websocket.cpython-39.pyc create mode 100644 public/novnc/utils/websockify/websockify/__pycache__/websocketproxy.cpython-39.pyc create mode 100644 public/novnc/utils/websockify/websockify/__pycache__/websocketserver.cpython-39.pyc create mode 100644 public/novnc/utils/websockify/websockify/__pycache__/websockifyserver.cpython-39.pyc create mode 100644 public/novnc/utils/websockify/websockify/auth_plugins.py create mode 100644 public/novnc/utils/websockify/websockify/sysloghandler.py create mode 100644 public/novnc/utils/websockify/websockify/token_plugins.py create mode 100644 public/novnc/utils/websockify/websockify/websocket.py create mode 100644 public/novnc/utils/websockify/websockify/websocketproxy.py create mode 100644 public/novnc/utils/websockify/websockify/websocketserver.py create mode 100644 public/novnc/utils/websockify/websockify/websockifyserver.py diff --git a/public/novnc/utils/websockify/websockify/__init__.py b/public/novnc/utils/websockify/websockify/__init__.py new file mode 100644 index 00000000..37a6f47b --- /dev/null +++ b/public/novnc/utils/websockify/websockify/__init__.py @@ -0,0 +1,2 @@ +from websockify.websocket import * +from websockify.websocketproxy import * diff --git a/public/novnc/utils/websockify/websockify/__main__.py b/public/novnc/utils/websockify/websockify/__main__.py new file mode 100644 index 00000000..8378d467 --- /dev/null +++ b/public/novnc/utils/websockify/websockify/__main__.py @@ -0,0 +1,4 @@ +import websockify + +if __name__ == '__main__': + websockify.websocketproxy.websockify_init() diff --git a/public/novnc/utils/websockify/websockify/__pycache__/__init__.cpython-39.pyc b/public/novnc/utils/websockify/websockify/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fcbe49439b3fafbe79f06a3b17b9a04d62fc175d GIT binary patch literal 239 zcmYe~<>g`kf&~YECwl|w#~=x8Ga+Jm0!@tjVNYd zXLpBTm}oEv0CB#(#1{9xeY&9dEO6WeDTbJ6*nlK4O=v!raG>Yj1qkYE2zw9oz^d^% zPa|bG54|R+S4I_A$om-eq34p7E@#?`NVBS{ sn9$Z($Fg##m0-pmjbLS=v}6C2?)hso&9K5OwQ;HCoM623kbLTsH?F5lJOBUy literal 0 HcmV?d00001 diff --git a/public/novnc/utils/websockify/websockify/__pycache__/auth_plugins.cpython-39.pyc b/public/novnc/utils/websockify/websockify/__pycache__/auth_plugins.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c109877f468d2881c18e629e996114634ef6772d GIT binary patch literal 4138 zcmbtX-EQ2*73T17xvSlcA~#CpxB=_bO}34#v~>bUjSD4;QyUG|2INNV_J^Q2lti0a zZfCfbMXYWj7rAnQqAwr;z0n)=vH|)OyzP}> zq2FpXG(1ONeG`1u)UMIyWF0Av-jBI%JbukgZUM zY(Tbo4RQ_ICM1W~A=M#SkQ#gr(i|k4H`ldJE5Hgfj{#{FMZSS5)Ow6-Jsoq>s;p-e ztMyFI@OCY4eC*5cK|I`!`rSas8|6c7Dr+c`b~|Dt&BIpKwG-Cuu-ul(Pz2$Lo|`g^ zw{k;@;6xKOsD9mD9!jwsCxIU?@9pg^2XWF*6P{@2P^~H9WdOL|sD`Aj`RH+x0LvJ+6zObK0fuDvnM`_ZlQzlL8bXfPq zqFwa;-O%&$hUe`jd>B)^>3M$~`f)KL>SSG;DstphMGFhs4HZedaahApJ?9`$Izj#b zY83<+*CT*%%luN?VjMttR__^KYNpmR0R=0+^wDHjQPRI7L?XJUf5mzM?Xi5QO@(kK zZ=o9L1dNe^nvG1<`UsdCYT}KNdB9TRP&)$JM%p7lXiHzzju}&t=;+m`Oa;uy7w+|+_;JMViwMV8#VaV^r#YxyMe1z` z#SvEVVyS+pJu*0EMXyeA;l^`)92v8lwa1gY#q8NnXJz-UIy5Wpou`8^NJH)>{m@Oe z5Y2bo?Id;Ix+dT1=;C!OD_%p@F?%nCV_h#%7WZ0ykOKN+QM^jJu&YEk%P~UZeN^i3;e_V(q{ju_NH@m>pOnOZ#HTe(p%#k&Hr)*Ka=+wKX*oxL`Q78c1 z%um6NnO(%^lsuR(lxkGI(HIgFGf^B}XN#=K+Pbc{)qfMUqc@Pavdgo8B2&tYdXB`1$P}pP z&?1$L35!z7V_huLN#X_yg$vY$q_T2NKKxXCAL|?XJsnV>%ZJ*Pq2!U2@UK5-U*HtLFrzEJgI*lErQ(nH9}+S2Sxc^??=DJt`d*v0 zVuGA=7SLH%uA;RH3%C(6Za&w?c39o$FR(S6-`Lo={?U}%vtkujgnqnx`|3t@GrI~N zC!57(sy?8KXz#4i@1gIPC}a1TAmMCj*6?Xy;u2XulS=cN=kX*!{v;|Xu0a$OLH-al z|J0h3Ebt}rm?aT_~BD7jsv^r`r$ zG(zX#bCmIJnmAP1(#&qA4?eER;w5NL%`%DdhZwj|Q_(g-F}9$d7e|+@pUKR%bD5bk zu#?ZP#Sy4yEfncmmkvbs`zi?sYRKK(ch}cHa|5U#In%BGTrS?*iBeok{UCH@IPe8@ zxEn_@rAtwe?CvIgmvXhd@&AKJ+{WHsh@L-Sa2h>JXZLq8g2=5Ef?Nm_1dUO1CDrLG zvSKu|n=imE-r8sP?sOk|>)t0_Z>?ZCK_3P)*dewPbl;u|ZUqsJeic_@-R!&P3_b&r zj(vtW#LsBr327+QOe(d`DpY<;>(IecEMW`k>vn4DD=M$wc^ZTRO77}vE?f*M22ns$ fx>L4Sz|5Twi*LD~QwC6<%57)9IX~B4Za4o6KKg29 literal 0 HcmV?d00001 diff --git a/public/novnc/utils/websockify/websockify/__pycache__/websocket.cpython-39.pyc b/public/novnc/utils/websockify/websockify/__pycache__/websocket.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b147aa31e9bf0a7444ad7a832e2ac4a985c806a GIT binary patch literal 21821 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 literal 0 HcmV?d00001 diff --git a/public/novnc/utils/websockify/websockify/__pycache__/websockifyserver.cpython-39.pyc b/public/novnc/utils/websockify/websockify/__pycache__/websockifyserver.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c66533a0d32ab513a50292188a3822461aa0a77 GIT binary patch literal 22783 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 literal 0 HcmV?d00001 diff --git a/public/novnc/utils/websockify/websockify/auth_plugins.py b/public/novnc/utils/websockify/websockify/auth_plugins.py new file mode 100644 index 00000000..36fac520 --- /dev/null +++ b/public/novnc/utils/websockify/websockify/auth_plugins.py @@ -0,0 +1,102 @@ +class BasePlugin(): + 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().__init__('%s %s' % (self.code, log_msg)) + + +class InvalidOriginError(AuthenticationError): + def __init__(self, expected, actual): + self.expected_origin = expected + self.actual_origin = actual + + super().__init__( + response_msg='Invalid Origin', + log_msg="Invalid Origin Header: Expected one of " + "%s, got '%s'" % (expected, actual)) + + +class BasicHTTPAuth(): + """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 '): + self.auth_error() + + try: + user_pass_raw = base64.b64decode(auth_header[6:]) + except TypeError: + self.auth_error() + + 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: + self.auth_error() + + user_pass = user_pass_as_text.split(':', 1) + if len(user_pass) != 2: + self.auth_error() + + if not self.validate_creds(*user_pass): + self.demand_auth() + + else: + self.demand_auth() + + def validate_creds(self, username, password): + if '%s:%s' % (username, password) == self.src: + return True + else: + return False + + def auth_error(self): + raise AuthenticationError(response_code=403) + + def demand_auth(self): + raise AuthenticationError(response_code=401, + response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'}) + +class ExpectOrigin(): + 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) + +class ClientCertCNAuth(): + """Verifies client by SSL certificate. Specify src as whitespace separated list of common names.""" + + def __init__(self, src=None): + if src is None: + self.source = [] + else: + self.source = src.split() + + def authenticate(self, headers, target_host, target_port): + if headers.get('SSL_CLIENT_S_DN_CN', None) not in self.source: + raise AuthenticationError(response_code=403) diff --git a/public/novnc/utils/websockify/websockify/sysloghandler.py b/public/novnc/utils/websockify/websockify/sysloghandler.py new file mode 100644 index 00000000..37ee9dd2 --- /dev/null +++ b/public/novnc/utils/websockify/websockify/sysloghandler.py @@ -0,0 +1,118 @@ +import logging.handlers as handlers, socket, os, time + + +class WebsockifySysLogHandler(handlers.SysLogHandler): + """ + A handler class that sends proper Syslog-formatted messages, + as defined by RFC 5424. + """ + + _legacy_head_fmt = '<{pri}>{ident}[{pid}]: ' + _rfc5424_head_fmt = '<{pri}>1 {timestamp} {hostname} {ident} {pid} - - ' + _head_fmt = _rfc5424_head_fmt + _legacy = False + _timestamp_fmt = '%Y-%m-%dT%H:%M:%SZ' + _max_hostname = 255 + _max_ident = 24 #safer for old daemons + _send_length = False + _tail = '\n' + + + ident = None + + + def __init__(self, address=('localhost', handlers.SYSLOG_UDP_PORT), + facility=handlers.SysLogHandler.LOG_USER, + socktype=None, ident=None, legacy=False): + """ + Initialize a handler. + + If address is specified as a string, a UNIX socket is used. To log to a + local syslogd, "WebsockifySysLogHandler(address="/dev/log")" can be + used. If facility is not specified, LOG_USER is used. If socktype is + specified as socket.SOCK_DGRAM or socket.SOCK_STREAM, that specific + socket type will be used. For Unix sockets, you can also specify a + socktype of None, in which case socket.SOCK_DGRAM will be used, falling + back to socket.SOCK_STREAM. If ident is specified, this string will be + used as the application name in all messages sent. Set legacy to True + to use the old version of the protocol. + """ + + self.ident = ident + + if legacy: + self._legacy = True + self._head_fmt = self._legacy_head_fmt + + super().__init__(address, facility, socktype) + + + def emit(self, record): + """ + Emit a record. + + The record is formatted, and then sent to the syslog server. If + exception information is present, it is NOT sent to the server. + """ + + try: + # Gather info. + text = self.format(record).replace(self._tail, ' ') + if not text: # nothing to log + return + + pri = self.encodePriority(self.facility, + self.mapPriority(record.levelname)) + + timestamp = time.strftime(self._timestamp_fmt, time.gmtime()); + + hostname = socket.gethostname()[:self._max_hostname] + + if self.ident: + ident = self.ident[:self._max_ident] + else: + ident = '' + + pid = os.getpid() # shouldn't need truncation + + # Format the header. + head = { + 'pri': pri, + 'timestamp': timestamp, + 'hostname': hostname, + 'ident': ident, + 'pid': pid, + } + msg = self._head_fmt.format(**head).encode('ascii', 'ignore') + + # Encode text as plain ASCII if possible, else use UTF-8 with BOM. + try: + msg += text.encode('ascii') + except UnicodeEncodeError: + msg += text.encode('utf-8-sig') + + # Add length or tail character, if necessary. + if self.socktype != socket.SOCK_DGRAM: + if self._send_length: + msg = ('%d ' % len(msg)).encode('ascii') + msg + else: + msg += self._tail.encode('ascii') + + # Send the message. + if self.unixsocket: + try: + self.socket.send(msg) + except socket.error: + self._connect_unixsocket(self.address) + self.socket.send(msg) + + else: + if self.socktype == socket.SOCK_DGRAM: + self.socket.sendto(msg, self.address) + else: + self.socket.sendall(msg) + + except (KeyboardInterrupt, SystemExit): + raise + except: + self.handleError(record) diff --git a/public/novnc/utils/websockify/websockify/token_plugins.py b/public/novnc/utils/websockify/websockify/token_plugins.py new file mode 100644 index 00000000..d42414e2 --- /dev/null +++ b/public/novnc/utils/websockify/websockify/token_plugins.py @@ -0,0 +1,316 @@ +import logging +import os +import sys +import time +import re +import json + +logger = logging.getLogger(__name__) + + +class BasePlugin(): + 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().__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 = {} + index = 1 + for f in cfg_files: + for line in [l.strip() for l in open(f).readlines()]: + if line and not line.startswith('#'): + try: + tok, target = re.split(':\s', line) + self._targets[tok] = target.strip().rsplit(':', 1) + except ValueError: + logger.error("Syntax error in %s on line %d" % (self.source, index)) + index += 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().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 unnecessary dependencies + + def process_result(self, resp): + host, port = resp.text.split(':') + port = port.encode('ascii','ignore') + return [ host, port ] + + 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']) + + +class JWTTokenApi(BasePlugin): + # source is a JWT-token, with hostname and port included + # Both JWS as JWE tokens are accepted. With regards to JWE tokens, the key is re-used for both validation and decryption. + + def lookup(self, token): + try: + from jwcrypto import jwt, jwk + import json + + key = jwk.JWK() + + try: + with open(self.source, 'rb') as key_file: + key_data = key_file.read() + except Exception as e: + logger.error("Error loading key file: %s" % str(e)) + return None + + try: + key.import_from_pem(key_data) + except: + try: + key.import_key(k=key_data.decode('utf-8'),kty='oct') + except: + logger.error('Failed to correctly parse key data!') + return None + + try: + token = jwt.JWT(key=key, jwt=token) + parsed_header = json.loads(token.header) + + if 'enc' in parsed_header: + # Token is encrypted, so we need to decrypt by passing the claims to a new instance + token = jwt.JWT(key=key, jwt=token.claims) + + parsed = json.loads(token.claims) + + if 'nbf' in parsed: + # Not Before is present, so we need to check it + if time.time() < parsed['nbf']: + logger.warning('Token can not be used yet!') + return None + + if 'exp' in parsed: + # Expiration time is present, so we need to check it + if time.time() > parsed['exp']: + logger.warning('Token has expired!') + return None + + return (parsed['host'], parsed['port']) + except Exception as e: + logger.error("Failed to parse token: %s" % str(e)) + return None + except ImportError: + logger.error("package jwcrypto not found, are you sure you've installed it correctly?") + return None + + +class TokenRedis(BasePlugin): + """Token plugin based on the Redis in-memory data store. + + The token source is in the format: + + host[:port[:db[:password]]] + + where port, db and password are optional. If port or db are left empty + they will take its default value, ie. 6379 and 0 respectively. + + If your redis server is using the default port (6379) then you can use: + + my-redis-host + + In case you need to authenticate with the redis server and you are using + the default database and port you can use: + + my-redis-host:::verysecretpass + + In the more general case you will use: + + my-redis-host:6380:1:verysecretpass + + The TokenRedis plugin expects the format of the target in one of these two + formats: + + - JSON + + {"host": "target-host:target-port"} + + - Plain text + + target-host:target-port + + Prepare data with: + + redis-cli set my-token '{"host": "127.0.0.1:5000"}' + + Verify with: + + redis-cli --raw get my-token + + Spawn a test "server" using netcat + + nc -l 5000 -v + + Note: This Token Plugin depends on the 'redis' module, so you have + to install it before using this plugin: + + pip install redis + """ + def __init__(self, src): + try: + import redis + except ImportError: + logger.error("Unable to load redis module") + sys.exit() + # Default values + self._port = 6379 + self._db = 0 + self._password = None + try: + fields = src.split(":") + if len(fields) == 1: + self._server = fields[0] + elif len(fields) == 2: + self._server, self._port = fields + if not self._port: + self._port = 6379 + elif len(fields) == 3: + self._server, self._port, self._db = fields + if not self._port: + self._port = 6379 + if not self._db: + self._db = 0 + elif len(fields) == 4: + self._server, self._port, self._db, self._password = fields + if not self._port: + self._port = 6379 + if not self._db: + self._db = 0 + if not self._password: + self._password = None + else: + raise ValueError + self._port = int(self._port) + self._db = int(self._db) + logger.info("TokenRedis backend initilized (%s:%s)" % + (self._server, self._port)) + except ValueError: + logger.error("The provided --token-source='%s' is not in the " + "expected format [:[:[:]]]" % + src) + sys.exit() + + def lookup(self, token): + try: + import redis + except ImportError: + logger.error("package redis not found, are you sure you've installed them correctly?") + sys.exit() + + logger.info("resolving token '%s'" % token) + client = redis.Redis(host=self._server, port=self._port, + db=self._db, password=self._password) + stuff = client.get(token) + if stuff is None: + return None + else: + responseStr = stuff.decode("utf-8").strip() + logger.debug("response from redis : %s" % responseStr) + if responseStr.startswith("{"): + try: + combo = json.loads(responseStr) + host, port = combo["host"].split(":") + except ValueError: + logger.error("Unable to decode JSON token: %s" % + responseStr) + return None + except KeyError: + logger.error("Unable to find 'host' key in JSON token: %s" % + responseStr) + return None + elif re.match(r'\S+:\S+', responseStr): + host, port = responseStr.split(":") + else: + logger.error("Unable to parse token: %s" % responseStr) + return None + logger.debug("host: %s, port: %s" % (host, port)) + return [host, port] + + +class UnixDomainSocketDirectory(BasePlugin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._dir_path = os.path.abspath(self.source) + + def lookup(self, token): + try: + import stat + + if not os.path.isdir(self._dir_path): + return None + + uds_path = os.path.abspath(os.path.join(self._dir_path, token)) + if not uds_path.startswith(self._dir_path): + return None + + if not os.path.exists(uds_path): + return None + + if not stat.S_ISSOCK(os.stat(uds_path).st_mode): + return None + + return [ 'unix_socket', uds_path ] + except Exception as e: + logger.error("Error finding unix domain socket: %s" % str(e)) + return None diff --git a/public/novnc/utils/websockify/websockify/websocket.py b/public/novnc/utils/websockify/websockify/websocket.py new file mode 100644 index 00000000..5a819f7a --- /dev/null +++ b/public/novnc/utils/websockify/websockify/websocket.py @@ -0,0 +1,874 @@ +#!/usr/bin/env python + +''' +Python WebSocket library +Copyright 2011 Joel Martin +Copyright 2016 Pierre Ossman +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +Supports following protocol versions: + - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 + - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 + - http://tools.ietf.org/html/rfc6455 +''' + +import sys +import array +import email +import errno +import random +import socket +import ssl +import struct +from base64 import b64encode +from hashlib import sha1 +from urllib.parse import urlparse + +try: + import numpy +except ImportError: + import warnings + warnings.warn("no 'numpy' module, HyBi protocol will be slower") + numpy = None + +class WebSocketWantReadError(ssl.SSLWantReadError): + pass +class WebSocketWantWriteError(ssl.SSLWantWriteError): + pass + +class WebSocket(object): + """WebSocket protocol socket like class. + + This provides access to the WebSocket protocol by behaving much + like a real socket would. It shares many similarities with + ssl.SSLSocket. + + The WebSocket protocols requires extra data to be sent and received + compared to the application level data. This means that a socket + that is ready to be read may not hold enough data to decode any + application data, and a socket that is ready to be written to may + not have enough space for an entire WebSocket frame. This is + handled by the exceptions WebSocketWantReadError and + WebSocketWantWriteError. When these are raised the caller must wait + for the socket to become ready again and call the relevant function + again. + + A connection is established by using either connect() or accept(), + depending on if a client or server session is desired. See the + respective functions for details. + + The following methods are passed on to the underlying socket: + + - fileno + - getpeername, getsockname + - getsockopt, setsockopt + - gettimeout, settimeout + - setblocking + """ + + GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + def __init__(self): + """Creates an unconnected WebSocket""" + + self._state = "new" + + self._partial_msg = b'' + + self._recv_buffer = b'' + self._recv_queue = [] + self._send_buffer = b'' + + self._previous_sendmsg = None + + self._sent_close = False + self._received_close = False + + self.close_code = None + self.close_reason = None + + self.socket = None + + def __getattr__(self, name): + # These methods are just redirected to the underlying socket + if name in ["fileno", + "getpeername", "getsockname", + "getsockopt", "setsockopt", + "gettimeout", "settimeout", + "setblocking"]: + assert self.socket is not None + return getattr(self.socket, name) + else: + raise AttributeError("%s instance has no attribute '%s'" % + (self.__class__.__name__, name)) + + def connect(self, uri, origin=None, protocols=[]): + """Establishes a new connection to a WebSocket server. + + This method connects to the host specified by uri and + negotiates a WebSocket connection. origin should be specified + in accordance with RFC 6454 if known. A list of valid + sub-protocols can be specified in the protocols argument. + + The data will be sent in the clear if the "ws" scheme is used, + and encrypted if the "wss" scheme is used. + + Both WebSocketWantReadError and WebSocketWantWriteError can be + raised whilst negotiating the connection. Repeated calls to + connect() must retain the same arguments. + """ + + self.client = True; + + uri = urlparse(uri) + + port = uri.port + if uri.scheme in ("ws", "http"): + if not port: + port = 80 + elif uri.scheme in ("wss", "https"): + if not port: + port = 443 + else: + raise Exception("Unknown scheme '%s'" % uri.scheme) + + # This is a state machine in order to handle + # WantRead/WantWrite events + + if self._state == "new": + self.socket = socket.create_connection((uri.hostname, port)) + + if uri.scheme in ("wss", "https"): + self.socket = ssl.wrap_socket(self.socket) + self._state = "ssl_handshake" + else: + self._state = "headers" + + if self._state == "ssl_handshake": + self.socket.do_handshake() + self._state = "headers" + + if self._state == "headers": + self._key = '' + for i in range(16): + self._key += chr(random.randrange(256)) + self._key = b64encode(self._key.encode("latin-1")).decode("ascii") + + path = uri.path + if not path: + path = "/" + + self.send_request("GET", path) + self.send_header("Host", uri.hostname) + self.send_header("Upgrade", "websocket") + self.send_header("Connection", "upgrade") + self.send_header("Sec-WebSocket-Key", self._key) + self.send_header("Sec-WebSocket-Version", 13) + + if origin is not None: + self.send_header("Origin", origin) + if len(protocols) > 0: + self.send_header("Sec-WebSocket-Protocol", ", ".join(protocols)) + + self.end_headers() + + self._state = "send_headers" + + if self._state == "send_headers": + self._flush() + self._state = "response" + + if self._state == "response": + if not self._recv(): + raise Exception("Socket closed unexpectedly") + + if self._recv_buffer.find(b'\r\n\r\n') == -1: + raise WebSocketWantReadError + + (request, self._recv_buffer) = self._recv_buffer.split(b'\r\n', 1) + request = request.decode("latin-1") + + words = request.split() + if (len(words) < 2) or (words[0] != "HTTP/1.1"): + raise Exception("Invalid response") + if words[1] != "101": + raise Exception("WebSocket request denied: %s" % " ".join(words[1:])) + + (headers, self._recv_buffer) = self._recv_buffer.split(b'\r\n\r\n', 1) + headers = headers.decode('latin-1') + '\r\n' + headers = email.message_from_string(headers) + + if headers.get("Upgrade", "").lower() != "websocket": + print(type(headers)) + raise Exception("Missing or incorrect upgrade header") + + accept = headers.get('Sec-WebSocket-Accept') + if accept is None: + raise Exception("Missing Sec-WebSocket-Accept header"); + + expected = sha1((self._key + self.GUID).encode("ascii")).digest() + expected = b64encode(expected).decode("ascii") + + del self._key + + if accept != expected: + raise Exception("Invalid Sec-WebSocket-Accept header"); + + self.protocol = headers.get('Sec-WebSocket-Protocol') + if len(protocols) == 0: + if self.protocol is not None: + raise Exception("Unexpected Sec-WebSocket-Protocol header") + else: + if self.protocol not in protocols: + raise Exception("Invalid protocol chosen by server") + + self._state = "done" + + return + + raise Exception("WebSocket is in an invalid state") + + def accept(self, socket, headers): + """Establishes a new WebSocket session with a client. + + This method negotiates a WebSocket connection with an incoming + client. The caller must provide the client socket and the + headers from the HTTP request. + + A server can identify that a client is requesting a WebSocket + connection by looking at the "Upgrade" header. It will include + the value "websocket" in such cases. + + WebSocketWantWriteError can be raised if the response cannot be + sent right away. accept() must be called again once more space + is available using the same arguments. + """ + + # This is a state machine in order to handle + # WantRead/WantWrite events + + if self._state == "new": + self.client = False + self.socket = socket + + if headers.get("upgrade", "").lower() != "websocket": + raise Exception("Missing or incorrect upgrade header") + + ver = headers.get('Sec-WebSocket-Version') + if ver is None: + raise Exception("Missing Sec-WebSocket-Version header"); + + # HyBi-07 report version 7 + # HyBi-08 - HyBi-12 report version 8 + # HyBi-13 reports version 13 + if ver in ['7', '8', '13']: + self.version = "hybi-%02d" % int(ver) + else: + raise Exception("Unsupported protocol version %s" % ver) + + key = headers.get('Sec-WebSocket-Key') + if key is None: + raise Exception("Missing Sec-WebSocket-Key header"); + + # Generate the hash value for the accept header + accept = sha1((key + self.GUID).encode("ascii")).digest() + accept = b64encode(accept).decode("ascii") + + self.protocol = '' + protocols = headers.get('Sec-WebSocket-Protocol', '').split(',') + if protocols: + self.protocol = self.select_subprotocol(protocols) + # We are required to choose one of the protocols + # presented by the client + if self.protocol not in protocols: + raise Exception('Invalid protocol selected') + + self.send_response(101, "Switching Protocols") + self.send_header("Upgrade", "websocket") + self.send_header("Connection", "Upgrade") + self.send_header("Sec-WebSocket-Accept", accept) + + if self.protocol: + self.send_header("Sec-WebSocket-Protocol", self.protocol) + + self.end_headers() + + self._state = "flush" + + if self._state == "flush": + self._flush() + self._state = "done" + + return + + raise Exception("WebSocket is in an invalid state") + + def select_subprotocol(self, protocols): + """Returns which sub-protocol should be used. + + This method does not select any sub-protocol by default and is + meant to be overridden by an implementation that wishes to make + use of sub-protocols. It will be called during handling of + accept(). + """ + return "" + + def handle_ping(self, data): + """Called when a WebSocket ping message is received. + + This will be called whilst processing recv()/recvmsg(). The + default implementation sends a pong reply back.""" + self.pong(data) + + def handle_pong(self, data): + """Called when a WebSocket pong message is received. + + This will be called whilst processing recv()/recvmsg(). The + default implementation does nothing.""" + pass + + def recv(self): + """Read data from the WebSocket. + + This will return any available data on the socket (which may + be the empty string if the peer sent an empty message or + messages). If the socket is closed then None will be + returned. The reason for the close is found in the + 'close_code' and 'close_reason' properties. + + Unlike recvmsg() this method may return data from more than one + WebSocket message. It is however not guaranteed to return all + buffered data. Callers should continue calling recv() whilst + pending() returns True. + + Both WebSocketWantReadError and WebSocketWantWriteError can be + raised when calling recv(). + """ + return self.recvmsg() + + def recvmsg(self): + """Read a single message from the WebSocket. + + This will return a single WebSocket message from the socket + (which will be the empty string if the peer sent an empty + message). If the socket is closed then None will be + returned. The reason for the close is found in the + 'close_code' and 'close_reason' properties. + + Unlike recv() this method will not return data from more than + one WebSocket message. Callers should continue calling + recvmsg() whilst pending() returns True. + + Both WebSocketWantReadError and WebSocketWantWriteError can be + raised when calling recvmsg(). + """ + # May have been called to flush out a close + if self._received_close: + self._flush() + return None + + # Anything already queued? + if self.pending(): + return self._recvmsg() + # Note: If self._recvmsg() raised WebSocketWantReadError, + # we cannot proceed to self._recv() here as we may + # have already called it once as part of the caller's + # "while websock.pending():" loop + + # Nope, let's try to read a bit + if not self._recv_frames(): + return None + + # Anything queued now? + return self._recvmsg() + + def pending(self): + """Check if any WebSocket data is pending. + + This method will return True as long as there are WebSocket + frames that have yet been processed. A single recv() from the + underlying socket may return multiple WebSocket frames and it + is therefore important that a caller continues calling recv() + or recvmsg() as long as pending() returns True. + + Note that this function merely tells if there are raw WebSocket + frames pending. Those frames may not contain any application + data. + """ + return len(self._recv_queue) > 0 + + def send(self, bytes): + """Write data to the WebSocket + + This will queue the given data and attempt to send it to the + peer. Unlike sendmsg() this method might coalesce the data with + data from other calls, or split it over multiple messages. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket. send() must be called again + once more space is available using the same arguments. + """ + if len(bytes) == 0: + return 0 + + return self.sendmsg(bytes) + + def sendmsg(self, msg): + """Write a single message to the WebSocket + + This will queue the given message and attempt to send it to the + peer. Unlike send() this method will preserve the data as a + single WebSocket message. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket. sendmsg() must be called again + once more space is available using the same arguments. + """ + if not isinstance(msg, bytes): + raise TypeError + + if self._sent_close: + return 0 + + if self._previous_sendmsg is not None: + if self._previous_sendmsg != msg: + raise ValueError + + self._flush() + self._previous_sendmsg = None + + return len(msg) + + try: + self._sendmsg(0x2, msg) + except WebSocketWantWriteError: + self._previous_sendmsg = msg + raise + + return len(msg) + + def send_response(self, code, message): + self._queue_str("HTTP/1.1 %d %s\r\n" % (code, message)) + + def send_header(self, keyword, value): + self._queue_str("%s: %s\r\n" % (keyword, value)) + + def end_headers(self): + self._queue_str("\r\n") + + def send_request(self, type, path): + self._queue_str("%s %s HTTP/1.1\r\n" % (type.upper(), path)) + + def ping(self, data=b''): + """Write a ping message to the WebSocket + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket. ping() must be called again once + more space is available using the same arguments. + """ + if not isinstance(data, bytes): + raise TypeError + + if self._previous_sendmsg is not None: + if self._previous_sendmsg != data: + raise ValueError + + self._flush() + self._previous_sendmsg = None + + return + + try: + self._sendmsg(0x9, data) + except WebSocketWantWriteError: + self._previous_sendmsg = data + raise + + def pong(self, data=b''): + """Write a pong message to the WebSocket + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket. pong() must be called again once + more space is available using the same arguments. + """ + if not isinstance(data, bytes): + raise TypeError + + if self._previous_sendmsg is not None: + if self._previous_sendmsg != data: + raise ValueError + + self._flush() + self._previous_sendmsg = None + + return + + try: + self._sendmsg(0xA, data) + except WebSocketWantWriteError: + self._previous_sendmsg = data + raise + + def shutdown(self, how, code=1000, reason=None): + """Gracefully terminate the WebSocket connection. + + This will start the process to terminate the WebSocket + connection. The caller must continue to calling recv() or + recvmsg() after this function in order to wait for the peer to + acknowledge the close. Calls to send() and sendmsg() will be + ignored. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket for the close message. shutdown() + must be called again once more space is available using the same + arguments. + + The how argument is currently ignored. + """ + + # Already closing? + if self._sent_close: + self._flush() + return + + # Special code to indicate that we closed the connection + if not self._received_close: + self.close_code = 1000 + self.close_reason = "Locally initiated close" + + self._sent_close = True + + msg = b'' + if code is not None: + msg += struct.pack(">H", code) + if reason is not None: + msg += reason.encode("UTF-8") + + self._sendmsg(0x8, msg) + + def close(self, code=1000, reason=None): + """Terminate the WebSocket connection immediately. + + This will close the WebSocket connection directly after sending + a close message to the peer. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket for the close message. close() + must be called again once more space is available using the same + arguments. + """ + self.shutdown(socket.SHUT_RDWR, code, reason) + self._close() + + def _recv(self): + # Fetches more data from the socket to the buffer + assert self.socket is not None + + while True: + try: + data = self.socket.recv(4096) + except OSError as exc: + if exc.errno == errno.EWOULDBLOCK: + raise WebSocketWantReadError + raise + + if len(data) == 0: + return False + + self._recv_buffer += data + + # Support for SSLSocket like objects + if hasattr(self.socket, "pending"): + if not self.socket.pending(): + break + else: + break + + return True + + def _recv_frames(self): + # Fetches more data and decodes the frames + if not self._recv(): + if self.close_code is None: + self.close_code = 1006 + self.close_reason = "Connection closed abnormally" + self._sent_close = self._received_close = True + self._close() + return False + + while True: + frame = self._decode_hybi(self._recv_buffer) + if frame is None: + break + self._recv_buffer = self._recv_buffer[frame['length']:] + self._recv_queue.append(frame) + + return True + + def _recvmsg(self): + # Process pending frames and returns any application data + while self._recv_queue: + frame = self._recv_queue.pop(0) + + if not self.client and not frame['masked']: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Frame not masked") + continue + if self.client and frame['masked']: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Frame masked") + continue + + if frame["opcode"] == 0x0: + if not self._partial_msg: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Unexpected continuation frame") + continue + + self._partial_msg += frame["payload"] + + if frame["fin"]: + msg = self._partial_msg + self._partial_msg = b'' + return msg + elif frame["opcode"] == 0x1: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Text frames are not supported") + elif frame["opcode"] == 0x2: + if self._partial_msg: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Unexpected new frame") + continue + + if frame["fin"]: + return frame["payload"] + else: + self._partial_msg = frame["payload"] + elif frame["opcode"] == 0x8: + if self._received_close: + continue + + self._received_close = True + + if self._sent_close: + self._close() + return None + + if not frame["fin"]: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented close") + continue + + code = None + reason = None + if len(frame["payload"]) >= 2: + code = struct.unpack(">H", frame["payload"][:2])[0] + if len(frame["payload"]) > 2: + reason = frame["payload"][2:] + try: + reason = reason.decode("UTF-8") + except UnicodeDecodeError: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Invalid UTF-8 in close") + continue + + if code is None: + self.close_code = code = 1005 + self.close_reason = "No close status code specified by peer" + else: + self.close_code = code + if reason is not None: + self.close_reason = reason + + self.shutdown(None, code, reason) + return None + elif frame["opcode"] == 0x9: + if not frame["fin"]: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented ping") + continue + + self.handle_ping(frame["payload"]) + elif frame["opcode"] == 0xA: + if not frame["fin"]: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented pong") + continue + + self.handle_pong(frame["payload"]) + else: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Unknown opcode 0x%02x" % frame["opcode"]) + + raise WebSocketWantReadError + + def _flush(self): + # Writes pending data to the socket + if not self._send_buffer: + return + + assert self.socket is not None + + try: + sent = self.socket.send(self._send_buffer) + except OSError as exc: + if exc.errno == errno.EWOULDBLOCK: + raise WebSocketWantWriteError + raise + + self._send_buffer = self._send_buffer[sent:] + + if self._send_buffer: + raise WebSocketWantWriteError + + # We had a pending close and we've flushed the buffer, + # time to end things + if self._received_close and self._sent_close: + self._close() + + def _send(self, data): + # Queues data and attempts to send it + self._send_buffer += data + self._flush() + + def _queue_str(self, string): + # Queue some data to be sent later. + # Only used by the connecting methods. + self._send_buffer += string.encode("latin-1") + + def _sendmsg(self, opcode, msg): + # Sends a standard data message + if self.client: + mask = b'' + for i in range(4): + mask += random.randrange(256) + frame = self._encode_hybi(opcode, msg, mask) + else: + frame = self._encode_hybi(opcode, msg) + + return self._send(frame) + + def _close(self): + # Close the underlying socket + self.socket.close() + self.socket = None + + def _mask(self, buf, mask): + # Mask a frame + return self._unmask(buf, mask) + + def _unmask(self, buf, mask): + # Unmask a frame + if numpy: + plen = len(buf) + pstart = 0 + pend = plen + b = c = b'' + if plen >= 4: + dtype=numpy.dtype('') + mask = numpy.frombuffer(mask, dtype, count=1) + data = numpy.frombuffer(buf, dtype, count=int(plen / 4)) + #b = numpy.bitwise_xor(data, mask).data + b = numpy.bitwise_xor(data, mask).tobytes() + + if plen % 4: + dtype=numpy.dtype('B') + if sys.byteorder == 'big': + dtype = dtype.newbyteorder('>') + mask = numpy.frombuffer(mask, dtype, count=(plen % 4)) + data = numpy.frombuffer(buf, dtype, + offset=plen - (plen % 4), count=(plen % 4)) + c = numpy.bitwise_xor(data, mask).tobytes() + return b + c + else: + # Slower fallback + data = array.array('B') + data.frombytes(buf) + for i in range(len(data)): + data[i] ^= mask[i % 4] + return data.tobytes() + + def _encode_hybi(self, opcode, buf, mask_key=None, fin=True): + """ Encode a HyBi style WebSocket frame. + Optional opcode: + 0x0 - continuation + 0x1 - text frame + 0x2 - binary frame + 0x8 - connection close + 0x9 - ping + 0xA - pong + """ + + b1 = opcode & 0x0f + if fin: + b1 |= 0x80 + + mask_bit = 0 + if mask_key is not None: + mask_bit = 0x80 + buf = self._mask(buf, mask_key) + + payload_len = len(buf) + if payload_len <= 125: + header = struct.pack('>BB', b1, payload_len | mask_bit) + elif payload_len > 125 and payload_len < 65536: + header = struct.pack('>BBH', b1, 126 | mask_bit, payload_len) + elif payload_len >= 65536: + header = struct.pack('>BBQ', b1, 127 | mask_bit, payload_len) + + if mask_key is not None: + return header + mask_key + buf + else: + return header + buf + + def _decode_hybi(self, buf): + """ Decode HyBi style WebSocket packets. + Returns: + {'fin' : boolean, + 'opcode' : number, + 'masked' : boolean, + 'length' : encoded_length, + 'payload' : decoded_buffer} + """ + + f = {'fin' : 0, + 'opcode' : 0, + 'masked' : False, + 'length' : 0, + 'payload' : None} + + blen = len(buf) + hlen = 2 + + if blen < hlen: + return None + + b1, b2 = struct.unpack(">BB", buf[:2]) + f['opcode'] = b1 & 0x0f + f['fin'] = not not (b1 & 0x80) + f['masked'] = not not (b2 & 0x80) + + if f['masked']: + hlen += 4 + if blen < hlen: + return None + + length = b2 & 0x7f + + if length == 126: + hlen += 2 + if blen < hlen: + return None + length, = struct.unpack('>H', buf[2:4]) + elif length == 127: + hlen += 8 + if blen < hlen: + return None + length, = struct.unpack('>Q', buf[2:10]) + + f['length'] = hlen + length + + if blen < f['length']: + return None + + if f['masked']: + # unmask payload + mask_key = buf[hlen-4:hlen] + f['payload'] = self._unmask(buf[hlen:(hlen+length)], mask_key) + else: + f['payload'] = buf[hlen:(hlen+length)] + + return f + diff --git a/public/novnc/utils/websockify/websockify/websocketproxy.py b/public/novnc/utils/websockify/websockify/websocketproxy.py new file mode 100644 index 00000000..9b4595b4 --- /dev/null +++ b/public/novnc/utils/websockify/websockify/websocketproxy.py @@ -0,0 +1,800 @@ +#!/usr/bin/env python + +''' +A WebSocket to TCP socket proxy with support for "wss://" encryption. +Copyright 2011 Joel Martin +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +You can make a cert/key with openssl using: +openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem +as taken from http://docs.python.org/dev/library/ssl.html#certificates + +''' + +import signal, socket, optparse, time, os, sys, subprocess, logging, errno, ssl, stat +from socketserver import ThreadingMixIn +from http.server import HTTPServer + +import select +from websockify import websockifyserver +from websockify import auth_plugins as auth +from urllib.parse import parse_qs, urlparse + +class ProxyRequestHandler(websockifyserver.WebSockifyRequestHandler): + + buffer_size = 65536 + + traffic_legend = """ +Traffic Legend: + } - Client receive + }. - Client receive partial + { - Target receive + + > - Target send + >. - Target send partial + < - Client send + <. - 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 not self.server.token_plugin: + return + + host, port = self.get_target(self.server.token_plugin) + if host == 'unix_socket': + self.server.unix_target = port + + else: + self.server.target_host = host + self.server.target_port = port + + def auth_connection(self): + if not self.server.auth_plugin: + return + + try: + # get client certificate data + client_cert_data = self.request.getpeercert() + # extract subject information + client_cert_subject = client_cert_data['subject'] + # flatten data structure + client_cert_subject = dict([x[0] for x in client_cert_subject]) + # add common name to headers (apache +StdEnvVars style) + self.headers['SSL_CLIENT_S_DN_CN'] = client_cert_subject['commonName'] + except (TypeError, AttributeError, KeyError): + # not a SSL connection or client presented no certificate with valid data + pass + + 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. + """ + # Checking for a token is done in validate_connection() + + # Connect to the target + if self.server.wrap_cmd: + msg = "connecting to command: '%s' (port %s)" % (" ".join(self.server.wrap_cmd), self.server.target_port) + elif self.server.unix_target: + msg = "connecting to unix socket: %s" % self.server.unix_target + else: + msg = "connecting to: %s:%s" % ( + self.server.target_host, self.server.target_port) + + if self.server.ssl_target: + msg += " (using SSL)" + self.log_message(msg) + + try: + tsock = websockifyserver.WebSockifyServer.socket(self.server.target_host, + self.server.target_port, + connect=True, + use_ssl=self.server.ssl_target, + unix_socket=self.server.unix_target) + except Exception as e: + self.log_message("Failed to connect to %s:%s: %s", + self.server.target_host, self.server.target_port, e) + raise self.CClose(1011, "Failed to connect to downstream server") + + # Option unavailable when listening to unix socket + if not self.server.unix_listen: + self.request.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + if not self.server.wrap_cmd and not self.server.unix_target: + tsock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + + self.print_traffic(self.traffic_legend) + + # Start proxying + try: + self.do_proxy(tsock) + finally: + if tsock: + tsock.shutdown(socket.SHUT_RDWR) + tsock.close() + if self.verbose: + self.log_message("%s:%s: Closed target", + self.server.target_host, self.server.target_port) + + def get_target(self, target_plugin): + """ + Gets a token from either the path or the host, + depending on --host-token, and looks up a target + for that token using the token plugin. Used by + validate_connection() to set target_host and target_port. + """ + # The files in targets contain the lines + # in the form of token: host:port + + if self.host_token: + # Use hostname as token + token = self.headers.get('Host') + + # Remove port from hostname, as it'll always be the one where + # websockify listens (unless something between the client and + # websockify is redirecting traffic, but that's beside the point) + if token: + token = token.partition(':')[0] + + else: + # Extract the token parameter from url + args = parse_qs(urlparse(self.path)[4]) # 4 is the query from url + + if 'token' in args and len(args['token']): + token = args['token'][0].rstrip('\n') + else: + token = None + + if token is None: + raise self.server.EClose("Token not present") + + result_pair = target_plugin.lookup(token) + + if result_pair is not None: + return result_pair + else: + raise self.server.EClose("Token '%s' not found" % token) + + def do_proxy(self, target): + """ + Proxy client WebSocket to normal target socket. + """ + cqueue = [] + c_pend = 0 + 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) + 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: + # Send queued target data to the client + c_pend = self.send_frames(cqueue) + + cqueue = [] + + if self.request in ins: + # Receive client data, decode it, and queue for target + bufs, closed = self.recv_frames() + tqueue.extend(bufs) + + if closed: + + while (len(tqueue) != 0): + # Send queued client data to the target + dat = tqueue.pop(0) + sent = target.send(dat) + if sent == len(dat): + self.print_traffic(">") + else: + # requeue the remaining data + tqueue.insert(0, dat[sent:]) + self.print_traffic(".>") + + # TODO: What about blocking on client socket? + 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']) + + + if target in outs: + # Send queued client data to the target + dat = tqueue.pop(0) + sent = target.send(dat) + if sent == len(dat): + self.print_traffic(">") + else: + # requeue the remaining data + tqueue.insert(0, dat[sent:]) + self.print_traffic(".>") + + + if target in ins: + # Receive target data, encode it and queue for client + buf = target.recv(self.buffer_size) + if len(buf) == 0: + + # Target socket closed, flushing queues and closing client-side websocket + # Send queued target data to the client + if len(cqueue) != 0: + c_pend = True + while(c_pend): + c_pend = self.send_frames(cqueue) + + cqueue = [] + + if self.verbose: + self.log_message("%s:%s: Target closed connection", + self.server.target_host, self.server.target_port) + raise self.CClose(1000, "Target closed") + + cqueue.append(buf) + self.print_traffic("{") + +class WebSocketProxy(websockifyserver.WebSockifyServer): + """ + Proxy traffic to and from a WebSockets client to a normal TCP + socket server target. + """ + + buffer_size = 65536 + + def __init__(self, RequestHandlerClass=ProxyRequestHandler, *args, **kwargs): + # Save off proxy specific options + self.target_host = kwargs.pop('target_host', None) + self.target_port = kwargs.pop('target_port', None) + self.wrap_cmd = kwargs.pop('wrap_cmd', None) + 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.heartbeat = kwargs.pop('heartbeat', None) + + self.token_plugin = kwargs.pop('token_plugin', None) + self.host_token = kwargs.pop('host_token', None) + self.auth_plugin = kwargs.pop('auth_plugin', None) + + # Last 3 timestamps command was run + self.wrap_times = [0, 0, 0] + + if self.wrap_cmd: + wsdir = os.path.dirname(sys.argv[0]) + rebinder_path = [os.path.join(wsdir, "..", "lib"), + os.path.join(wsdir, "..", "lib", "websockify"), + os.path.join(wsdir, ".."), + wsdir] + self.rebinder = None + + for rdir in rebinder_path: + rpath = os.path.join(rdir, "rebind.so") + if os.path.exists(rpath): + self.rebinder = rpath + break + + if not self.rebinder: + raise Exception("rebind.so not found, perhaps you need to run make") + self.rebinder = os.path.abspath(self.rebinder) + + self.target_host = "127.0.0.1" # Loopback + # Find a free high port + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('', 0)) + self.target_port = sock.getsockname()[1] + sock.close() + + # Insert rebinder at the head of the (possibly empty) LD_PRELOAD pathlist + ld_preloads = filter(None, [ self.rebinder, os.environ.get("LD_PRELOAD", None) ]) + + os.environ.update({ + "LD_PRELOAD": os.pathsep.join(ld_preloads), + "REBIND_OLD_PORT": str(kwargs['listen_port']), + "REBIND_NEW_PORT": str(self.target_port)}) + + super().__init__(RequestHandlerClass, *args, **kwargs) + + def run_wrap_cmd(self): + self.msg("Starting '%s'", " ".join(self.wrap_cmd)) + self.wrap_times.append(time.time()) + self.wrap_times.pop(0) + self.cmd = subprocess.Popen( + self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup) + self.spawn_message = True + + def started(self): + """ + Called after Websockets server startup (i.e. after daemonize) + """ + # Need to call wrapped command after daemonization so we can + # know when the wrapped command exits + if self.wrap_cmd: + dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port) + elif self.unix_target: + dst_string = self.unix_target + else: + dst_string = "%s:%s" % (self.target_host, self.target_port) + + if self.listen_fd != None: + src_string = "inetd" + else: + src_string = "%s:%s" % (self.listen_host, self.listen_port) + + if self.token_plugin: + msg = " - proxying from %s to targets generated by %s" % ( + src_string, type(self.token_plugin).__name__) + else: + msg = " - proxying from %s to %s" % ( + src_string, dst_string) + + if self.ssl_target: + msg += " (using SSL)" + + self.msg("%s", msg) + + if self.wrap_cmd: + self.run_wrap_cmd() + + def poll(self): + # If we are wrapping a command, check it's status + + if self.wrap_cmd and self.cmd: + ret = self.cmd.poll() + if ret != None: + self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret) + self.cmd = None + + if self.wrap_cmd and self.cmd == None: + # Response to wrapped command being gone + if self.wrap_mode == "ignore": + pass + elif self.wrap_mode == "exit": + sys.exit(ret) + elif self.wrap_mode == "respawn": + now = time.time() + avg = sum(self.wrap_times)/len(self.wrap_times) + if (now - avg) < 10: + # 3 times in the last 10 seconds + if self.spawn_message: + self.warn("Command respawning too fast") + self.spawn_message = False + else: + self.run_wrap_cmd() + + +def _subprocess_setup(): + # Python installs a SIGPIPE handler by default. This is usually not what + # non-Python successfulbprocesses expect. + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + + +SSL_OPTIONS = { + 'default': ssl.OP_ALL, + 'tlsv1_1': ssl.PROTOCOL_SSLv23 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1, + 'tlsv1_2': ssl.PROTOCOL_SSLv23 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1, + 'tlsv1_3': ssl.PROTOCOL_SSLv23 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2, +} + +def select_ssl_version(version): + """Returns SSL options for the most secure TSL version available on this + Python version""" + if version in SSL_OPTIONS: + return SSL_OPTIONS[version] + else: + # It so happens that version names sorted lexicographically form a list + # from the least to the most secure + keys = list(SSL_OPTIONS.keys()) + keys.sort() + fallback = keys[-1] + logger = logging.getLogger(WebSocketProxy.log_prefix) + logger.warn("TLS version %s unsupported. Falling back to %s", + version, fallback) + + return SSL_OPTIONS[fallback] + +def websockify_init(): + # Setup basic logging to stderr. + stderr_handler = logging.StreamHandler() + stderr_handler.setLevel(logging.DEBUG) + log_formatter = logging.Formatter("%(message)s") + stderr_handler.setFormatter(log_formatter) + root = logging.getLogger() + root.addHandler(stderr_handler) + root.setLevel(logging.INFO) + + # Setup optparse. + usage = "\n %prog [options]" + usage += " [source_addr:]source_port [target_addr:target_port]" + usage += "\n %prog [options]" + usage += " --token-plugin=CLASS [source_addr:]source_port" + usage += "\n %prog [options]" + usage += " --unix-target=FILE [source_addr:]source_port" + usage += "\n %prog [options]" + usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" + parser = optparse.OptionParser(usage=usage) + parser.add_option("--verbose", "-v", action="store_true", + help="verbose messages") + parser.add_option("--traffic", action="store_true", + help="per frame traffic") + parser.add_option("--record", + help="record sessions to FILE.[session_number]", metavar="FILE") + parser.add_option("--daemon", "-D", + dest="daemon", action="store_true", + help="become a daemon (background process)") + parser.add_option("--run-once", action="store_true", + help="handle a single WebSocket connection and exit") + parser.add_option("--timeout", type=int, default=0, + help="after TIMEOUT seconds exit when not connected") + parser.add_option("--idle-timeout", type=int, default=0, + help="server exits after TIMEOUT seconds if there are no " + "active connections") + parser.add_option("--cert", default="self.pem", + help="SSL certificate file") + parser.add_option("--key", default=None, + help="SSL key file (if separate from cert)") + parser.add_option("--key-password", default=None, + help="SSL key password") + parser.add_option("--ssl-only", action="store_true", + help="disallow non-encrypted client connections") + parser.add_option("--ssl-target", action="store_true", + help="connect to SSL target as SSL client") + parser.add_option("--verify-client", action="store_true", + help="require encrypted client to present a valid certificate " + "(needs Python 2.7.9 or newer or Python 3.4 or newer)") + parser.add_option("--cafile", metavar="FILE", + help="file of concatenated certificates of authorities trusted " + "for validating clients (only effective with --verify-client). " + "If omitted, system default list of CAs is used.") + parser.add_option("--ssl-version", type="choice", default="default", + choices=["default", "tlsv1_1", "tlsv1_2", "tlsv1_3"], action="store", + help="minimum TLS version to use (default, tlsv1_1, tlsv1_2, tlsv1_3)") + parser.add_option("--ssl-ciphers", action="store", + help="list of ciphers allowed for connection. For a list of " + "supported ciphers run `openssl ciphers`") + parser.add_option("--unix-listen", + help="listen to unix socket", metavar="FILE", default=None) + parser.add_option("--unix-listen-mode", default=None, + help="specify mode for unix socket (defaults to 0600)") + parser.add_option("--unix-target", + help="connect to unix socket target", metavar="FILE") + parser.add_option("--inetd", + help="inetd mode, receive listening socket from stdin", action="store_true") + parser.add_option("--web", default=None, metavar="DIR", + help="run webserver on same port. Serve files from DIR.") + parser.add_option("--web-auth", action="store_true", + help="require authentication to access webserver.") + parser.add_option("--wrap-mode", default="exit", metavar="MODE", + choices=["exit", "ignore", "respawn"], + help="action to take when the wrapped program exits " + "or daemonizes: exit (default), ignore, respawn") + 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 " + "(DEPRECATED: use `--token-plugin TokenFile --token-source " + " path/to/token/file` instead)") + parser.add_option("--token-plugin", default=None, metavar="CLASS", + help="use a Python class, usually one from websockify.token_plugins, " + "such as TokenFile, 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("--host-token", action="store_true", + help="use the host HTTP header as token instead of the " + "token URL query parameter") + parser.add_option("--auth-plugin", default=None, metavar="CLASS", + help="use a Python class, usually one from websockify.auth_plugins, " + "such as BasicHTTPAuth, 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("--heartbeat", type=int, default=0, metavar="INTERVAL", + help="send a ping to the client every INTERVAL seconds") + parser.add_option("--log-file", metavar="FILE", + dest="log_file", + help="File where logs will be saved") + parser.add_option("--syslog", default=None, metavar="SERVER", + help="Log to syslog server. SERVER can be local socket, " + "such as /dev/log, or a UDP host:port pair.") + parser.add_option("--legacy-syslog", action="store_true", + help="Use the old syslog protocol instead of RFC 5424. " + "Use this if the messages produced by websockify seem abnormal.") + parser.add_option("--file-only", action="store_true", + help="use this to disable directory listings in web server.") + + (opts, args) = parser.parse_args() + + + # Validate options. + + if opts.token_source and not opts.token_plugin: + parser.error("You must use --token-plugin to use --token-source") + + if opts.host_token and not opts.token_plugin: + parser.error("You must use --token-plugin to use --host-token") + + if opts.auth_source and not opts.auth_plugin: + parser.error("You must use --auth-plugin to use --auth-source") + + if opts.web_auth and not opts.auth_plugin: + parser.error("You must use --auth-plugin to use --web-auth") + + if opts.web_auth and not opts.web: + parser.error("You must use --web to use --web-auth") + + if opts.legacy_syslog and not opts.syslog: + parser.error("You must use --syslog to use --legacy-syslog") + + + opts.ssl_options = select_ssl_version(opts.ssl_version) + del opts.ssl_version + + + if opts.log_file: + # Setup logging to user-specified file. + opts.log_file = os.path.abspath(opts.log_file) + log_file_handler = logging.FileHandler(opts.log_file) + log_file_handler.setLevel(logging.DEBUG) + log_file_handler.setFormatter(log_formatter) + root = logging.getLogger() + root.addHandler(log_file_handler) + + del opts.log_file + + if opts.syslog: + # Determine how to connect to syslog... + if opts.syslog.count(':'): + # User supplied a host:port pair. + syslog_host, syslog_port = opts.syslog.rsplit(':', 1) + try: + syslog_port = int(syslog_port) + except ValueError: + parser.error("Error parsing syslog port") + syslog_dest = (syslog_host, syslog_port) + else: + # User supplied a local socket file. + syslog_dest = os.path.abspath(opts.syslog) + + from websockify.sysloghandler import WebsockifySysLogHandler + + # Determine syslog facility. + if opts.daemon: + syslog_facility = WebsockifySysLogHandler.LOG_DAEMON + else: + syslog_facility = WebsockifySysLogHandler.LOG_USER + + # Start logging to syslog. + syslog_handler = WebsockifySysLogHandler(address=syslog_dest, + facility=syslog_facility, + ident='websockify', + legacy=opts.legacy_syslog) + syslog_handler.setLevel(logging.DEBUG) + syslog_handler.setFormatter(log_formatter) + root = logging.getLogger() + root.addHandler(syslog_handler) + + del opts.syslog + del opts.legacy_syslog + + if opts.verbose: + root = logging.getLogger() + root.setLevel(logging.DEBUG) + + + # 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 + + if sys.argv.count('--'): + opts.wrap_cmd = args[1:] + else: + opts.wrap_cmd = None + + if not websockifyserver.ssl and opts.ssl_target: + parser.error("SSL target requested and Python SSL module not loaded."); + + if opts.ssl_only and not os.path.exists(opts.cert): + parser.error("SSL only and %s not found" % opts.cert) + + if opts.inetd: + opts.listen_fd = sys.stdin.fileno() + elif opts.unix_listen: + if opts.unix_listen_mode: + try: + # Parse octal notation (like 750) + opts.unix_listen_mode = int(opts.unix_listen_mode, 8) + except ValueError: + parser.error("Error parsing listen unix socket mode") + else: + # Default to 0600 (Owner Read/Write) + opts.unix_listen_mode = stat.S_IREAD | stat.S_IWRITE + else: + if len(args) < 1: + parser.error("Too few arguments") + arg = args.pop(0) + # Parse host:port and convert ports to numbers + if arg.count(':') > 0: + opts.listen_host, opts.listen_port = arg.rsplit(':', 1) + opts.listen_host = opts.listen_host.strip('[]') + else: + opts.listen_host, opts.listen_port = '', arg + + try: + opts.listen_port = int(opts.listen_port) + except ValueError: + parser.error("Error parsing listen port") + + del opts.inetd + + if opts.wrap_cmd or opts.unix_target or opts.token_plugin: + opts.target_host = None + opts.target_port = None + else: + if len(args) < 1: + parser.error("Too few arguments") + arg = args.pop(0) + if arg.count(':') > 0: + opts.target_host, opts.target_port = arg.rsplit(':', 1) + opts.target_host = opts.target_host.strip('[]') + else: + parser.error("Error parsing target") + + try: + opts.target_port = int(opts.target_port) + except ValueError: + parser.error("Error parsing target port") + + if len(args) > 0 and opts.wrap_cmd == None: + parser.error("Too many arguments") + + 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 + del opts.libserver + if libserver: + # Use standard Python SocketServer framework + server = LibProxyServer(**opts.__dict__) + server.serve_forever() + else: + # Use internal service framework + server = WebSocketProxy(**opts.__dict__) + server.start_server() + + +class LibProxyServer(ThreadingMixIn, HTTPServer): + """ + Just like WebSocketProxy, but uses standard Python SocketServer + framework. + """ + + def __init__(self, RequestHandlerClass=ProxyRequestHandler, **kwargs): + # Save off proxy specific options + self.target_host = kwargs.pop('target_host', None) + self.target_port = kwargs.pop('target_port', None) + self.wrap_cmd = kwargs.pop('wrap_cmd', None) + 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.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 + + # Server configuration + listen_host = kwargs.pop('listen_host', '') + listen_port = kwargs.pop('listen_port', None) + web = kwargs.pop('web', '') + + # Configuration affecting base request handler + self.only_upgrade = not web + self.verbose = kwargs.pop('verbose', False) + record = kwargs.pop('record', '') + if record: + self.record = os.path.abspath(record) + self.run_once = kwargs.pop('run_once', False) + self.handler_id = 0 + + for arg in kwargs.keys(): + print("warning: option %s ignored when using --libserver" % arg) + + if web: + os.chdir(web) + + super().__init__((listen_host, listen_port), RequestHandlerClass) + + + def process_request(self, request, client_address): + """Override process_request to implement a counter""" + self.handler_id += 1 + super().process_request(request, client_address) + + +if __name__ == '__main__': + websockify_init() diff --git a/public/novnc/utils/websockify/websockify/websocketserver.py b/public/novnc/utils/websockify/websockify/websocketserver.py new file mode 100644 index 00000000..4e62f2ec --- /dev/null +++ b/public/novnc/utils/websockify/websockify/websocketserver.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +''' +Python WebSocket server base +Copyright 2011 Joel Martin +Copyright 2016-2018 Pierre Ossman +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) +''' + +import sys +from http.server import BaseHTTPRequestHandler, HTTPServer + +from websockify.websocket import WebSocket, WebSocketWantReadError, WebSocketWantWriteError + +class HttpWebSocket(WebSocket): + """Class to glue websocket and http request functionality together""" + def __init__(self, request_handler): + super().__init__() + + self.request_handler = request_handler + + def send_response(self, code, message=None): + self.request_handler.send_response(code, message) + + def send_header(self, keyword, value): + self.request_handler.send_header(keyword, value) + + def end_headers(self): + self.request_handler.end_headers() + + +class WebSocketRequestHandlerMixIn: + """WebSocket request handler mix-in class + + This class modifies and existing request handler to handle + WebSocket requests. The request handler will continue to function + as before, except that WebSocket requests are intercepted and the + methods handle_upgrade() and handle_websocket() are called. The + standard do_GET() will be called for normal requests. + + The class instance SocketClass can be overridden with the class to + use for the WebSocket connection. + """ + + SocketClass = HttpWebSocket + + def handle_one_request(self): + """Extended request handler + + This is where WebSocketRequestHandler redirects requests to the + new methods. Any sub-classes must call this method in order for + the calls to function. + """ + self._real_do_GET = self.do_GET + self.do_GET = self._websocket_do_GET + try: + super().handle_one_request() + finally: + self.do_GET = self._real_do_GET + + def _websocket_do_GET(self): + # Checks if it is a websocket request and redirects + self.do_GET = self._real_do_GET + + if (self.headers.get('upgrade') and + self.headers.get('upgrade').lower() == 'websocket'): + self.handle_upgrade() + else: + self.do_GET() + + def handle_upgrade(self): + """Initial handler for a WebSocket request + + This method is called when a WebSocket is requested. By default + it will create a WebSocket object and perform the negotiation. + The WebSocket object will then replace the request object and + handle_websocket() will be called. + """ + websocket = self.SocketClass(self) + try: + websocket.accept(self.request, self.headers) + except Exception: + exc = sys.exc_info()[1] + self.send_error(400, str(exc)) + return + + self.request = websocket + + # Other requests cannot follow Websocket data + self.close_connection = True + + self.handle_websocket() + + def handle_websocket(self): + """Handle a WebSocket connection. + + This is called when the WebSocket is ready to be used. A + sub-class should perform the necessary communication here and + return once done. + """ + pass + +# Convenient ready made classes + +class WebSocketRequestHandler(WebSocketRequestHandlerMixIn, + BaseHTTPRequestHandler): + pass + +class WebSocketServer(HTTPServer): + pass diff --git a/public/novnc/utils/websockify/websockify/websockifyserver.py b/public/novnc/utils/websockify/websockify/websockifyserver.py new file mode 100644 index 00000000..74f9f536 --- /dev/null +++ b/public/novnc/utils/websockify/websockify/websockifyserver.py @@ -0,0 +1,862 @@ +#!/usr/bin/env python + +''' +Python WebSocket server base with support for "wss://" encryption. +Copyright 2011 Joel Martin +Copyright 2016 Pierre Ossman +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +You can make a cert/key with openssl using: +openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem +as taken from http://docs.python.org/dev/library/ssl.html#certificates + +''' + +import os, sys, time, errno, signal, socket, select, logging +import multiprocessing +from http.server import SimpleHTTPRequestHandler + +# Degraded functionality if these imports are missing +for mod, msg in [('ssl', 'TLS/SSL/wss is disabled'), + ('resource', 'daemonizing is disabled')]: + try: + globals()[mod] = __import__(mod) + except ImportError: + globals()[mod] = None + print("WARNING: no '%s' module, %s" % (mod, msg)) + +if sys.platform == 'win32': + # make sockets pickle-able/inheritable + import multiprocessing.reduction + +from websockify.websocket import WebSocketWantReadError, WebSocketWantWriteError +from websockify.websocketserver import WebSocketRequestHandlerMixIn + +class CompatibleWebSocket(WebSocketRequestHandlerMixIn.SocketClass): + def select_subprotocol(self, protocols): + # Handle old websockify clients that still specify a sub-protocol + if 'binary' in protocols: + return 'binary' + else: + return '' + +# HTTP handler with WebSocket upgrade support +class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHandler): + """ + WebSocket Request Handler Class, derived from SimpleHTTPRequestHandler. + Must be sub-classed with new_websocket_client method definition. + The request handler can be configured by setting optional + attributes on the server object: + + * only_upgrade: If true, SimpleHTTPRequestHandler will not be enabled, + only websocket is allowed. + * verbose: If true, verbose logging is activated. + * daemon: Running as daemon, do not write to console etc + * record: Record raw frame data as JavaScript array into specified filename + * run_once: Handle a single request + * handler_id: A sequence number for this connection, appended to record filename + """ + server_version = "WebSockify" + + protocol_version = "HTTP/1.1" + + SocketClass = CompatibleWebSocket + + # An exception while the WebSocket client was connected + class CClose(Exception): + pass + + def __init__(self, req, addr, server): + # Retrieve a few configuration variables from the server + self.only_upgrade = getattr(server, "only_upgrade", False) + self.verbose = getattr(server, "verbose", False) + self.daemon = getattr(server, "daemon", False) + self.record = getattr(server, "record", False) + self.run_once = getattr(server, "run_once", False) + self.rec = None + self.handler_id = getattr(server, "handler_id", False) + self.file_only = getattr(server, "file_only", False) + self.traffic = getattr(server, "traffic", False) + self.web_auth = getattr(server, "web_auth", False) + self.host_token = getattr(server, "host_token", False) + + self.logger = getattr(server, "logger", None) + if self.logger is None: + self.logger = WebSockifyServer.get_logger() + + super().__init__(req, addr, server) + + def log_message(self, format, *args): + self.logger.info("%s - - [%s] %s" % (self.client_address[0], self.log_date_time_string(), format % args)) + + # + # WebSocketRequestHandler logging/output functions + # + + def print_traffic(self, token="."): + """ Show traffic flow mode. """ + if self.traffic: + sys.stdout.write(token) + sys.stdout.flush() + + def msg(self, msg, *args, **kwargs): + """ Output message with handler_id prefix. """ + prefix = "% 3d: " % self.handler_id + self.logger.log(logging.INFO, "%s%s" % (prefix, msg), *args, **kwargs) + + def vmsg(self, msg, *args, **kwargs): + """ Same as msg() but as debug. """ + prefix = "% 3d: " % self.handler_id + self.logger.log(logging.DEBUG, "%s%s" % (prefix, msg), *args, **kwargs) + + def warn(self, msg, *args, **kwargs): + """ Same as msg() but as warning. """ + prefix = "% 3d: " % self.handler_id + self.logger.log(logging.WARN, "%s%s" % (prefix, msg), *args, **kwargs) + + # + # Main WebSocketRequestHandler methods + # + def send_frames(self, bufs=None): + """ Encode and send WebSocket frames. Any frames already + queued will be sent first. If buf is not set then only queued + frames will be sent. Returns True if any frames could not be + fully sent, in which case the caller should call again when + the socket is ready. """ + + tdelta = int(time.time()*1000) - self.start_time + + if bufs: + for buf in bufs: + if self.rec: + # Python 3 compatible conversion + bufstr = buf.decode('latin1').encode('unicode_escape').decode('ascii').replace("'", "\\'") + self.rec.write("'{{{0}{{{1}',\n".format(tdelta, bufstr)) + self.send_parts.append(buf) + + while self.send_parts: + # Send pending frames + try: + self.request.sendmsg(self.send_parts[0]) + except WebSocketWantWriteError: + self.print_traffic("<.") + return True + self.send_parts.pop(0) + self.print_traffic("<") + + return False + + def recv_frames(self): + """ Receive and decode WebSocket frames. + + Returns: + (bufs_list, closed_string) + """ + + closed = False + bufs = [] + tdelta = int(time.time()*1000) - self.start_time + + while True: + try: + buf = self.request.recvmsg() + except WebSocketWantReadError: + self.print_traffic("}.") + break + + if buf is None: + closed = {'code': self.request.close_code, + 'reason': self.request.close_reason} + return bufs, closed + + self.print_traffic("}") + + if self.rec: + # Python 3 compatible conversion + bufstr = buf.decode('latin1').encode('unicode_escape').decode('ascii').replace("'", "\\'") + self.rec.write("'}}{0}}}{1}',\n".format(tdelta, bufstr)) + + bufs.append(buf) + + if not self.request.pending(): + break + + return bufs, closed + + def send_close(self, code=1000, reason=''): + """ Send a WebSocket orderly close frame. """ + self.request.shutdown(socket.SHUT_RDWR, code, reason) + + def send_pong(self, data=''.encode('ascii')): + """ Send a WebSocket pong frame. """ + self.request.pong(data) + + def send_ping(self, data=''.encode('ascii')): + """ Send a WebSocket ping frame. """ + self.request.ping(data) + + def handle_upgrade(self): + # ensure connection is authorized, and determine the target + self.validate_connection() + self.auth_connection() + + super().handle_upgrade() + + def handle_websocket(self): + # Indicate to server that a Websocket upgrade was done + self.server.ws_connection = True + # Initialize per client settings + self.send_parts = [] + self.recv_part = None + self.start_time = int(time.time()*1000) + + # client_address is empty with, say, UNIX domain sockets + client_addr = "" + is_ssl = False + try: + client_addr = self.client_address[0] + is_ssl = self.client_address[2] + except IndexError: + pass + + if is_ssl: + self.stype = "SSL/TLS (wss://)" + else: + self.stype = "Plain non-SSL (ws://)" + + self.log_message("%s: %s WebSocket connection", client_addr, + self.stype) + if self.path != '/': + self.log_message("%s: Path: '%s'", client_addr, self.path) + + if self.record: + # Record raw frame data as JavaScript array + fname = "%s.%s" % (self.record, + self.handler_id) + self.log_message("opening record file: %s", fname) + self.rec = open(fname, 'w+') + self.rec.write("var VNC_frame_data = [\n") + + try: + self.new_websocket_client() + except self.CClose: + # Close the client + _, exc, _ = sys.exc_info() + self.send_close(exc.args[0], exc.args[1]) + + def do_GET(self): + if self.web_auth: + # ensure connection is authorized, this seems to apply to list_directory() as well + self.auth_connection() + + if self.only_upgrade: + self.send_error(405) + else: + super().do_GET() + + def list_directory(self, path): + if self.file_only: + self.send_error(404) + else: + return super().list_directory(path) + + def new_websocket_client(self): + """ Do something with a WebSockets client connection. """ + raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded") + + def validate_connection(self): + """ Ensure that the connection has a valid token, and set the target. """ + pass + + def auth_connection(self): + """ Ensure that the connection is authorized. """ + pass + + def do_HEAD(self): + if self.web_auth: + self.auth_connection() + + if self.only_upgrade: + self.send_error(405) + else: + super().do_HEAD() + + def finish(self): + if self.rec: + self.rec.write("'EOF'];\n") + self.rec.close() + super().finish() + + def handle(self): + # When using run_once, we have a single process, so + # we cannot loop in BaseHTTPRequestHandler.handle; we + # must return and handle new connections + if self.run_once: + self.handle_one_request() + else: + super().handle() + + def log_request(self, code='-', size='-'): + if self.verbose: + super().log_request(code, size) + + +class WebSockifyServer(): + """ + WebSockets server class. + As an alternative, the standard library SocketServer can be used + """ + + policy_response = """\n""" + log_prefix = "websocket" + + # An exception before the WebSocket connection was established + class EClose(Exception): + pass + + class Terminate(Exception): + pass + + def __init__(self, RequestHandlerClass, listen_fd=None, + listen_host='', listen_port=None, source_is_ipv6=False, + verbose=False, cert='', key='', key_password=None, ssl_only=None, + verify_client=False, cafile=None, + daemon=False, record='', web='', web_auth=False, + 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, ssl_ciphers=None, ssl_options=0, + unix_listen=None, unix_listen_mode=None): + + # settings + self.RequestHandlerClass = RequestHandlerClass + self.verbose = verbose + self.listen_fd = listen_fd + self.unix_listen = unix_listen + self.unix_listen_mode = unix_listen_mode + self.listen_host = listen_host + self.listen_port = listen_port + self.prefer_ipv6 = source_is_ipv6 + self.ssl_only = ssl_only + self.ssl_ciphers = ssl_ciphers + self.ssl_options = ssl_options + self.verify_client = verify_client + self.daemon = daemon + self.run_once = run_once + self.timeout = timeout + self.idle_timeout = idle_timeout + self.traffic = traffic + self.file_only = file_only + self.web_auth = web_auth + + self.launch_time = time.time() + self.ws_connection = False + self.handler_id = 1 + self.terminating = False + + self.logger = self.get_logger() + self.tcp_keepalive = tcp_keepalive + self.tcp_keepcnt = tcp_keepcnt + self.tcp_keepidle = tcp_keepidle + self.tcp_keepintvl = tcp_keepintvl + + # keyfile path must be None if not specified + self.key = None + self.key_password = key_password + + # Make paths settings absolute + self.cert = os.path.abspath(cert) + self.web = self.record = self.cafile = '' + if key: + self.key = os.path.abspath(key) + if web: + self.web = os.path.abspath(web) + if record: + self.record = os.path.abspath(record) + if cafile: + self.cafile = os.path.abspath(cafile) + + if self.web: + os.chdir(self.web) + self.only_upgrade = not self.web + + # Sanity checks + if not ssl and self.ssl_only: + raise Exception("No 'ssl' module and SSL-only specified") + if self.daemon and not resource: + raise Exception("Module 'resource' required to daemonize") + + # Show configuration + self.msg("WebSocket server settings:") + if self.listen_fd != None: + self.msg(" - Listen for inetd connections") + elif self.unix_listen != None: + self.msg(" - Listen on unix socket %s", self.unix_listen) + else: + self.msg(" - Listen on %s:%s", + self.listen_host, self.listen_port) + if 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") + if self.ssl_only: + self.msg(" - Deny non-SSL/TLS connections") + else: + self.msg(" - No SSL/TLS support (no cert file)") + else: + self.msg(" - No SSL/TLS support (no 'ssl' module)") + if self.daemon: + self.msg(" - Backgrounding (daemon)") + if self.record: + self.msg(" - Recording to '%s.*'", self.record) + + # + # WebSockifyServer static methods + # + + @staticmethod + def get_logger(): + return logging.getLogger("%s.%s" % ( + WebSockifyServer.log_prefix, + WebSockifyServer.__class__.__name__)) + + @staticmethod + def socket(host, port=None, connect=False, prefer_ipv6=False, + unix_socket=None, unix_socket_mode=None, unix_socket_listen=False, + use_ssl=False, tcp_keepalive=True, tcp_keepcnt=None, + tcp_keepidle=None, tcp_keepintvl=None): + """ Resolve a host (and optional port) to an IPv4 or IPv6 + address. Create a socket. Bind to it if listen is set, + otherwise connect to it. Return the socket. + """ + flags = 0 + if host == '': + host = None + if connect and not (port or unix_socket): + raise Exception("Connect mode requires a port") + if use_ssl and not ssl: + raise Exception("SSL socket requested but Python SSL module not loaded."); + if not connect and use_ssl: + raise Exception("SSL only supported in connect mode (for now)") + if not connect: + flags = flags | socket.AI_PASSIVE + + if not unix_socket: + addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, + socket.IPPROTO_TCP, flags) + if not addrs: + raise Exception("Could not resolve host '%s'" % host) + addrs.sort(key=lambda x: x[0]) + if prefer_ipv6: + addrs.reverse() + sock = socket.socket(addrs[0][0], addrs[0][1]) + + if tcp_keepalive: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + if tcp_keepcnt: + sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, + tcp_keepcnt) + if tcp_keepidle: + sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, + tcp_keepidle) + if tcp_keepintvl: + sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, + tcp_keepintvl) + + if connect: + sock.connect(addrs[0][4]) + if use_ssl: + sock = ssl.wrap_socket(sock) + else: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(addrs[0][4]) + sock.listen(100) + else: + if unix_socket_listen: + # Make sure the socket does not already exist + try: + os.unlink(unix_socket) + except FileNotFoundError: + pass + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + oldmask = os.umask(0o777 ^ unix_socket_mode) + try: + sock.bind(unix_socket) + finally: + os.umask(oldmask) + sock.listen(100) + else: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(unix_socket) + + return sock + + @staticmethod + def daemonize(keepfd=None, chdir='/'): + + if keepfd is None: + keepfd = [] + + os.umask(0) + if chdir: + os.chdir(chdir) + else: + os.chdir('/') + os.setgid(os.getgid()) # relinquish elevations + os.setuid(os.getuid()) # relinquish elevations + + # Double fork to daemonize + if os.fork() > 0: os._exit(0) # Parent exits + os.setsid() # Obtain new process group + if os.fork() > 0: os._exit(0) # Parent exits + + # Signal handling + signal.signal(signal.SIGTERM, signal.SIG_IGN) + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # Close open files + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if maxfd == resource.RLIM_INFINITY: maxfd = 256 + for fd in reversed(range(maxfd)): + try: + if fd not in keepfd: + os.close(fd) + except OSError: + _, exc, _ = sys.exc_info() + if exc.errno != errno.EBADF: raise + + # Redirect I/O to /dev/null + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno()) + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno()) + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno()) + + def do_handshake(self, sock, address): + """ + do_handshake does the following: + - Peek at the first few bytes from the socket. + - If the connection is an HTTPS/SSL/TLS connection then SSL + wrap the socket. + - Read from the (possibly wrapped) socket. + - If we have received a HTTP GET request and the webserver + functionality is enabled, answer it, close the socket and + return. + - Assume we have a WebSockets connection, parse the client + handshake data. + - Send a WebSockets handshake server response. + - Return the socket for this WebSocket client. + """ + ready = select.select([sock], [], [], 3)[0] + + if not ready: + raise self.EClose("") + # Peek, but do not read the data so that we have a opportunity + # to SSL wrap the socket first + handshake = sock.recv(1024, socket.MSG_PEEK) + #self.msg("Handshake [%s]" % handshake) + + if not handshake: + raise self.EClose("") + + elif handshake[0] in (22, 128): + # SSL wrap the connection + if not ssl: + raise self.EClose("SSL connection but no 'ssl' module") + if not os.path.exists(self.cert): + raise self.EClose("SSL connection but '%s' not found" + % self.cert) + retsock = None + try: + # create new-style SSL wrapping for extended features + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + if self.ssl_ciphers is not None: + context.set_ciphers(self.ssl_ciphers) + context.options = self.ssl_options + context.load_cert_chain(certfile=self.cert, keyfile=self.key, password=self.key_password) + if self.verify_client: + context.verify_mode = ssl.CERT_REQUIRED + if self.cafile: + context.load_verify_locations(cafile=self.cafile) + else: + context.set_default_verify_paths() + retsock = context.wrap_socket( + sock, + server_side=True) + except ssl.SSLError: + _, x, _ = sys.exc_info() + if x.args[0] == ssl.SSL_ERROR_EOF: + if len(x.args) > 1: + raise self.EClose(x.args[1]) + else: + raise self.EClose("Got SSL_ERROR_EOF") + else: + raise + + elif self.ssl_only: + raise self.EClose("non-SSL connection received but disallowed") + + else: + retsock = sock + + # If the address is like (host, port), we are extending it + # with a flag indicating SSL. Not many other options + # available... + if len(address) == 2: + address = (address[0], address[1], (retsock != sock)) + + self.RequestHandlerClass(retsock, address, self) + + # Return the WebSockets socket which may be SSL wrapped + return retsock + + # + # WebSockifyServer logging/output functions + # + + def msg(self, *args, **kwargs): + """ Output message as info """ + self.logger.log(logging.INFO, *args, **kwargs) + + def vmsg(self, *args, **kwargs): + """ Same as msg() but as debug. """ + self.logger.log(logging.DEBUG, *args, **kwargs) + + def warn(self, *args, **kwargs): + """ Same as msg() but as warning. """ + self.logger.log(logging.WARN, *args, **kwargs) + + + # + # Events that can/should be overridden in sub-classes + # + def started(self): + """ Called after WebSockets startup """ + self.vmsg("WebSockets server started") + + def poll(self): + """ Run periodically while waiting for connections. """ + #self.vmsg("Running poll()") + pass + + def terminate(self): + if not self.terminating: + self.terminating = True + raise self.Terminate() + + def multiprocessing_SIGCHLD(self, sig, stack): + # 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) + # 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]: + self.vmsg("Reaped child process %s" % result[0]) + result = os.waitpid(-1, os.WNOHANG) + except (OSError): + pass + + def do_SIGINT(self, sig, stack): + # 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): + # 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 + client = None + try: + try: + client = self.do_handshake(startsock, address) + except self.EClose: + _, exc, _ = sys.exc_info() + # Connection was not a WebSockets connection + if exc.args[0]: + self.msg("%s: %s" % (address[0], exc.args[0])) + except WebSockifyServer.Terminate: + raise + except Exception: + _, exc, _ = sys.exc_info() + self.msg("handler exception: %s" % str(exc)) + self.vmsg("exception", exc_info=True) + finally: + + if client and client != startsock: + # Close the SSL wrapped socket + # 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 + do_handshake() method for each connection. If the connection + is a WebSockets client then call new_websocket_client() method (which must + be overridden) for each new client connection. + """ + + if self.listen_fd != None: + lsock = socket.fromfd(self.listen_fd, socket.AF_INET, socket.SOCK_STREAM) + elif self.unix_listen != None: + lsock = self.socket(host=None, + unix_socket=self.unix_listen, + unix_socket_mode=self.unix_listen_mode, + unix_socket_listen=True) + else: + lsock = self.socket(self.listen_host, self.listen_port, False, + self.prefer_ipv6, + tcp_keepalive=self.tcp_keepalive, + tcp_keepcnt=self.tcp_keepcnt, + tcp_keepidle=self.tcp_keepidle, + tcp_keepintvl=self.tcp_keepintvl) + + if self.daemon: + 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 + + # Allow override of signals + original_signals = { + signal.SIGINT: signal.getsignal(signal.SIGINT), + signal.SIGTERM: signal.getsignal(signal.SIGTERM), + } + if getattr(signal, 'SIGCHLD', None) is not None: + original_signals[signal.SIGCHLD] = signal.getsignal(signal.SIGCHLD) + signal.signal(signal.SIGINT, self.do_SIGINT) + signal.signal(signal.SIGTERM, self.do_SIGTERM) + # make sure that _cleanup is called when children die + # by calling active_children on SIGCHLD + if getattr(signal, 'SIGCHLD', None) is not None: + signal.signal(signal.SIGCHLD, self.multiprocessing_SIGCHLD) + + last_active_time = self.launch_time + try: + while True: + try: + try: + startsock = None + pid = err = 0 + child_count = 0 + + # Collect zombie child processes + child_count = len(multiprocessing.active_children()) + + time_elapsed = time.time() - self.launch_time + if self.timeout and time_elapsed > self.timeout: + self.msg('listener exit due to --timeout %s' + % self.timeout) + break + + if self.idle_timeout: + idle_time = 0 + if child_count == 0: + idle_time = time.time() - last_active_time + else: + idle_time = 0 + last_active_time = time.time() + + if idle_time > self.idle_timeout and child_count == 0: + self.msg('listener exit due to --idle-timeout %s' + % self.idle_timeout) + break + + try: + self.poll() + + ready = select.select([lsock], [], [], 1)[0] + if lsock in ready: + startsock, address = lsock.accept() + # Unix Socket will not report address (empty string), but address[0] is logged a bunch + if self.unix_listen != None: + address = [ self.unix_listen ] + else: + continue + except self.Terminate: + raise + except Exception: + _, exc, _ = sys.exc_info() + if hasattr(exc, 'errno'): + err = exc.errno + elif hasattr(exc, 'args'): + err = exc.args[0] + else: + err = exc[0] + if err == errno.EINTR: + self.vmsg("Ignoring interrupted syscall") + continue + else: + raise + + if self.run_once: + # Run in same process if run_once + self.top_new_client(startsock, address) + if self.ws_connection : + self.msg('%s: exiting due to --run-once' + % address[0]) + break + else: + self.vmsg('%s: new handler Process' % address[0]) + p = multiprocessing.Process( + target=self.top_new_client, + args=(startsock, address)) + p.start() + # child will not return + + # parent process + self.handler_id += 1 + + except (self.Terminate, SystemExit, KeyboardInterrupt): + self.msg("In exit") + # terminate all child processes + if 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) + + finally: + if startsock: + startsock.close() + finally: + # Close listen port + self.vmsg("Closing socket listening at %s:%s", + self.listen_host, self.listen_port) + lsock.close() + + # Restore signals + for sig, func in original_signals.items(): + signal.signal(sig, func) + +