From 5bde6cea4db1cc1e54c358eaec75529ee23f9e4c Mon Sep 17 00:00:00 2001 From: Giuseppe Corbelli Date: Wed, 6 Mar 2019 10:54:19 +0100 Subject: [PATCH 1/6] [websocketproxy] Added a websockify.spec file to be used with pyinstaller to build a frozen distribution Explicit excludes have been disabled and comments added on how to proceed to re-enable them (see PR #384 discussion) --- websockify.spec | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 websockify.spec diff --git a/websockify.spec b/websockify.spec new file mode 100644 index 00000000..3f1d176d --- /dev/null +++ b/websockify.spec @@ -0,0 +1,102 @@ +# -*- mode: python -*- +# To be used with https://www.pyinstaller.org to build a "frozen" executable distribution +# Tested with pyinstaller 3.4, python 3.7.2, linux x86_64 +# NOT tested with SSL + +# Set to True to bring in numpy if available (increases output package size by ~45MB on linux64/python3.7). +# If False it will be left out +USE_NUMPY=True + +# Get debug messages by pyinstaller bootloader +DEBUG=False + +# Explicitly excluded modules +excludes = [] + +# It is possible to reduce the resulting package size (~3MB on linux64/python3.7) +# by excluding unused encodings. +# By default (as of pyinstaller 3.4/python 3.7) all encodings are pulled in: +# the following list can be used to reduce the encodings pulled in to the bare minimum. +# You should include the target platform encoding or call locale.setlocale(locale.LC_ALL, 'C') +# early in websockify_init() +# +# These are required: +# - 'encodings.base64_codec' This is required when payload is b64 encoded, don't exclude +# - 'encodings' Father package must also be included +# - 'encodings.aliases' Required, see initstdio() function in cPython sources +# - 'encodings.utf_8' +# - 'encodings.latin_1' +# - 'encodings.ascii' Required by some cascaded import from http +# - encodings.idna' Required by +# excludes.extend(('encodings.undefined', 'encodings.utf_32_be', 'encodings.utf_32', +# 'encodings.utf_16_le', 'encodings.utf_16_be', 'encodings.utf_16', 'encodings.utf_32_le', +# 'encodings.zlib_codec', 'encodings.euc_jis_2004', 'encodings.ptcp154', 'encodings.cp874', +# 'encodings.cp424', +# 'encodings.iso2022_jp_2', 'encodings.euc_jp', 'encodings.mac_arabic', 'encodings.shift_jis', +# 'encodings.utf_7', 'encodings.cp866', 'encodings.cp855', 'encodings.rot_13', 'encodings.cp1006', +# 'encodings.johab', 'encodings.cp865', 'encodings.mac_cyrillic', 'encodings.cp737', 'encodings.kz1048', +# 'encodings.cp1256', 'encodings.cp1252', 'encodings.hp_roman8', 'encodings.cp1026', 'encodings.iso8859_6', +# 'encodings.hz', 'encodings.shift_jisx0213', 'encodings.cp500', 'encodings.palmos', 'encodings.euc_jisx0213', +# 'encodings.cp864', 'encodings.cp875', 'encodings.mac_iceland', 'encodings.cp856', 'encodings.big5', +# 'encodings.iso2022_jp_ext', 'encodings.charmap', 'encodings.iso8859_7', 'encodings.cp852', +# 'encodings.mac_croatian', 'encodings.bz2_codec', 'encodings.cp863', 'encodings.iso8859_14', +# 'encodings.cp65001', 'encodings.cp1254', 'encodings.iso2022_jp_2004', 'encodings.cp932', +# 'encodings.raw_unicode_escape', 'encodings.mac_romanian', 'encodings.gb18030', 'encodings.cp1257', +# 'encodings.mac_latin2', 'encodings.iso2022_kr', 'encodings.shift_jis_2004', 'encodings.cp850', +# 'encodings.iso2022_jp_1', 'encodings.cp862', 'encodings.iso8859_15', 'encodings.hex_codec', +# 'encodings.cp857', 'encodings.iso8859_4', 'encodings.mac_roman', 'encodings.cp1250', +# 'encodings.iso8859_9', 'encodings.mbcs', 'encodings.mac_greek', +# 'encodings.cp1125', 'encodings.koi8_u', 'encodings.cp273', 'encodings.big5hkscs', 'encodings.cp1140', +# 'encodings.utf_8_sig', 'encodings.iso8859_13', 'encodings.tis_620', 'encodings.cp037', +# 'encodings.iso2022_jp_3', 'encodings.cp861', 'encodings.mac_farsi', 'encodings.iso8859_1', +# 'encodings.cp869', 'encodings.iso8859_8', 'encodings.unicode_internal', +# 'encodings.iso8859_3', 'encodings.cp720', 'encodings.koi8_r', 'encodings.cp437', 'encodings.cp858', +# 'encodings.euc_kr', 'encodings.iso8859_2', 'encodings.cp1251', 'encodings.cp950', 'encodings.gbk', +# 'encodings.cp775', 'encodings.unicode_escape', 'encodings.quopri_codec', 'encodings.cp860', +# 'encodings.koi8_t', 'encodings.uu_codec', 'encodings.cp1253', 'encodings.iso8859_5', +# 'encodings.mac_centeuro', 'encodings.iso8859_11', 'encodings.iso8859_16', 'encodings.iso8859_10', +# 'encodings.gb2312', 'encodings.iso2022_jp', 'encodings.mac_turkish', +# 'encodings.cp1255', 'encodings.cp949', 'encodings.cp1258', 'encodings.punycode')) + +# Also some other modules may be safely left out (save ~2MB on linux64/python3.7) +# excludes.extend(('bz2', 'curses', 'decimal', 'grp', 'gzip', 'json', 'lzma', 'pdb', 'pkg_resources', +# 'plistlib', 'pyexpat', 'readline', 'termios', 'uuid', 'xml', 'zlib')) + +block_cipher = None +CONSOLE=True + +if not USE_NUMPY: + # Apparently unittes is required??? by numpy + excludes.extend(("numpy", "unittest")) + +hiddenimports=[] + +a = Analysis( + ['run'], + pathex=[], + binaries=[], + datas=[("docs", "docs"),], + hiddenimports=hiddenimports, + hookspath=[], + runtime_hooks=[], + excludes=excludes, + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +exe = EXE( + pyz, + a.scripts, + # Must remain True or it will assume a single-file deploy + exclude_binaries=True, + name='websockify', + debug=DEBUG, + strip=False, + upx=False, + console=CONSOLE +) +coll = COLLECT( + exe, a.binaries, a.zipfiles, a.datas, + strip=False, upx=False, name='websockify' +) From f5dbb83fecbdbfe94af3e2918fab630d37caca06 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sat, 2 Mar 2019 17:21:28 +0100 Subject: [PATCH 2/6] Add option for cert key password --- tests/test_websockifyserver.py | 6 +++--- websockify/websocketproxy.py | 2 ++ websockify/websockifyserver.py | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_websockifyserver.py b/tests/test_websockifyserver.py index 7ce82dad..b9312dc6 100644 --- a/tests/test_websockifyserver.py +++ b/tests/test_websockifyserver.py @@ -271,7 +271,7 @@ class fake_create_default_context(): def __init__(self, purpose): self.verify_mode = None self.options = 0 - def load_cert_chain(self, certfile, keyfile): + def load_cert_chain(self, certfile, keyfile, password): pass def set_default_verify_paths(self): pass @@ -310,7 +310,7 @@ class fake_create_default_context(): def __init__(self, purpose): self.verify_mode = None self.options = 0 - def load_cert_chain(self, certfile, keyfile): + def load_cert_chain(self, certfile, keyfile, password): pass def set_default_verify_paths(self): pass @@ -351,7 +351,7 @@ class fake_create_default_context(object): def __init__(self, purpose): self.verify_mode = None self._options = 0 - def load_cert_chain(self, certfile, keyfile): + def load_cert_chain(self, certfile, keyfile, password): pass def set_default_verify_paths(self): pass diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 16b00d85..d69dad68 100644 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -478,6 +478,8 @@ def websockify_init(): help="SSL certificate file") parser.add_option("--key", default=None, help="SSL key file (if separate from cert)") + parser.add_option("--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", diff --git a/websockify/websockifyserver.py b/websockify/websockifyserver.py index fe01f975..c379d643 100644 --- a/websockify/websockifyserver.py +++ b/websockify/websockifyserver.py @@ -340,7 +340,7 @@ class Terminate(Exception): def __init__(self, RequestHandlerClass, listen_fd=None, listen_host='', listen_port=None, source_is_ipv6=False, - verbose=False, cert='', key='', ssl_only=None, + verbose=False, cert='', key='', password=None, ssl_only=None, verify_client=False, cafile=None, daemon=False, record='', web='', web_auth=False, file_only=False, @@ -380,6 +380,7 @@ def __init__(self, RequestHandlerClass, listen_fd=None, # keyfile path must be None if not specified self.key = None + self.password = password # Make paths settings absolute self.cert = os.path.abspath(cert) @@ -577,7 +578,7 @@ def do_handshake(self, sock, address): 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) + context.load_cert_chain(certfile=self.cert, keyfile=self.key, password=self.password) if self.verify_client: context.verify_mode = ssl.CERT_REQUIRED if self.cafile: From 39ae9b6250a3e218b3961db96e90b2f4191e54bb Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Mon, 4 Mar 2019 09:31:01 +0100 Subject: [PATCH 3/6] Rename certificate key password option --- websockify/websocketproxy.py | 2 +- websockify/websockifyserver.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index d69dad68..6e89fa66 100644 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -478,7 +478,7 @@ def websockify_init(): help="SSL certificate file") parser.add_option("--key", default=None, help="SSL key file (if separate from cert)") - parser.add_option("--password", default=None, + 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") diff --git a/websockify/websockifyserver.py b/websockify/websockifyserver.py index c379d643..9d9cfb9d 100644 --- a/websockify/websockifyserver.py +++ b/websockify/websockifyserver.py @@ -340,7 +340,7 @@ class Terminate(Exception): def __init__(self, RequestHandlerClass, listen_fd=None, listen_host='', listen_port=None, source_is_ipv6=False, - verbose=False, cert='', key='', password=None, ssl_only=None, + 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, @@ -380,7 +380,7 @@ def __init__(self, RequestHandlerClass, listen_fd=None, # keyfile path must be None if not specified self.key = None - self.password = password + self.key_password = key_password # Make paths settings absolute self.cert = os.path.abspath(cert) @@ -578,7 +578,7 @@ def do_handshake(self, sock, address): 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.password) + 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: From fb6405bffbddabdbc5aae65a0ae18e1a3e54c0bb Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 2 Apr 2019 17:02:08 +0200 Subject: [PATCH 4/6] Allow main script to be imported We should only start the server if we are the main module, and not imported some other way. This is important for multiprocessing to work correctly on Windows. --- run | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run b/run index 9ad217c0..9ea69969 100755 --- a/run +++ b/run @@ -2,4 +2,5 @@ import websockify -websockify.websocketproxy.websockify_init() +if __name__ == '__main__': + websockify.websocketproxy.websockify_init() From 05c4ac96704ec420f44449d5f788b901d8753aa2 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 2 Apr 2019 17:03:08 +0200 Subject: [PATCH 5/6] Use ThreadingMixIn for the simple server ForkingMixIn isn't available on Windows. This is the simple server without features, so use ThreadingMixIn to keep things consistent. --- websockify/websocketproxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 6e89fa66..525904e5 100644 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -13,9 +13,9 @@ import signal, socket, optparse, time, os, sys, subprocess, logging, errno, ssl try: - from socketserver import ForkingMixIn + from socketserver import ThreadingMixIn except ImportError: - from SocketServer import ForkingMixIn + from SocketServer import ThreadingMixIn try: from http.server import HTTPServer @@ -726,7 +726,7 @@ def websockify_init(): server.start_server() -class LibProxyServer(ForkingMixIn, HTTPServer): +class LibProxyServer(ThreadingMixIn, HTTPServer): """ Just like WebSocketProxy, but uses standard Python SocketServer framework. From dc3d28cbb6209d52ef5a417dfc33c4fcedd35859 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 2 Apr 2019 17:04:19 +0200 Subject: [PATCH 6/6] Re-enable Windows support It works well enough now with the recent fixes and a modern Python. --- websockify/websockifyserver.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/websockify/websockifyserver.py b/websockify/websockifyserver.py index 9d9cfb9d..0ec3bb0d 100644 --- a/websockify/websockifyserver.py +++ b/websockify/websockifyserver.py @@ -40,9 +40,6 @@ if sys.platform == 'win32': # make sockets pickle-able/inheritable import multiprocessing.reduction - # the multiprocesssing module behaves much differently on Windows, - # and we have yet to fix all the bugs - sys.exit("Windows is not supported at this time") from websockify.websocket import WebSocket, WebSocketWantReadError, WebSocketWantWriteError from websockify.websocketserver import WebSocketRequestHandlerMixIn