diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..c481949 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,19 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Update pip and setuptools + run: | + python -m pip install --upgrade pip + python -m pip install setuptools + - name: Install dependencies + run: | + python -m pip install flake8 + - name: Lint with flake8 + run: | + flake8 diff --git a/setup.py b/setup.py index 9ea3443..4e20f72 100644 --- a/setup.py +++ b/setup.py @@ -1,45 +1,46 @@ -from setuptools import setup, find_packages +from setuptools import setup version = '0.13.0' name = 'websockify' long_description = open("README.md").read() + "\n" + \ open("CHANGES.txt").read() + "\n" -setup(name=name, - version=version, - description="Websockify.", - long_description=long_description, - long_description_content_type="text/markdown", - classifiers=[ - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - python_requires='>=3.6', - keywords='noVNC websockify', - license='LGPLv3', - url="https://github.com/novnc/websockify", - author="Joel Martin", - author_email="github@martintribe.org", - - packages=['websockify'], - include_package_data=True, - install_requires=[ - 'numpy', 'requests', +setup( + name=name, + version=version, + description="Websockify.", + long_description=long_description, + long_description_content_type="text/markdown", + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + python_requires='>=3.6', + keywords='noVNC websockify', + license='LGPLv3', + url="https://github.com/novnc/websockify", + author="Joel Martin", + author_email="github@martintribe.org", + packages=['websockify'], + include_package_data=True, + install_requires=[ + 'numpy', + 'requests', 'jwcrypto', 'redis', - ], - zip_safe=False, - entry_points={ + ], + zip_safe=False, + entry_points={ 'console_scripts': [ 'websockify = websockify.websocketproxy:websockify_init', ] - }, - ) + }, +) diff --git a/tests/echo.py b/tests/echo.py index 780891c..4df6011 100755 --- a/tests/echo.py +++ b/tests/echo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python - +# flake8: noqa: E402 ''' A WebSocket server that echos back whatever it receives from the client. Copyright 2010 Joel Martin @@ -10,9 +10,17 @@ 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, select, optparse, logging -sys.path.insert(0,os.path.join(os.path.dirname(__file__), "..")) -from websockify.websockifyserver import WebSockifyServer, WebSockifyRequestHandler +import logging +import optparse +import os +import select +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from websockify.websockifyserver import WebSockifyServer +from websockify.websockifyserver import WebSockifyRequestHandler + class WebSocketEcho(WebSockifyRequestHandler): """ @@ -27,15 +35,17 @@ class WebSocketEcho(WebSockifyRequestHandler): cqueue = [] c_pend = 0 - cpartial = "" + cpartial = "" # noqa: F841 rlist = [self.request] while True: wlist = [] - if cqueue or c_pend: wlist.append(self.request) + if cqueue or c_pend: + wlist.append(self.request) ins, outs, excepts = select.select(rlist, wlist, [], 1) - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") if self.request in outs: # Send queued target data to the client @@ -50,20 +60,22 @@ class WebSocketEcho(WebSockifyRequestHandler): if closed: break + if __name__ == '__main__': parser = optparse.OptionParser(usage="%prog [options] listen_port") parser.add_option("--verbose", "-v", action="store_true", - help="verbose messages and per frame traffic") + help="verbose messages and per frame traffic") parser.add_option("--cert", default="self.pem", - help="SSL certificate file") + help="SSL certificate file") parser.add_option("--key", default=None, - help="SSL key file (if separate from cert)") + help="SSL key file (if separate from cert)") parser.add_option("--ssl-only", action="store_true", - help="disallow non-encrypted connections") + help="disallow non-encrypted connections") (opts, args) = parser.parse_args() try: - if len(args) != 1: raise ValueError + if len(args) != 1: + raise ValueError opts.listen_port = int(args[0]) except ValueError: parser.error("Invalid arguments") @@ -73,4 +85,3 @@ if __name__ == '__main__': opts.web = "." server = WebSockifyServer(WebSocketEcho, **opts.__dict__) server.start_server() - diff --git a/tests/echo_client.py b/tests/echo_client.py index 4f238f6..dbc799d 100755 --- a/tests/echo_client.py +++ b/tests/echo_client.py @@ -1,13 +1,16 @@ #!/usr/bin/env python +# flake8: noqa: E402 -import os -import sys import optparse +import os import select +import sys -sys.path.insert(0,os.path.join(os.path.dirname(__file__), "..")) -from websockify.websocket import WebSocket, \ - WebSocketWantReadError, WebSocketWantWriteError +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from websockify.websocket import WebSocket +from websockify.websocket import WebSocketWantReadError +from websockify.websocket import WebSocketWantWriteError parser = optparse.OptionParser(usage="%prog URL") (opts, args) = parser.parse_args() @@ -22,6 +25,7 @@ print("Connecting to %s..." % URL) sock.connect(URL) print("Connected.") + def send(msg): while True: try: @@ -30,11 +34,14 @@ def send(msg): except WebSocketWantReadError: msg = '' ins, outs, excepts = select.select([sock], [], []) - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") except WebSocketWantWriteError: msg = '' ins, outs, excepts = select.select([], [sock], []) - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") + def read(): while True: @@ -42,10 +49,13 @@ def read(): return sock.recvmsg() except WebSocketWantReadError: ins, outs, excepts = select.select([sock], [], []) - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") except WebSocketWantWriteError: ins, outs, excepts = select.select([], [sock], []) - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") + counter = 1 while True: @@ -56,7 +66,8 @@ while True: while True: ins, outs, excepts = select.select([sock], [], [], 1.0) - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") if ins == []: break diff --git a/tests/load.py b/tests/load.py index 710b593..8f0fc79 100755 --- a/tests/load.py +++ b/tests/load.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# flake8: noqa: E402 ''' WebSocket server-side load test program. Sends and receives traffic @@ -6,9 +7,19 @@ that has a random payload (length and content) that is checksummed and given a sequence number. Any errors are reported and counted. ''' -import sys, os, select, random, time, optparse, logging -sys.path.insert(0,os.path.join(os.path.dirname(__file__), "..")) -from websockify.websockifyserver import WebSockifyServer, WebSockifyRequestHandler +import logging +import optparse +import os +import random +import select +import sys +import time + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from websockify.websockifyserver import WebSockifyRequestHandler +from websockify.websockifyserver import WebSockifyServer + class WebSocketLoadServer(WebSockifyServer): @@ -26,7 +37,7 @@ class WebSocketLoad(WebSockifyRequestHandler): max_packet_size = 10000 def new_websocket_client(self): - print "Prepopulating random array" + print("Prepopulating random array") self.rand_array = [] for i in range(0, self.max_packet_size): self.rand_array.append(random.randint(0, 9)) @@ -37,19 +48,18 @@ class WebSocketLoad(WebSockifyRequestHandler): self.responder(self.request) - print "accumulated errors:", self.errors + print("accumulated errors:", self.errors) self.errors = 0 def responder(self, client): c_pend = 0 - cqueue = [] - cpartial = "" socks = [client] last_send = time.time() * 1000 while True: ins, outs, excepts = select.select(socks, socks, socks, 1) - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") if client in ins: frames, closed = self.recv_frames() @@ -57,7 +67,7 @@ class WebSocketLoad(WebSockifyRequestHandler): err = self.check(frames) if err: self.errors = self.errors + 1 - print err + print(err) if closed: break @@ -73,19 +83,14 @@ class WebSocketLoad(WebSockifyRequestHandler): def generate(self): length = random.randint(10, self.max_packet_size) - numlist = self.rand_array[self.max_packet_size-length:] - # Error in length - #numlist.append(5) + numlist = self.rand_array[self.max_packet_size - length:] chksum = sum(numlist) - # Error in checksum - #numlist[0] = 5 - nums = "".join( [str(n) for n in numlist] ) + nums = "".join([str(n) for n in numlist]) data = "^%d:%d:%d:%s$" % (self.send_cnt, length, chksum, nums) self.send_cnt += 1 return data - def check(self, frames): err = "" @@ -102,11 +107,11 @@ class WebSocketLoad(WebSockifyRequestHandler): try: cnt, length, chksum, nums = data[1:-1].split(':') - cnt = int(cnt) + cnt = int(cnt) length = int(length) chksum = int(chksum) except ValueError: - print "\n" + repr(data) + "" + print("\n" + repr(data) + "") err += "Invalid data format\n" continue @@ -138,20 +143,22 @@ class WebSocketLoad(WebSockifyRequestHandler): if __name__ == '__main__': parser = optparse.OptionParser(usage="%prog [options] listen_port") parser.add_option("--verbose", "-v", action="store_true", - help="verbose messages and per frame traffic") + help="verbose messages and per frame traffic") parser.add_option("--cert", default="self.pem", - help="SSL certificate file") + help="SSL certificate file") parser.add_option("--key", default=None, - help="SSL key file (if separate from cert)") + help="SSL key file (if separate from cert)") parser.add_option("--ssl-only", action="store_true", - help="disallow non-encrypted connections") + help="disallow non-encrypted connections") (opts, args) = parser.parse_args() try: - if len(args) != 1: raise ValueError + if len(args) != 1: + raise ValueError opts.listen_port = int(args[0]) - if len(args) not in [1,2]: raise ValueError + if len(args) not in [1, 2]: + raise ValueError opts.listen_port = int(args[0]) if len(args) == 2: opts.delay = int(args[1]) @@ -165,4 +172,3 @@ if __name__ == '__main__': opts.web = "." server = WebSocketLoadServer(WebSocketLoad, **opts.__dict__) server.start_server() - diff --git a/tests/test_token_plugins.py b/tests/test_token_plugins.py index 1e8e7e5..c058023 100644 --- a/tests/test_token_plugins.py +++ b/tests/test_token_plugins.py @@ -7,8 +7,14 @@ import unittest from unittest.mock import patch, MagicMock from jwcrypto import jwt, jwk +try: + import redis +except ImportError: + redis = None + from websockify.token_plugins import parse_source_args, ReadOnlyTokenFile, JWTTokenApi, TokenRedis + class ParseSourceArgumentsTestCase(unittest.TestCase): def test_parameterized(self): params = [ @@ -31,6 +37,7 @@ class ParseSourceArgumentsTestCase(unittest.TestCase): for src, args in params: self.assertEqual(args, parse_source_args(src)) + class ReadOnlyTokenFileTestCase(unittest.TestCase): def test_empty(self): mock_source_file = MagicMock() @@ -143,7 +150,7 @@ class JWSTokenTestCase(unittest.TestCase): key = jwk.JWK() private_key = open("./tests/fixtures/private.pem", "rb").read() key.import_from_pem(private_key) - jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 }) + jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200}) jwt_token.make_signed_token(key) mock_time.return_value = 150 @@ -160,7 +167,7 @@ class JWSTokenTestCase(unittest.TestCase): key = jwk.JWK() private_key = open("./tests/fixtures/private.pem", "rb").read() key.import_from_pem(private_key) - jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 }) + jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200}) jwt_token.make_signed_token(key) mock_time.return_value = 50 @@ -175,7 +182,7 @@ class JWSTokenTestCase(unittest.TestCase): key = jwk.JWK() private_key = open("./tests/fixtures/private.pem", "rb").read() key.import_from_pem(private_key) - jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 }) + jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200}) jwt_token.make_signed_token(key) mock_time.return_value = 250 @@ -188,7 +195,7 @@ class JWSTokenTestCase(unittest.TestCase): secret = open("./tests/fixtures/symmetric.key").read() key = jwk.JWK() - key.import_key(kty="oct",k=secret) + key.import_key(kty="oct", k=secret) jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token.make_signed_token(key) @@ -203,7 +210,7 @@ class JWSTokenTestCase(unittest.TestCase): secret = open("./tests/fixtures/symmetric.key").read() key = jwk.JWK() - key.import_key(kty="oct",k=secret) + key.import_key(kty="oct", k=secret) jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token.make_signed_token(key) @@ -223,7 +230,7 @@ class JWSTokenTestCase(unittest.TestCase): jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token.make_signed_token(private_key) jwe_token = jwt.JWT(header={"alg": "RSA-OAEP", "enc": "A256CBC-HS512"}, - claims=jwt_token.serialize()) + claims=jwt_token.serialize()) jwe_token.make_encrypted_token(public_key) result = plugin.lookup(jwt_token.serialize()) @@ -232,11 +239,10 @@ class JWSTokenTestCase(unittest.TestCase): self.assertEqual(result[0], "remote_host") self.assertEqual(result[1], "remote_port") + class TokenRedisTestCase(unittest.TestCase): def setUp(self): - try: - import redis - except ImportError: + if redis is None: patcher = patch.dict(sys.modules, {'redis': MagicMock()}) patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 8ee44f9..7f89312 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -18,6 +18,7 @@ import unittest from websockify import websocket + class FakeSocket: def __init__(self): self.data = b'' @@ -26,6 +27,7 @@ class FakeSocket: self.data += buf return len(buf) + class AcceptTestCase(unittest.TestCase): def test_success(self): ws = websocket.WebSocket() @@ -81,7 +83,7 @@ class AcceptTestCase(unittest.TestCase): ws.accept(sock, {'upgrade': 'websocket', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q==', - 'Sec-WebSocket-Protocol': 'foobar gazonk'}) + 'Sec-WebSocket-Protocol': 'foobar,gazonk'}) self.assertEqual(sock.data[:13], b'HTTP/1.1 101 ') self.assertTrue(b'\r\nSec-WebSocket-Protocol: gazonk\r\n' in sock.data) @@ -101,9 +103,9 @@ class AcceptTestCase(unittest.TestCase): sock, {'upgrade': 'websocket', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q==', - 'Sec-WebSocket-Protocol': 'foobar gazonk'}) + 'Sec-WebSocket-Protocol': 'foobar,gazonk'}) - def test_protocol(self): + def test_unsupported_protocol(self): class ProtoSocket(websocket.WebSocket): def select_subprotocol(self, protocol): return 'oddball' @@ -114,7 +116,8 @@ class AcceptTestCase(unittest.TestCase): sock, {'upgrade': 'websocket', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q==', - 'Sec-WebSocket-Protocol': 'foobar gazonk'}) + 'Sec-WebSocket-Protocol': 'foobar,gazonk'}) + class PingPongTest(unittest.TestCase): def setUp(self): @@ -142,6 +145,7 @@ class PingPongTest(unittest.TestCase): self.ws.pong(b'foo') self.assertEqual(self.sock.data, b'\x8a\x03foo') + class HyBiEncodeDecodeTestCase(unittest.TestCase): def test_decode_hybi_text(self): buf = b'\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58' diff --git a/tests/test_websocketproxy.py b/tests/test_websocketproxy.py index e56bef7..2da8b08 100644 --- a/tests/test_websocketproxy.py +++ b/tests/test_websocketproxy.py @@ -16,8 +16,6 @@ """ Unit tests for websocketproxy """ -import sys -import unittest import unittest import socket from io import StringIO @@ -58,6 +56,7 @@ class FakeServer: self.ssl_target = None self.unix_target = None + class ProxyRequestHandlerTestCase(unittest.TestCase): def setUp(self): super().setUp() @@ -128,4 +127,3 @@ class ProxyRequestHandlerTestCase(unittest.TestCase): self.handler.server.target_host = "someotherhost" self.handler.auth_connection() - diff --git a/tests/test_websocketserver.py b/tests/test_websocketserver.py index 0e37e3d..8822572 100644 --- a/tests/test_websocketserver.py +++ b/tests/test_websocketserver.py @@ -66,4 +66,3 @@ class HttpWebSocketTest(unittest.TestCase): # Then req_obj.end_headers.assert_called_once_with() - diff --git a/tests/test_websockifyserver.py b/tests/test_websockifyserver.py index dad55ac..a9a554c 100644 --- a/tests/test_websockifyserver.py +++ b/tests/test_websockifyserver.py @@ -17,18 +17,12 @@ """ Unit tests for websockifyserver """ import errno import os -import logging -import select -import shutil import socket import ssl -from unittest.mock import patch, MagicMock, ANY +from unittest.mock import patch import sys import tempfile import unittest -import socket -import signal -from http.server import BaseHTTPRequestHandler from io import StringIO from io import BytesIO @@ -237,6 +231,7 @@ class WebSockifyServerTestCase(unittest.TestCase): def test_do_handshake_no_ssl(self): class FakeHandler: CALLED = False + def __init__(self, *args, **kwargs): type(self).CALLED = True @@ -292,12 +287,16 @@ class WebSockifyServerTestCase(unittest.TestCase): def __init__(self, purpose): self.verify_mode = None self.options = 0 + def load_cert_chain(self, certfile, keyfile, password): pass + def set_default_verify_paths(self): pass + def load_verify_locations(self, cafile): pass + def wrap_socket(self, *args, **kwargs): raise ssl.SSLError(ssl.SSL_ERROR_EOF) @@ -314,7 +313,7 @@ class WebSockifyServerTestCase(unittest.TestCase): def __init__(self, *args, **kwargs): pass - server = self._get_server(handler_class=FakeHandler, daemon=True, + server = self._get_server(handler_class=FakeHandler, daemon=True, idle_timeout=1, ssl_ciphers=test_ciphers) sock = FakeSocket(b"\x16some ssl data") @@ -323,17 +322,23 @@ class WebSockifyServerTestCase(unittest.TestCase): class fake_create_default_context(): CIPHERS = '' + def __init__(self, purpose): self.verify_mode = None self.options = 0 + def load_cert_chain(self, certfile, keyfile, password): pass + def set_default_verify_paths(self): pass + def load_verify_locations(self, cafile): pass + def wrap_socket(self, *args, **kwargs): pass + def set_ciphers(self, ciphers_to_set): fake_create_default_context.CIPHERS = ciphers_to_set @@ -349,7 +354,7 @@ class WebSockifyServerTestCase(unittest.TestCase): def __init__(self, *args, **kwargs): pass - server = self._get_server(handler_class=FakeHandler, daemon=True, + server = self._get_server(handler_class=FakeHandler, daemon=True, idle_timeout=1, ssl_options=test_options) sock = FakeSocket(b"\x16some ssl data") @@ -358,19 +363,26 @@ class WebSockifyServerTestCase(unittest.TestCase): class fake_create_default_context: OPTIONS = 0 + def __init__(self, purpose): self.verify_mode = None self._options = 0 + def load_cert_chain(self, certfile, keyfile, password): pass + def set_default_verify_paths(self): pass + def load_verify_locations(self, cafile): pass + def wrap_socket(self, *args, **kwargs): pass + def get_options(self): return self._options + def set_options(self, val): fake_create_default_context.OPTIONS = val options = property(get_options, set_options) @@ -386,7 +398,6 @@ class WebSockifyServerTestCase(unittest.TestCase): def test_start_server_error(self): server = self._get_server(daemon=False, ssl_only=1, idle_timeout=1) - sock = server.socket('localhost') def fake_select(rlist, wlist, xlist, timeout=None): raise Exception("fake error") @@ -398,7 +409,6 @@ class WebSockifyServerTestCase(unittest.TestCase): def test_start_server_keyboardinterrupt(self): server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1) - sock = server.socket('localhost') def fake_select(rlist, wlist, xlist, timeout=None): raise KeyboardInterrupt @@ -410,7 +420,6 @@ class WebSockifyServerTestCase(unittest.TestCase): def test_start_server_systemexit(self): server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1) - sock = server.socket('localhost') def fake_select(rlist, wlist, xlist, timeout=None): sys.exit() diff --git a/tox.ini b/tox.ini index 8c629cf..f1a19e1 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,13 @@ deps = -r{toxinidir}/test-requirements.txt # At some point we should enable this since tox expects it to exist but # the code will need pep8ising first. -#[testenv:pep8] -#commands = flake8 -#dep = flake8 +[testenv:pep8] +commands = flake8 +deps = flake8 + +[flake8] +max-line-length = 160 +# E129 visually indented line with same indent as next logical line +# W503 line break before binary operator +# W504 line break after binary operator +ignore = E129,W503,W504 diff --git a/websockify/__init__.py b/websockify/__init__.py index 37a6f47..f9f8c7c 100644 --- a/websockify/__init__.py +++ b/websockify/__init__.py @@ -1,2 +1,2 @@ -from websockify.websocket import * -from websockify.websocketproxy import * +from websockify.websocket import * # noqa: F401,F403 +from websockify.websocketproxy import * # noqa: F401,F403 diff --git a/websockify/auth_plugins.py b/websockify/auth_plugins.py index 36fac52..dda188e 100644 --- a/websockify/auth_plugins.py +++ b/websockify/auth_plugins.py @@ -76,6 +76,7 @@ class BasicHTTPAuth(): raise AuthenticationError(response_code=401, response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'}) + class ExpectOrigin(): def __init__(self, src=None): if src is None: @@ -88,6 +89,7 @@ class ExpectOrigin(): 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.""" diff --git a/websockify/sysloghandler.py b/websockify/sysloghandler.py index 8b344a1..14e4f03 100644 --- a/websockify/sysloghandler.py +++ b/websockify/sysloghandler.py @@ -1,4 +1,7 @@ -import logging.handlers as handlers, socket, os, time +import logging.handlers as handlers +import os +import socket +import time class WebsockifySysLogHandler(handlers.SysLogHandler): @@ -13,14 +16,11 @@ class WebsockifySysLogHandler(handlers.SysLogHandler): _legacy = False _timestamp_fmt = '%Y-%m-%dT%H:%M:%SZ' _max_hostname = 255 - _max_ident = 24 #safer for old daemons + _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): @@ -46,7 +46,6 @@ class WebsockifySysLogHandler(handlers.SysLogHandler): super().__init__(address, facility, socktype) - def emit(self, record): """ Emit a record. @@ -58,13 +57,13 @@ class WebsockifySysLogHandler(handlers.SysLogHandler): try: # Gather info. text = self.format(record).replace(self._tail, ' ') - if not text: # nothing to log + if not text: # nothing to log return pri = self.encodePriority(self.facility, self.mapPriority(record.levelname)) - timestamp = time.strftime(self._timestamp_fmt, time.gmtime()); + timestamp = time.strftime(self._timestamp_fmt, time.gmtime()) hostname = socket.gethostname()[:self._max_hostname] @@ -73,7 +72,7 @@ class WebsockifySysLogHandler(handlers.SysLogHandler): else: ident = '' - pid = os.getpid() # shouldn't need truncation + pid = os.getpid() # shouldn't need truncation # Format the header. head = { @@ -112,7 +111,5 @@ class WebsockifySysLogHandler(handlers.SysLogHandler): else: self.socket.sendall(msg) - except (KeyboardInterrupt, SystemExit): - raise - except: + except Exception: self.handleError(record) diff --git a/websockify/token_plugins.py b/websockify/token_plugins.py index a0c1701..9fb413c 100644 --- a/websockify/token_plugins.py +++ b/websockify/token_plugins.py @@ -5,6 +5,11 @@ import re import json from pathlib import Path +try: + import redis +except ImportError: + redis = None + logger = logging.getLogger(__name__) _SOURCE_SPLIT_REGEX = re.compile( @@ -84,6 +89,7 @@ class TokenFile(ReadOnlyTokenFile): return super().lookup(token) + class TokenFileName(BasePlugin): # source is a directory # token is filename @@ -113,8 +119,8 @@ class BaseTokenAPI(BasePlugin): def process_result(self, resp): host, port = resp.text.split(':') - port = port.encode('ascii','ignore') - return [ host, port ] + port = port.encode('ascii', 'ignore') + return [host, port] def lookup(self, token): import requests @@ -156,10 +162,10 @@ class JWTTokenApi(BasePlugin): try: key.import_from_pem(key_data) - except: + except Exception: try: - key.import_key(k=key_data.decode('utf-8'),kty='oct') - except: + key.import_key(k=key_data.decode('utf-8'), kty='oct') + except Exception: logger.error('Failed to correctly parse key data!') return None @@ -255,9 +261,7 @@ class TokenRedis(BasePlugin): pip install redis """ def __init__(self, src): - try: - import redis - except ImportError: + if redis is None: logger.error("Unable to load redis module") sys.exit() # Default values @@ -305,7 +309,7 @@ class TokenRedis(BasePlugin): self._namespace += ":" logger.info("TokenRedis backend initialized (%s:%s)" % - (self._server, self._port)) + (self._server, self._port)) except ValueError: logger.error("The provided --token-source='%s' is not in the " "expected format [:[:[:[:]]]]" % @@ -313,9 +317,7 @@ class TokenRedis(BasePlugin): sys.exit() def lookup(self, token): - try: - import redis - except ImportError: + if redis is None: logger.error("package redis not found, are you sure you've installed them correctly?") sys.exit() @@ -372,7 +374,7 @@ class UnixDomainSocketDirectory(BasePlugin): if not stat.S_ISSOCK(uds_path.stat().st_mode): return None - return [ 'unix_socket', uds_path ] + return ['unix_socket', uds_path] except Exception as e: - logger.error("Error finding unix domain socket: %s" % str(e)) - return None + logger.error("Error finding unix domain socket: %s" % str(e)) + return None diff --git a/websockify/websocket.py b/websockify/websocket.py index 1bed8cc..54d4ee1 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -31,11 +31,15 @@ except ImportError: warnings.warn("no 'numpy' module, HyBi protocol will be slower") numpy = None + class WebSocketWantReadError(ssl.SSLWantReadError): pass + + class WebSocketWantWriteError(ssl.SSLWantWriteError): pass + class WebSocket: """WebSocket protocol socket like class. @@ -118,7 +122,7 @@ class WebSocket: connect() must retain the same arguments. """ - self.client = True; + self.client = True uri = urlparse(uri) @@ -206,7 +210,7 @@ class WebSocket: accept = headers.get('Sec-WebSocket-Accept') if accept is None: - raise Exception("Missing Sec-WebSocket-Accept header"); + raise Exception("Missing Sec-WebSocket-Accept header") expected = sha1((self._key + self.GUID).encode("ascii")).digest() expected = b64encode(expected).decode("ascii") @@ -214,7 +218,7 @@ class WebSocket: del self._key if accept != expected: - raise Exception("Invalid Sec-WebSocket-Accept header"); + raise Exception("Invalid Sec-WebSocket-Accept header") self.protocol = headers.get('Sec-WebSocket-Protocol') if len(protocols) == 0: @@ -258,7 +262,7 @@ class WebSocket: ver = headers.get('Sec-WebSocket-Version') if ver is None: - raise Exception("Missing Sec-WebSocket-Version header"); + raise Exception("Missing Sec-WebSocket-Version header") # HyBi-07 report version 7 # HyBi-08 - HyBi-12 report version 8 @@ -270,7 +274,7 @@ class WebSocket: key = headers.get('Sec-WebSocket-Key') if key is None: - raise Exception("Missing Sec-WebSocket-Key header"); + raise Exception("Missing Sec-WebSocket-Key header") # Generate the hash value for the accept header accept = sha1((key + self.GUID).encode("ascii")).digest() @@ -753,25 +757,22 @@ class WebSocket: # Unmask a frame if numpy: plen = len(buf) - pstart = 0 - pend = plen b = c = b'' if plen >= 4: - dtype=numpy.dtype('') mask = numpy.frombuffer(mask, dtype, count=1) data = numpy.frombuffer(buf, dtype, count=int(plen / 4)) - #b = numpy.bitwise_xor(data, mask).data b = numpy.bitwise_xor(data, mask).tobytes() if plen % 4: - dtype=numpy.dtype('B') + 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)) + offset=plen - (plen % 4), count=(plen % 4)) c = numpy.bitwise_xor(data, mask).tobytes() return b + c else: @@ -825,11 +826,11 @@ class WebSocket: 'payload' : decoded_buffer} """ - f = {'fin' : 0, - 'opcode' : 0, - 'masked' : False, - 'length' : 0, - 'payload' : None} + f = {'fin': 0, + 'opcode': 0, + 'masked': False, + 'length': 0, + 'payload': None} blen = len(buf) hlen = 2 @@ -867,10 +868,9 @@ class WebSocket: if f['masked']: # unmask payload - mask_key = buf[hlen-4:hlen] - f['payload'] = self._unmask(buf[hlen:(hlen+length)], mask_key) + mask_key = buf[hlen - 4:hlen] + f['payload'] = self._unmask(buf[hlen:(hlen + length)], mask_key) else: - f['payload'] = buf[hlen:(hlen+length)] + f['payload'] = buf[hlen:(hlen + length)] return f - diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 9f5dcf2..4410e8e 100644 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -11,14 +11,26 @@ 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 +import errno from http.server import HTTPServer - +import logging +import optparse +import os import select +import signal +import socket +from socketserver import ThreadingMixIn +import ssl +import stat +import subprocess +import sys +import time +from urllib.parse import parse_qs +from urllib.parse import urlparse + from websockify import websockifyserver from websockify import auth_plugins as auth -from urllib.parse import parse_qs, urlparse + class ProxyRequestHandler(websockifyserver.WebSockifyRequestHandler): @@ -62,7 +74,7 @@ Traffic Legend: # clear out any existing SSL_ headers that the client might # have maliciously set - ssl_headers = [ h for h in self.headers if h.startswith('SSL_') ] + ssl_headers = [h for h in self.headers if h.startswith('SSL_')] for h in ssl_headers: del self.headers[h] @@ -101,7 +113,7 @@ Traffic Legend: msg = "connecting to unix socket: %s" % self.server.unix_target else: msg = "connecting to: %s:%s" % ( - self.server.target_host, self.server.target_port) + self.server.target_host, self.server.target_port) if self.server.ssl_target: msg += " (using SSL)" @@ -109,10 +121,10 @@ Traffic Legend: 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) + 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) @@ -135,7 +147,7 @@ Traffic Legend: tsock.close() if self.verbose: self.log_message("%s:%s: Closed target", - self.server.target_host, self.server.target_port) + self.server.target_host, self.server.target_port) def get_target(self, target_plugin): """ @@ -159,7 +171,7 @@ Traffic Legend: else: # Extract the token parameter from url - args = parse_qs(urlparse(self.path)[4]) # 4 is the query 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') @@ -200,8 +212,10 @@ Traffic Legend: self.heartbeat = now + self.server.heartbeat self.send_ping() - if tqueue: wlist.append(target) - if cqueue or c_pend: wlist.append(self.request) + if tqueue: + wlist.append(target) + if cqueue or c_pend: + wlist.append(self.request) try: ins, outs, excepts = select.select(rlist, wlist, [], 1) except OSError: @@ -216,7 +230,8 @@ Traffic Legend: else: continue - if excepts: raise Exception("Socket exception") + if excepts: + raise Exception("Socket exception") if self.request in outs: # Send queued target data to the client @@ -245,10 +260,9 @@ Traffic Legend: # 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) + 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) @@ -260,7 +274,6 @@ Traffic Legend: 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) @@ -270,19 +283,20 @@ Traffic Legend: # Send queued target data to the client if len(cqueue) != 0: c_pend = True - while(c_pend): + 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) + 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 @@ -293,20 +307,20 @@ class WebSocketProxy(websockifyserver.WebSockifyServer): 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.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] + self.wrap_times = [0, 0, 0] if self.wrap_cmd: wsdir = os.path.dirname(sys.argv[0]) @@ -334,7 +348,7 @@ class WebSocketProxy(websockifyserver.WebSockifyServer): 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) ]) + ld_preloads = filter(None, [self.rebinder, os.environ.get("LD_PRELOAD", None)]) os.environ.update({ "LD_PRELOAD": os.pathsep.join(ld_preloads), @@ -348,7 +362,7 @@ class WebSocketProxy(websockifyserver.WebSockifyServer): 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.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup) self.spawn_message = True def started(self): @@ -364,7 +378,7 @@ class WebSocketProxy(websockifyserver.WebSockifyServer): else: dst_string = "%s:%s" % (self.target_host, self.target_port) - if self.listen_fd != None: + if self.listen_fd is not None: src_string = "inetd" else: src_string = "%s:%s" % (self.listen_host, self.listen_port) @@ -389,11 +403,11 @@ class WebSocketProxy(websockifyserver.WebSockifyServer): if self.wrap_cmd and self.cmd: ret = self.cmd.poll() - if ret != None: + if ret is not None: self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret) self.cmd = None - if self.wrap_cmd and self.cmd == None: + if self.wrap_cmd and self.cmd is None: # Response to wrapped command being gone if self.wrap_mode == "ignore": pass @@ -401,7 +415,7 @@ class WebSocketProxy(websockifyserver.WebSockifyServer): sys.exit(ret) elif self.wrap_mode == "respawn": now = time.time() - avg = sum(self.wrap_times)/len(self.wrap_times) + avg = sum(self.wrap_times) / len(self.wrap_times) if (now - avg) < 10: # 3 times in the last 10 seconds if self.spawn_message: @@ -427,6 +441,7 @@ SSL_OPTIONS = { 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""" @@ -444,6 +459,7 @@ def select_ssl_version(version): return SSL_OPTIONS[fallback] + def websockify_init(): # Setup basic logging to stderr. stderr_handler = logging.StreamHandler() @@ -465,72 +481,72 @@ def websockify_init(): usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" parser = optparse.OptionParser(usage=usage) parser.add_option("--verbose", "-v", action="store_true", - help="verbose messages") + help="verbose messages") parser.add_option("--traffic", action="store_true", - help="per frame traffic") + help="per frame traffic") parser.add_option("--record", - help="record sessions to FILE.[session_number]", metavar="FILE") + 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)") + 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") + help="handle a single WebSocket connection and exit") parser.add_option("--timeout", type=int, default=0, - help="after TIMEOUT seconds exit when not connected") + 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") + help="server exits after TIMEOUT seconds if there are no " + "active connections") parser.add_option("--cert", default="self.pem", - help="SSL certificate file") + help="SSL certificate file") parser.add_option("--key", default=None, - help="SSL key file (if separate from cert)") + help="SSL key file (if separate from cert)") parser.add_option("--key-password", default=None, - help="SSL key password") + help="SSL key password") parser.add_option("--ssl-only", action="store_true", - help="disallow non-encrypted client connections") + help="disallow non-encrypted client connections") parser.add_option("--ssl-target", action="store_true", - help="connect to SSL target as SSL client") + 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)") + 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.") + 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)") + 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`") + 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) + 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)") + help="specify mode for unix socket (defaults to 0600)") parser.add_option("--unix-target", - help="connect to unix socket target", metavar="FILE") + help="connect to unix socket target", metavar="FILE") parser.add_option("--inetd", - help="inetd mode, receive listening socket from stdin", action="store_true") + 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.") + help="run webserver on same port. Serve files from DIR.") parser.add_option("--web-auth", action="store_true", - help="require authentication to access webserver.") + 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") + 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") + 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") + 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)") + 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") @@ -547,13 +563,13 @@ def websockify_init(): 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") + 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") + 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.") + 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.") @@ -562,9 +578,7 @@ def websockify_init(): (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") @@ -583,11 +597,9 @@ def websockify_init(): 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) @@ -638,7 +650,6 @@ def websockify_init(): 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) @@ -655,7 +666,7 @@ def websockify_init(): opts.wrap_cmd = None if not websockifyserver.ssl and opts.ssl_target: - parser.error("SSL target requested and Python SSL module not loaded."); + 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) @@ -708,7 +719,7 @@ def websockify_init(): except ValueError: parser.error("Error parsing target port") - if len(args) > 0 and opts.wrap_cmd == None: + if len(args) > 0 and opts.wrap_cmd is None: parser.error("Too many arguments") if opts.token_plugin is not None: @@ -759,32 +770,32 @@ class LibProxyServer(ThreadingMixIn, HTTPServer): 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.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', '') + 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) + 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.run_once = kwargs.pop('run_once', False) self.handler_id = 0 for arg in kwargs.keys(): @@ -795,7 +806,6 @@ class LibProxyServer(ThreadingMixIn, HTTPServer): 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 diff --git a/websockify/websocketserver.py b/websockify/websocketserver.py index 4e62f2e..1592dc8 100644 --- a/websockify/websocketserver.py +++ b/websockify/websocketserver.py @@ -10,7 +10,8 @@ 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 +from websockify.websocket import WebSocket + class HttpWebSocket(WebSocket): """Class to glue websocket and http request functionality together""" @@ -93,18 +94,21 @@ class WebSocketRequestHandlerMixIn: def handle_websocket(self): """Handle a WebSocket connection. - + This is called when the WebSocket is ready to be used. A sub-class should perform the necessary communication here and return once done. """ pass + # Convenient ready made classes + class WebSocketRequestHandler(WebSocketRequestHandlerMixIn, BaseHTTPRequestHandler): pass + class WebSocketServer(HTTPServer): pass diff --git a/websockify/websockifyserver.py b/websockify/websockifyserver.py index 17770aa..fcb28a2 100644 --- a/websockify/websockifyserver.py +++ b/websockify/websockifyserver.py @@ -12,18 +12,29 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates ''' -import os, sys, time, errno, signal, socket, select, logging -import multiprocessing +import errno from http.server import SimpleHTTPRequestHandler +import logging +import multiprocessing +import os +import select +import signal +import socket +import sys +import time # 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)) +try: + import ssl +except ImportError: + ssl = None + print("WARNING: no 'ssl' module, TLS/SSL/wss is disabled") + +try: + import resource +except ImportError: + resource = None + print("WARNING: no 'resource' module, daemonizing is disabled") if sys.platform == 'win32': # make sockets pickle-able/inheritable @@ -32,6 +43,7 @@ if sys.platform == 'win32': 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 @@ -40,6 +52,7 @@ class CompatibleWebSocket(WebSocketRequestHandlerMixIn.SocketClass): else: return '' + # HTTP handler with WebSocket upgrade support class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHandler): """ @@ -73,7 +86,7 @@ class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHa self.daemon = getattr(server, "daemon", False) self.record = getattr(server, "record", False) self.run_once = getattr(server, "run_once", False) - self.rec = None + 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) @@ -124,7 +137,7 @@ class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHa fully sent, in which case the caller should call again when the socket is ready. """ - tdelta = int(time.time()*1000) - self.start_time + tdelta = int(time.time() * 1000) - self.start_time if bufs: for buf in bufs: @@ -155,7 +168,7 @@ class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHa closed = False bufs = [] - tdelta = int(time.time()*1000) - self.start_time + tdelta = int(time.time() * 1000) - self.start_time while True: try: @@ -207,8 +220,8 @@ class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHa self.server.ws_connection = True # Initialize per client settings self.send_parts = [] - self.recv_part = None - self.start_time = int(time.time()*1000) + self.recv_part = None + self.start_time = int(time.time() * 1000) # client_address is empty with, say, UNIX domain sockets client_addr = "" @@ -333,47 +346,47 @@ class WebSockifyServer(): 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): + 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.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.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 + 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 @@ -403,13 +416,13 @@ class WebSockifyServer(): # Show configuration self.msg("WebSocket server settings:") - if self.listen_fd != None: + if self.listen_fd is not None: self.msg(" - Listen for inetd connections") - elif self.unix_listen != None: + elif self.unix_listen is not None: self.msg(" - Listen on unix socket %s", self.unix_listen) else: self.msg(" - Listen on %s:%s", - self.listen_host, self.listen_port) + 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) @@ -442,7 +455,7 @@ class WebSockifyServer(): @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, + 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, @@ -454,7 +467,7 @@ class WebSockifyServer(): 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."); + 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: @@ -462,7 +475,7 @@ class WebSockifyServer(): if not unix_socket: addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, - socket.IPPROTO_TCP, flags) + socket.IPPROTO_TCP, flags) if not addrs: raise Exception("Could not resolve host '%s'" % host) addrs.sort(key=lambda x: x[0]) @@ -470,7 +483,7 @@ class WebSockifyServer(): addrs.reverse() sock = socket.socket(addrs[0][0], addrs[0][1]) - if tcp_keepalive: + if tcp_keepalive: sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) if tcp_keepcnt: sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, @@ -513,7 +526,7 @@ class WebSockifyServer(): @staticmethod def daemonize(keepfd=None, chdir='/'): - + if keepfd is None: keepfd = [] @@ -526,9 +539,11 @@ class WebSockifyServer(): 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 + if os.fork() > 0: # Parent exits + os._exit(0) + os.setsid() # Obtain new process group + if os.fork() > 0: # Parent exits + os._exit(0) # Signal handling signal.signal(signal.SIGTERM, signal.SIG_IGN) @@ -536,14 +551,16 @@ class WebSockifyServer(): # Close open files maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] - if maxfd == resource.RLIM_INFINITY: maxfd = 256 + 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 + if exc.errno != errno.EBADF: + raise # Redirect I/O to /dev/null os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno()) @@ -572,7 +589,6 @@ class WebSockifyServer(): # 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("") @@ -599,8 +615,8 @@ class WebSockifyServer(): else: context.set_default_verify_paths() retsock = context.wrap_socket( - sock, - server_side=True) + sock, + server_side=True) except ssl.SSLError: _, x, _ = sys.exc_info() if x.args[0] == ssl.SSL_ERROR_EOF: @@ -644,17 +660,16 @@ class WebSockifyServer(): """ Same as msg() but as warning. """ self.logger.log(logging.WARNING, *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): @@ -735,9 +750,9 @@ class WebSockifyServer(): """ try: - if self.listen_fd != None: + if self.listen_fd is not None: lsock = socket.fromfd(self.listen_fd, socket.AF_INET, socket.SOCK_STREAM) - elif self.unix_listen != None: + elif self.unix_listen is not None: lsock = self.socket(host=None, unix_socket=self.unix_listen, unix_socket_mode=self.unix_listen_mode, @@ -781,7 +796,7 @@ class WebSockifyServer(): try: try: startsock = None - pid = err = 0 + err = 0 child_count = 0 # Collect zombie child processes @@ -790,7 +805,7 @@ class WebSockifyServer(): 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) + % self.timeout) break if self.idle_timeout: @@ -803,7 +818,7 @@ class WebSockifyServer(): if idle_time > self.idle_timeout and child_count == 0: self.msg('listener exit due to --idle-timeout %s' - % self.idle_timeout) + % self.idle_timeout) break try: @@ -813,8 +828,8 @@ class WebSockifyServer(): 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 ] + if self.unix_listen is not None: + address = [self.unix_listen] else: continue except self.Terminate: @@ -836,15 +851,15 @@ class WebSockifyServer(): if self.run_once: # Run in same process if run_once self.top_new_client(startsock, address) - if self.ws_connection : + if self.ws_connection: self.msg('%s: exiting due to --run-once' - % address[0]) + % address[0]) break else: self.vmsg('%s: new handler Process' % address[0]) p = multiprocessing.Process( - target=self.top_new_client, - args=(startsock, address)) + target=self.top_new_client, + args=(startsock, address)) p.start() # child will not return @@ -879,5 +894,3 @@ class WebSockifyServer(): # Restore signals for sig, func in original_signals.items(): signal.signal(sig, func) - -