mirror of
https://github.com/optim-enterprises-bv/control-pane.git
synced 2025-10-29 09:02:21 +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