mirror of
				https://github.com/optim-enterprises-bv/control-pane.git
				synced 2025-10-30 17:37:59 +00:00 
			
		
		
		
	re-added websockify
This commit is contained in:
		
							
								
								
									
										2
									
								
								public/novnc/utils/websockify/websockify/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								public/novnc/utils/websockify/websockify/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| from websockify.websocket import * | ||||
| from websockify.websocketproxy import * | ||||
							
								
								
									
										4
									
								
								public/novnc/utils/websockify/websockify/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								public/novnc/utils/websockify/websockify/__main__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| import websockify | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     websockify.websocketproxy.websockify_init() | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										102
									
								
								public/novnc/utils/websockify/websockify/auth_plugins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								public/novnc/utils/websockify/websockify/auth_plugins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										118
									
								
								public/novnc/utils/websockify/websockify/sysloghandler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								public/novnc/utils/websockify/websockify/sysloghandler.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										316
									
								
								public/novnc/utils/websockify/websockify/token_plugins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								public/novnc/utils/websockify/websockify/token_plugins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <host>[:<port>[:<db>[:<password>]]]" % | ||||
|                          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 | ||||
							
								
								
									
										874
									
								
								public/novnc/utils/websockify/websockify/websocket.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										874
									
								
								public/novnc/utils/websockify/websockify/websocket.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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('<u4') | ||||
|                 if sys.byteorder == 'big': | ||||
|                     dtype = dtype.newbyteorder('>') | ||||
|                 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 | ||||
|  | ||||
							
								
								
									
										800
									
								
								public/novnc/utils/websockify/websockify/websocketproxy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										800
									
								
								public/novnc/utils/websockify/websockify/websocketproxy.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
							
								
								
									
										110
									
								
								public/novnc/utils/websockify/websockify/websocketserver.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								public/novnc/utils/websockify/websockify/websocketserver.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										862
									
								
								public/novnc/utils/websockify/websockify/websockifyserver.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										862
									
								
								public/novnc/utils/websockify/websockify/websockifyserver.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 = """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\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) | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 olevole
					olevole