diff --git a/lib/websocket/__init__.py b/lib/websocket/__init__.py index a579342d..bfa71981 100644 --- a/lib/websocket/__init__.py +++ b/lib/websocket/__init__.py @@ -2,7 +2,7 @@ __init__.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,4 +23,4 @@ from ._exceptions import * from ._logging import * from ._socket import * -__version__ = "1.5.1" +__version__ = "1.6.2" diff --git a/lib/websocket/_abnf.py b/lib/websocket/_abnf.py index 2e5ad97c..6c2466ad 100644 --- a/lib/websocket/_abnf.py +++ b/lib/websocket/_abnf.py @@ -11,7 +11,7 @@ from threading import Lock _abnf.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,14 +33,14 @@ try: # Note that wsaccel is unmaintained. from wsaccel.xormask import XorMaskerSimple - def _mask(_m, _d): + def _mask(_m, _d) -> bytes: return XorMaskerSimple(_m).process(_d) except ImportError: # wsaccel is not available, use websocket-client _mask() native_byteorder = sys.byteorder - def _mask(mask_value, data_value): + def _mask(mask_value: array.array, data_value: array.array) -> bytes: datalen = len(data_value) data_value = int.from_bytes(data_value, native_byteorder) mask_value = int.from_bytes(mask_value * (datalen // 4) + mask_value[: datalen % 4], native_byteorder) @@ -131,8 +131,8 @@ class ABNF: LENGTH_16 = 1 << 16 LENGTH_63 = 1 << 63 - def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, - opcode=OPCODE_TEXT, mask=1, data=""): + def __init__(self, fin: int = 0, rsv1: int = 0, rsv2: int = 0, rsv3: int = 0, + opcode: int = OPCODE_TEXT, mask: int = 1, data: str or bytes = "") -> None: """ Constructor for ABNF. Please check RFC for arguments. """ @@ -147,7 +147,7 @@ class ABNF: self.data = data self.get_mask_key = os.urandom - def validate(self, skip_utf8_validation=False) -> None: + def validate(self, skip_utf8_validation: bool = False) -> None: """ Validate the ABNF frame. @@ -187,19 +187,19 @@ class ABNF: + " data=" + str(self.data) @staticmethod - def create_frame(data, opcode, fin=1): + def create_frame(data: str, opcode: int, fin: int = 1) -> 'ABNF': """ Create frame to send text, binary and other data. Parameters ---------- - data: + data: str data to send. This is string value(byte array). If opcode is OPCODE_TEXT and this value is unicode, data value is converted into unicode string, automatically. - opcode: - operation code. please see OPCODE_XXX. - fin: + opcode: int + operation code. please see OPCODE_MAP. + fin: int fin flag. if set to 0, create continue fragmentation. """ if opcode == ABNF.OPCODE_TEXT and isinstance(data, str): @@ -237,7 +237,7 @@ class ABNF: mask_key = self.get_mask_key(4) return frame_header + self._get_masked(mask_key) - def _get_masked(self, mask_key): + def _get_masked(self, mask_key: str or bytes) -> bytes: s = ABNF.mask(mask_key, self.data) if isinstance(mask_key, str): @@ -246,7 +246,7 @@ class ABNF: return mask_key + s @staticmethod - def mask(mask_key, data): + def mask(mask_key: str or bytes, data: str or bytes) -> bytes: """ Mask or unmask data. Just do xor for each byte @@ -273,7 +273,7 @@ class frame_buffer: _HEADER_MASK_INDEX = 5 _HEADER_LENGTH_INDEX = 6 - def __init__(self, recv_fn, skip_utf8_validation): + def __init__(self, recv_fn: int, skip_utf8_validation: bool) -> None: self.recv = recv_fn self.skip_utf8_validation = skip_utf8_validation # Buffers over the packets from the layer beneath until desired amount @@ -282,7 +282,7 @@ class frame_buffer: self.clear() self.lock = Lock() - def clear(self): + def clear(self) -> None: self.header = None self.length = None self.mask = None @@ -290,7 +290,7 @@ class frame_buffer: def has_received_header(self) -> bool: return self.header is None - def recv_header(self): + def recv_header(self) -> None: header = self.recv_strict(2) b1 = header[0] fin = b1 >> 7 & 1 @@ -304,7 +304,7 @@ class frame_buffer: self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits) - def has_mask(self): + def has_mask(self) -> bool or int: if not self.header: return False return self.header[frame_buffer._HEADER_MASK_INDEX] @@ -312,7 +312,7 @@ class frame_buffer: def has_received_length(self) -> bool: return self.length is None - def recv_length(self): + def recv_length(self) -> None: bits = self.header[frame_buffer._HEADER_LENGTH_INDEX] length_bits = bits & 0x7f if length_bits == 0x7e: @@ -327,10 +327,10 @@ class frame_buffer: def has_received_mask(self) -> bool: return self.mask is None - def recv_mask(self): + def recv_mask(self) -> None: self.mask = self.recv_strict(4) if self.has_mask() else "" - def recv_frame(self): + def recv_frame(self) -> ABNF: with self.lock: # Header @@ -386,20 +386,20 @@ class frame_buffer: class continuous_frame: - def __init__(self, fire_cont_frame, skip_utf8_validation): + def __init__(self, fire_cont_frame: bool, skip_utf8_validation: bool) -> None: self.fire_cont_frame = fire_cont_frame self.skip_utf8_validation = skip_utf8_validation self.cont_data = None self.recving_frames = None - def validate(self, frame): + def validate(self, frame: ABNF) -> None: if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT: raise WebSocketProtocolException("Illegal frame") if self.recving_frames and \ frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): raise WebSocketProtocolException("Illegal frame") - def add(self, frame): + def add(self, frame: ABNF) -> None: if self.cont_data: self.cont_data[1] += frame.data else: @@ -410,10 +410,10 @@ class continuous_frame: if frame.fin: self.recving_frames = None - def is_fire(self, frame): + def is_fire(self, frame: ABNF) -> bool or int: return frame.fin or self.fire_cont_frame - def extract(self, frame): + def extract(self, frame: ABNF) -> list: data = self.cont_data self.cont_data = None frame.data = data[1] diff --git a/lib/websocket/_app.py b/lib/websocket/_app.py index 0a16ddb6..2577f1ba 100644 --- a/lib/websocket/_app.py +++ b/lib/websocket/_app.py @@ -4,6 +4,9 @@ import sys import threading import time import traceback +import socket + +from typing import Callable, Any from . import _logging from ._abnf import ABNF @@ -15,7 +18,7 @@ from ._exceptions import * _app.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -35,7 +38,7 @@ __all__ = ["WebSocketApp"] RECONNECT = 0 -def setReconnect(reconnectInterval): +def setReconnect(reconnectInterval: int) -> None: global RECONNECT RECONNECT = reconnectInterval @@ -44,37 +47,40 @@ class DispatcherBase: """ DispatcherBase """ - def __init__(self, app, ping_timeout): + def __init__(self, app: Any, ping_timeout: float) -> None: self.app = app self.ping_timeout = ping_timeout - def timeout(self, seconds, callback): + def timeout(self, seconds: int, callback: Callable) -> None: time.sleep(seconds) callback() - def reconnect(self, seconds, reconnector): + def reconnect(self, seconds: int, reconnector: Callable) -> None: try: - _logging.info("reconnect() - retrying in %s seconds [%s frames in stack]" % (seconds, len(inspect.stack()))) + _logging.info("reconnect() - retrying in {seconds_count} seconds [{frame_count} frames in stack]".format( + seconds_count=seconds, frame_count=len(inspect.stack()))) time.sleep(seconds) reconnector(reconnecting=True) except KeyboardInterrupt as e: - _logging.info("User exited %s" % (e,)) + _logging.info("User exited {err}".format(err=e)) + raise e class Dispatcher(DispatcherBase): """ Dispatcher """ - def read(self, sock, read_callback, check_callback): - while self.app.keep_running: - sel = selectors.DefaultSelector() - sel.register(self.app.sock.sock, selectors.EVENT_READ) - - r = sel.select(self.ping_timeout) - if r: - if not read_callback(): - break - check_callback() + def read(self, sock: socket.socket, read_callback: Callable, check_callback: Callable) -> None: + sel = selectors.DefaultSelector() + sel.register(self.app.sock.sock, selectors.EVENT_READ) + try: + while self.app.keep_running: + r = sel.select(self.ping_timeout) + if r: + if not read_callback(): + break + check_callback() + finally: sel.close() @@ -82,24 +88,26 @@ class SSLDispatcher(DispatcherBase): """ SSLDispatcher """ - def read(self, sock, read_callback, check_callback): - while self.app.keep_running: - r = self.select() - if r: - if not read_callback(): - break - check_callback() + def read(self, sock: socket.socket, read_callback: Callable, check_callback: Callable) -> None: + sock = self.app.sock.sock + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ) + try: + while self.app.keep_running: + r = self.select(sock, sel) + if r: + if not read_callback(): + break + check_callback() + finally: + sel.close() - def select(self): + def select(self, sock, sel:selectors.DefaultSelector): sock = self.app.sock.sock if sock.pending(): return [sock,] - sel = selectors.DefaultSelector() - sel.register(sock, selectors.EVENT_READ) - r = sel.select(self.ping_timeout) - sel.close() if len(r) > 0: return r[0][0] @@ -109,20 +117,20 @@ class WrappedDispatcher: """ WrappedDispatcher """ - def __init__(self, app, ping_timeout, dispatcher): + def __init__(self, app, ping_timeout: float, dispatcher: Dispatcher) -> None: self.app = app self.ping_timeout = ping_timeout self.dispatcher = dispatcher dispatcher.signal(2, dispatcher.abort) # keyboard interrupt - def read(self, sock, read_callback, check_callback): + def read(self, sock: socket.socket, read_callback: Callable, check_callback: Callable) -> None: self.dispatcher.read(sock, read_callback) self.ping_timeout and self.timeout(self.ping_timeout, check_callback) - def timeout(self, seconds, callback): + def timeout(self, seconds: int, callback: Callable) -> None: self.dispatcher.timeout(seconds, callback) - def reconnect(self, seconds, reconnector): + def reconnect(self, seconds: int, reconnector: Callable) -> None: self.timeout(seconds, reconnector) @@ -131,14 +139,14 @@ class WebSocketApp: Higher level of APIs are provided. The interface is like JavaScript WebSocket object. """ - def __init__(self, url, header=None, - on_open=None, on_message=None, on_error=None, - on_close=None, on_ping=None, on_pong=None, - on_cont_message=None, - keep_running=True, get_mask_key=None, cookie=None, - subprotocols=None, - on_data=None, - socket=None): + def __init__(self, url: str, header: list or dict or Callable = None, + on_open: Callable = None, on_message: Callable = None, on_error: Callable = None, + on_close: Callable = None, on_ping: Callable = None, on_pong: Callable = None, + on_cont_message: Callable = None, + keep_running: bool = True, get_mask_key: Callable = None, cookie: str = None, + subprotocols: list = None, + on_data: Callable = None, + socket: socket.socket = None) -> None: """ WebSocketApp initialization @@ -146,8 +154,11 @@ class WebSocketApp: ---------- url: str Websocket url. - header: list or dict + header: list or dict or Callable Custom header for websocket handshake. + If the parameter is a callable object, it is called just before the connection attempt. + The returned dict or list is used as custom header value. + This could be useful in order to properly setup timestamp dependent headers. on_open: function Callback object which is called at opening websocket. on_open has one argument. @@ -222,8 +233,10 @@ class WebSocketApp: self.subprotocols = subprotocols self.prepared_socket = socket self.has_errored = False + self.has_done_teardown = False + self.has_done_teardown_lock = threading.Lock() - def send(self, data, opcode=ABNF.OPCODE_TEXT): + def send(self, data: str, opcode: int = ABNF.OPCODE_TEXT) -> None: """ send message @@ -240,7 +253,7 @@ class WebSocketApp: raise WebSocketConnectionClosedException( "Connection is already closed.") - def close(self, **kwargs): + def close(self, **kwargs) -> None: """ Close websocket connection. """ @@ -249,41 +262,41 @@ class WebSocketApp: self.sock.close(**kwargs) self.sock = None - def _start_ping_thread(self): + def _start_ping_thread(self) -> None: self.last_ping_tm = self.last_pong_tm = 0 self.stop_ping = threading.Event() self.ping_thread = threading.Thread(target=self._send_ping) self.ping_thread.daemon = True self.ping_thread.start() - def _stop_ping_thread(self): + def _stop_ping_thread(self) -> None: if self.stop_ping: self.stop_ping.set() if self.ping_thread and self.ping_thread.is_alive(): self.ping_thread.join(3) self.last_ping_tm = self.last_pong_tm = 0 - def _send_ping(self): - if self.stop_ping.wait(self.ping_interval): + def _send_ping(self) -> None: + if self.stop_ping.wait(self.ping_interval) or self.keep_running is False: return - while not self.stop_ping.wait(self.ping_interval): + while not self.stop_ping.wait(self.ping_interval) and self.keep_running is True: if self.sock: self.last_ping_tm = time.time() try: _logging.debug("Sending ping") self.sock.ping(self.ping_payload) - except Exception as ex: - _logging.debug("Failed to send ping: %s", ex) + except Exception as e: + _logging.debug("Failed to send ping: {err}".format(err=e)) - def run_forever(self, sockopt=None, sslopt=None, - ping_interval=0, ping_timeout=None, - ping_payload="", - http_proxy_host=None, http_proxy_port=None, - http_no_proxy=None, http_proxy_auth=None, - http_proxy_timeout=None, - skip_utf8_validation=False, - host=None, origin=None, dispatcher=None, - suppress_origin=False, proxy_type=None, reconnect=None): + def run_forever(self, sockopt: tuple = None, sslopt: dict = None, + ping_interval: float = 0, ping_timeout: float or None = None, + ping_payload: str = "", + http_proxy_host: str = None, http_proxy_port: int or str = None, + http_no_proxy: list = None, http_proxy_auth: tuple = None, + http_proxy_timeout: float = None, + skip_utf8_validation: bool = False, + host: str = None, origin: str = None, dispatcher: Dispatcher = None, + suppress_origin: bool = False, proxy_type: str = None, reconnect: int = None) -> bool: """ Run event loop for WebSocket framework. @@ -358,7 +371,7 @@ class WebSocketApp: self.ping_payload = ping_payload self.keep_running = True - def teardown(close_frame=None): + def teardown(close_frame: ABNF = None): """ Tears down the connection. @@ -369,6 +382,13 @@ class WebSocketApp: with the statusCode and reason from the provided frame. """ + # teardown() is called in many code paths to ensure resources are cleaned up and on_close is fired. + # To ensure the work is only done once, we use this bool and lock. + with self.has_done_teardown_lock: + if self.has_done_teardown: + return + self.has_done_teardown = True + self._stop_ping_thread() self.keep_running = False if self.sock: @@ -380,7 +400,7 @@ class WebSocketApp: # Finally call the callback AFTER all teardown is complete self._callback(self.on_close, close_status_code, close_reason) - def setSock(reconnecting=False): + def setSock(reconnecting: bool = False) -> None: if reconnecting and self.sock: self.sock.shutdown() @@ -392,8 +412,11 @@ class WebSocketApp: self.sock.settimeout(getdefaulttimeout()) try: + + header = self.header() if callable(self.header) else self.header + self.sock.connect( - self.url, header=self.header, cookie=self.cookie, + self.url, header=header, cookie=self.cookie, http_proxy_host=http_proxy_host, http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy, http_proxy_auth=http_proxy_auth, http_proxy_timeout=http_proxy_timeout, @@ -412,7 +435,7 @@ class WebSocketApp: except (WebSocketConnectionClosedException, ConnectionRefusedError, KeyboardInterrupt, SystemExit, Exception) as e: handleDisconnect(e, reconnecting) - def read(): + def read() -> bool: if not self.keep_running: return teardown() @@ -445,7 +468,7 @@ class WebSocketApp: return True - def check(): + def check() -> bool: if (self.ping_timeout): has_timeout_expired = time.time() - self.last_ping_tm > self.ping_timeout has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0 @@ -457,7 +480,7 @@ class WebSocketApp: raise WebSocketTimeoutException("ping/pong timed out") return True - def handleDisconnect(e, reconnecting=False): + def handleDisconnect(e: Exception, reconnecting: bool = False) -> bool: self.has_errored = True self._stop_ping_thread() if not reconnecting: @@ -469,26 +492,34 @@ class WebSocketApp: raise if reconnect: - _logging.info("%s - reconnect" % e) + _logging.info("{err} - reconnect".format(err=e)) if custom_dispatcher: - _logging.debug("Calling custom dispatcher reconnect [%s frames in stack]" % len(inspect.stack())) + _logging.debug("Calling custom dispatcher reconnect [{frame_count} frames in stack]".format(frame_count=len(inspect.stack()))) dispatcher.reconnect(reconnect, setSock) else: - _logging.error("%s - goodbye" % e) + _logging.error("{err} - goodbye".format(err=e)) teardown() custom_dispatcher = bool(dispatcher) dispatcher = self.create_dispatcher(ping_timeout, dispatcher, parse_url(self.url)[3]) - setSock() - if not custom_dispatcher and reconnect: - while self.keep_running: - _logging.debug("Calling dispatcher reconnect [%s frames in stack]" % len(inspect.stack())) - dispatcher.reconnect(reconnect, setSock) + try: + setSock() + if not custom_dispatcher and reconnect: + while self.keep_running: + _logging.debug("Calling dispatcher reconnect [{frame_count} frames in stack]".format(frame_count=len(inspect.stack()))) + dispatcher.reconnect(reconnect, setSock) + except (KeyboardInterrupt, Exception) as e: + _logging.info("tearing down on exception {err}".format(err=e)) + teardown() + finally: + if not custom_dispatcher: + # Ensure teardown was called before returning from run_forever + teardown() return self.has_errored - def create_dispatcher(self, ping_timeout, dispatcher=None, is_ssl=False): + def create_dispatcher(self, ping_timeout: int, dispatcher: Dispatcher = None, is_ssl: bool = False) -> DispatcherBase: if dispatcher: # If custom dispatcher is set, use WrappedDispatcher return WrappedDispatcher(self, ping_timeout, dispatcher) timeout = ping_timeout or 10 @@ -497,7 +528,7 @@ class WebSocketApp: return Dispatcher(self, timeout) - def _get_close_args(self, close_frame): + def _get_close_args(self, close_frame: ABNF) -> list: """ _get_close_args extracts the close code and reason from the close body if it exists (RFC6455 says WebSocket Connection Close Code is optional) @@ -516,12 +547,12 @@ class WebSocketApp: # Most likely reached this because len(close_frame_data.data) < 2 return [None, None] - def _callback(self, callback, *args): + def _callback(self, callback, *args) -> None: if callback: try: callback(self, *args) except Exception as e: - _logging.error("error from callback {}: {}".format(callback, e)) + _logging.error("error from callback {callback}: {err}".format(callback=callback, err=e)) if self.on_error: self.on_error(self, e) diff --git a/lib/websocket/_cookiejar.py b/lib/websocket/_cookiejar.py index 5476d1d4..2047eddd 100644 --- a/lib/websocket/_cookiejar.py +++ b/lib/websocket/_cookiejar.py @@ -4,7 +4,7 @@ import http.cookies _cookiejar.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ limitations under the License. class SimpleCookieJar: - def __init__(self): + def __init__(self) -> None: self.jar = dict() - def add(self, set_cookie): + def add(self, set_cookie: str) -> None: if set_cookie: simpleCookie = http.cookies.SimpleCookie(set_cookie) @@ -37,7 +37,7 @@ class SimpleCookieJar: cookie.update(simpleCookie) self.jar[domain.lower()] = cookie - def set(self, set_cookie): + def set(self, set_cookie: str) -> None: if set_cookie: simpleCookie = http.cookies.SimpleCookie(set_cookie) @@ -48,7 +48,7 @@ class SimpleCookieJar: domain = "." + domain self.jar[domain.lower()] = simpleCookie - def get(self, host): + def get(self, host: str) -> str: if not host: return "" diff --git a/lib/websocket/_core.py b/lib/websocket/_core.py index 1d688294..e81066ad 100644 --- a/lib/websocket/_core.py +++ b/lib/websocket/_core.py @@ -17,7 +17,7 @@ from ._utils import * _core.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -74,8 +74,8 @@ class WebSocket: """ def __init__(self, get_mask_key=None, sockopt=None, sslopt=None, - fire_cont_frame=False, enable_multithread=True, - skip_utf8_validation=False, **_): + fire_cont_frame: bool = False, enable_multithread: bool = True, + skip_utf8_validation: bool = False, **_): """ Initialize WebSocket object. @@ -133,7 +133,7 @@ class WebSocket: """ self.get_mask_key = func - def gettimeout(self): + def gettimeout(self) -> float: """ Get the websocket timeout (in seconds) as an int or float @@ -144,7 +144,7 @@ class WebSocket: """ return self.sock_opt.timeout - def settimeout(self, timeout): + def settimeout(self, timeout: float): """ Set the timeout to the websocket. @@ -265,7 +265,7 @@ class WebSocket: self.sock = None raise - def send(self, payload, opcode=ABNF.OPCODE_TEXT): + def send(self, payload: bytes or str, opcode: int = ABNF.OPCODE_TEXT) -> int: """ Send the data as string. @@ -282,7 +282,7 @@ class WebSocket: frame = ABNF.create_frame(payload, opcode) return self.send_frame(frame) - def send_frame(self, frame): + def send_frame(self, frame) -> int: """ Send the data frame. @@ -313,7 +313,7 @@ class WebSocket: return length - def send_binary(self, payload): + def send_binary(self, payload: bytes) -> int: """ Send a binary message (OPCODE_BINARY). @@ -324,7 +324,7 @@ class WebSocket: """ return self.send(payload, ABNF.OPCODE_BINARY) - def ping(self, payload=""): + def ping(self, payload: str or bytes = ""): """ Send ping data. @@ -337,7 +337,7 @@ class WebSocket: payload = payload.encode("utf-8") self.send(payload, ABNF.OPCODE_PING) - def pong(self, payload=""): + def pong(self, payload: str or bytes = ""): """ Send pong data. @@ -350,7 +350,7 @@ class WebSocket: payload = payload.encode("utf-8") self.send(payload, ABNF.OPCODE_PONG) - def recv(self): + def recv(self) -> str or bytes: """ Receive string data(byte array) from the server. @@ -367,7 +367,7 @@ class WebSocket: else: return '' - def recv_data(self, control_frame=False): + def recv_data(self, control_frame: bool = False) -> tuple: """ Receive data with operation code. @@ -385,7 +385,7 @@ class WebSocket: opcode, frame = self.recv_data_frame(control_frame) return opcode, frame.data - def recv_data_frame(self, control_frame=False): + def recv_data_frame(self, control_frame: bool = False): """ Receive data with operation code. @@ -411,7 +411,7 @@ class WebSocket: # handle error: # 'NoneType' object has no attribute 'opcode' raise WebSocketProtocolException( - "Not a valid frame %s" % frame) + "Not a valid frame {frame}".format(frame=frame)) elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): self.cont_frame.validate(frame) self.cont_frame.add(frame) @@ -444,7 +444,7 @@ class WebSocket: """ return self.frame_buffer.recv_frame() - def send_close(self, status=STATUS_NORMAL, reason=b""): + def send_close(self, status: int = STATUS_NORMAL, reason: bytes = b""): """ Send close data to the server. @@ -460,14 +460,14 @@ class WebSocket: self.connected = False self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) - def close(self, status=STATUS_NORMAL, reason=b"", timeout=3): + def close(self, status: int = STATUS_NORMAL, reason: bytes = b"", timeout: float = 3): """ Close Websocket object Parameters ---------- status: int - Status code to send. See STATUS_XXX. + Status code to send. See VALID_CLOSE_STATUS in ABNF. reason: bytes The reason to close in UTF-8. timeout: int or float @@ -521,7 +521,7 @@ class WebSocket: self.sock = None self.connected = False - def _send(self, data): + def _send(self, data: str or bytes): return send(self.sock, data) def _recv(self, bufsize): @@ -535,7 +535,7 @@ class WebSocket: raise -def create_connection(url, timeout=None, class_=WebSocket, **options): +def create_connection(url: str, timeout=None, class_=WebSocket, **options): """ Connect to url and return websocket object. diff --git a/lib/websocket/_exceptions.py b/lib/websocket/_exceptions.py index 811d5945..48f40a07 100644 --- a/lib/websocket/_exceptions.py +++ b/lib/websocket/_exceptions.py @@ -2,7 +2,7 @@ _exceptions.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -66,11 +66,11 @@ class WebSocketBadStatusException(WebSocketException): WebSocketBadStatusException will be raised when we get bad handshake status code. """ - def __init__(self, message, status_code, status_message=None, resp_headers=None): - msg = message % (status_code, status_message) - super().__init__(msg) + def __init__(self, message: str, status_code: int, status_message=None, resp_headers=None, resp_body=None): + super().__init__(message) self.status_code = status_code self.resp_headers = resp_headers + self.resp_body = resp_body class WebSocketAddressException(WebSocketException): diff --git a/lib/websocket/_handshake.py b/lib/websocket/_handshake.py index 07a4cfb2..d28aefd7 100644 --- a/lib/websocket/_handshake.py +++ b/lib/websocket/_handshake.py @@ -2,7 +2,7 @@ _handshake.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -40,14 +40,14 @@ CookieJar = SimpleCookieJar() class handshake_response: - def __init__(self, status, headers, subprotocol): + def __init__(self, status: int, headers: dict, subprotocol): self.status = status self.headers = headers self.subprotocol = subprotocol CookieJar.add(headers.get("set-cookie")) -def handshake(sock, url, hostname, port, resource, **options): +def handshake(sock, url: str, hostname: str, port: int, resource: str, **options): headers, key = _get_handshake_headers(resource, url, hostname, port, options) header_str = "\r\n".join(headers) @@ -64,7 +64,7 @@ def handshake(sock, url, hostname, port, resource, **options): return handshake_response(status, resp, subproto) -def _pack_hostname(hostname): +def _pack_hostname(hostname: str) -> str: # IPv6 address if ':' in hostname: return '[' + hostname + ']' @@ -72,41 +72,41 @@ def _pack_hostname(hostname): return hostname -def _get_handshake_headers(resource, url, host, port, options): +def _get_handshake_headers(resource: str, url: str, host: str, port: int, options: dict): headers = [ - "GET %s HTTP/1.1" % resource, + "GET {resource} HTTP/1.1".format(resource=resource), "Upgrade: websocket" ] if port == 80 or port == 443: hostport = _pack_hostname(host) else: - hostport = "%s:%d" % (_pack_hostname(host), port) + hostport = "{h}:{p}".format(h=_pack_hostname(host), p=port) if options.get("host"): - headers.append("Host: %s" % options["host"]) + headers.append("Host: {h}".format(h=options["host"])) else: - headers.append("Host: %s" % hostport) + headers.append("Host: {hp}".format(hp=hostport)) # scheme indicates whether http or https is used in Origin # The same approach is used in parse_url of _url.py to set default port scheme, url = url.split(":", 1) if not options.get("suppress_origin"): if "origin" in options and options["origin"] is not None: - headers.append("Origin: %s" % options["origin"]) + headers.append("Origin: {origin}".format(origin=options["origin"])) elif scheme == "wss": - headers.append("Origin: https://%s" % hostport) + headers.append("Origin: https://{hp}".format(hp=hostport)) else: - headers.append("Origin: http://%s" % hostport) + headers.append("Origin: http://{hp}".format(hp=hostport)) key = _create_sec_websocket_key() # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified if not options.get('header') or 'Sec-WebSocket-Key' not in options['header']: - headers.append("Sec-WebSocket-Key: %s" % key) + headers.append("Sec-WebSocket-Key: {key}".format(key=key)) else: key = options['header']['Sec-WebSocket-Key'] if not options.get('header') or 'Sec-WebSocket-Version' not in options['header']: - headers.append("Sec-WebSocket-Version: %s" % VERSION) + headers.append("Sec-WebSocket-Version: {version}".format(version=VERSION)) if not options.get('connection'): headers.append('Connection: Upgrade') @@ -115,7 +115,7 @@ def _get_handshake_headers(resource, url, host, port, options): subprotocols = options.get("subprotocols") if subprotocols: - headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols)) + headers.append("Sec-WebSocket-Protocol: {protocols}".format(protocols=",".join(subprotocols))) header = options.get("header") if header: @@ -133,18 +133,21 @@ def _get_handshake_headers(resource, url, host, port, options): cookie = "; ".join(filter(None, [server_cookie, client_cookie])) if cookie: - headers.append("Cookie: %s" % cookie) - - headers.append("") - headers.append("") + headers.append("Cookie: {cookie}".format(cookie=cookie)) + headers.extend(("", "")) return headers, key -def _get_resp_headers(sock, success_statuses=SUCCESS_STATUSES): +def _get_resp_headers(sock, success_statuses: tuple = SUCCESS_STATUSES) -> tuple: status, resp_headers, status_message = read_headers(sock) if status not in success_statuses: - raise WebSocketBadStatusException("Handshake status %d %s", status, status_message, resp_headers) + content_len = resp_headers.get('content-length') + if content_len: + response_body = sock.recv(int(content_len)) # read the body of the HTTP error message response and include it in the exception + else: + response_body = None + raise WebSocketBadStatusException("Handshake status {status} {message} -+-+- {headers} -+-+- {body}".format(status=status, message=status_message, headers=resp_headers, body=response_body), status, status_message, resp_headers, response_body) return status, resp_headers @@ -154,7 +157,7 @@ _HEADERS_TO_CHECK = { } -def _validate(headers, key, subprotocols): +def _validate(headers, key: str, subprotocols): subproto = None for k, v in _HEADERS_TO_CHECK.items(): r = headers.get(k, None) @@ -189,6 +192,6 @@ def _validate(headers, key, subprotocols): return False, None -def _create_sec_websocket_key(): +def _create_sec_websocket_key() -> str: randomness = os.urandom(16) return base64encode(randomness).decode('utf-8').strip() diff --git a/lib/websocket/_http.py b/lib/websocket/_http.py index 17d3f8aa..13183b20 100644 --- a/lib/websocket/_http.py +++ b/lib/websocket/_http.py @@ -2,7 +2,7 @@ _http.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ limitations under the License. import errno import os import socket -import sys from ._exceptions import * from ._logging import * @@ -69,7 +68,7 @@ class proxy_info: self.proxy_protocol = "http" -def _start_proxied_socket(url, options, proxy): +def _start_proxied_socket(url: str, options, proxy): if not HAVE_PYTHON_SOCKS: raise WebSocketException("Python Socks is needed for SOCKS proxying but is not available") @@ -107,7 +106,7 @@ def _start_proxied_socket(url, options, proxy): return sock, (hostname, port, resource) -def connect(url, options, proxy, socket): +def connect(url: str, options, proxy, socket): # Use _start_proxied_socket() only for socks4 or socks5 proxy # Use _tunnel() for http proxy # TODO: Use python-socks for http protocol also, to standardize flow @@ -211,6 +210,11 @@ def _wrap_sni_socket(sock, sslopt, hostname, check_hostname): context = sslopt.get('context', None) if not context: context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_TLS_CLIENT)) + # Non default context need to manually enable SSLKEYLOGFILE support by setting the keylog_filename attribute. + # For more details see also: + # * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#context-creation + # * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#ssl.SSLContext.keylog_filename + context.keylog_filename = os.environ.get("SSLKEYLOGFILE", None) if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: cafile = sslopt.get('ca_certs', None) @@ -275,8 +279,8 @@ def _ssl_socket(sock, user_sslopt, hostname): def _tunnel(sock, host, port, auth): debug("Connecting proxy...") - connect_header = "CONNECT %s:%d HTTP/1.1\r\n" % (host, port) - connect_header += "Host: %s:%d\r\n" % (host, port) + connect_header = "CONNECT {h}:{p} HTTP/1.1\r\n".format(h=host, p=port) + connect_header += "Host: {h}:{p}\r\n".format(h=host, p=port) # TODO: support digest auth. if auth and auth[0]: @@ -284,7 +288,7 @@ def _tunnel(sock, host, port, auth): if auth[1]: auth_str += ":" + auth[1] encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '') - connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str + connect_header += "Proxy-Authorization: Basic {str}\r\n".format(str=encoded_str) connect_header += "\r\n" dump("request header", connect_header) @@ -297,7 +301,7 @@ def _tunnel(sock, host, port, auth): if status != 200: raise WebSocketProxyException( - "failed CONNECT via proxy status: %r" % status) + "failed CONNECT via proxy status: {status}".format(status=status)) return sock diff --git a/lib/websocket/_logging.py b/lib/websocket/_logging.py index 3921111d..806de4d4 100644 --- a/lib/websocket/_logging.py +++ b/lib/websocket/_logging.py @@ -4,7 +4,7 @@ import logging _logging.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ try: from logging import NullHandler except ImportError: class NullHandler(logging.Handler): - def emit(self, record): + def emit(self, record) -> None: pass _logger.addHandler(NullHandler()) @@ -35,7 +35,9 @@ __all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace", "isEnabledForError", "isEnabledForDebug", "isEnabledForTrace"] -def enableTrace(traceable, handler=logging.StreamHandler(), level="DEBUG"): +def enableTrace(traceable: bool, + handler: logging.StreamHandler = logging.StreamHandler(), + level: str = "DEBUG") -> None: """ Turn on/off the traceability. @@ -51,41 +53,41 @@ def enableTrace(traceable, handler=logging.StreamHandler(), level="DEBUG"): _logger.setLevel(getattr(logging, level)) -def dump(title, message): +def dump(title: str, message: str) -> None: if _traceEnabled: _logger.debug("--- " + title + " ---") _logger.debug(message) _logger.debug("-----------------------") -def error(msg): +def error(msg: str) -> None: _logger.error(msg) -def warning(msg): +def warning(msg: str) -> None: _logger.warning(msg) -def debug(msg): +def debug(msg: str) -> None: _logger.debug(msg) -def info(msg): +def info(msg: str) -> None: _logger.info(msg) -def trace(msg): +def trace(msg: str) -> None: if _traceEnabled: _logger.debug(msg) -def isEnabledForError(): +def isEnabledForError() -> bool: return _logger.isEnabledFor(logging.ERROR) -def isEnabledForDebug(): +def isEnabledForDebug() -> bool: return _logger.isEnabledFor(logging.DEBUG) -def isEnabledForTrace(): +def isEnabledForTrace() -> bool: return _traceEnabled diff --git a/lib/websocket/_socket.py b/lib/websocket/_socket.py index 7cc02164..e8858fc8 100644 --- a/lib/websocket/_socket.py +++ b/lib/websocket/_socket.py @@ -10,7 +10,7 @@ from ._utils import * _socket.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ __all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefault class sock_opt: - def __init__(self, sockopt, sslopt): + def __init__(self, sockopt: list, sslopt: dict) -> None: if sockopt is None: sockopt = [] if sslopt is None: @@ -53,7 +53,7 @@ class sock_opt: self.timeout = None -def setdefaulttimeout(timeout): +def setdefaulttimeout(timeout: int or float) -> None: """ Set the global timeout setting to connect. @@ -66,7 +66,7 @@ def setdefaulttimeout(timeout): _default_timeout = timeout -def getdefaulttimeout(): +def getdefaulttimeout() -> int or float: """ Get default timeout @@ -78,7 +78,7 @@ def getdefaulttimeout(): return _default_timeout -def recv(sock, bufsize): +def recv(sock: socket.socket, bufsize: int) -> bytes: if not sock: raise WebSocketConnectionClosedException("socket is already closed.") @@ -125,7 +125,7 @@ def recv(sock, bufsize): return bytes_ -def recv_line(sock): +def recv_line(sock: socket.socket) -> bytes: line = [] while True: c = recv(sock, 1) @@ -135,7 +135,7 @@ def recv_line(sock): return b''.join(line) -def send(sock, data): +def send(sock: socket.socket, data: bytes) -> int: if isinstance(data, str): data = data.encode('utf-8') diff --git a/lib/websocket/_ssl_compat.py b/lib/websocket/_ssl_compat.py index e2278401..b2eba387 100644 --- a/lib/websocket/_ssl_compat.py +++ b/lib/websocket/_ssl_compat.py @@ -2,7 +2,7 @@ _ssl_compat.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/lib/websocket/_url.py b/lib/websocket/_url.py index 2d3d2653..2141b021 100644 --- a/lib/websocket/_url.py +++ b/lib/websocket/_url.py @@ -8,7 +8,7 @@ from urllib.parse import unquote, urlparse _url.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ limitations under the License. __all__ = ["parse_url", "get_proxy_info"] -def parse_url(url): +def parse_url(url: str) -> tuple: """ parse url and the result is tuple of (hostname, port, resource path and the flag of secure mode) @@ -75,7 +75,7 @@ def parse_url(url): DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"] -def _is_ip_address(addr): +def _is_ip_address(addr: str) -> bool: try: socket.inet_aton(addr) except socket.error: @@ -84,7 +84,7 @@ def _is_ip_address(addr): return True -def _is_subnet_address(hostname): +def _is_subnet_address(hostname: str) -> bool: try: addr, netmask = hostname.split("/") return _is_ip_address(addr) and 0 <= int(netmask) < 32 @@ -92,7 +92,7 @@ def _is_subnet_address(hostname): return False -def _is_address_in_network(ip, net): +def _is_address_in_network(ip: str, net: str) -> bool: ipaddr = struct.unpack('!I', socket.inet_aton(ip))[0] netaddr, netmask = net.split('/') netaddr = struct.unpack('!I', socket.inet_aton(netaddr))[0] @@ -101,7 +101,7 @@ def _is_address_in_network(ip, net): return ipaddr & netmask == netaddr -def _is_no_proxy_host(hostname, no_proxy): +def _is_no_proxy_host(hostname: str, no_proxy: list) -> bool: if not no_proxy: v = os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(" ", "") if v: @@ -122,8 +122,8 @@ def _is_no_proxy_host(hostname, no_proxy): def get_proxy_info( - hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None, - no_proxy=None, proxy_type='http'): + hostname: str, is_secure: bool, proxy_host: str = None, proxy_port: int = 0, proxy_auth: tuple = None, + no_proxy: list = None, proxy_type: str = 'http') -> tuple: """ Try to retrieve proxy host and port from environment if not provided in options. @@ -137,14 +137,14 @@ def get_proxy_info( Websocket server name. is_secure: bool Is the connection secure? (wss) looks for "https_proxy" in env - before falling back to "http_proxy" + instead of "http_proxy" proxy_host: str http proxy host name. - http_proxy_port: str or int + proxy_port: str or int http proxy port. - http_no_proxy: list + no_proxy: list Whitelisted host names that don't use the proxy. - http_proxy_auth: tuple + proxy_auth: tuple HTTP proxy auth information. Tuple of username and password. Default is None. proxy_type: str Specify the proxy protocol (http, socks4, socks4a, socks5, socks5h). Default is "http". @@ -158,15 +158,11 @@ def get_proxy_info( auth = proxy_auth return proxy_host, port, auth - env_keys = ["http_proxy"] - if is_secure: - env_keys.insert(0, "https_proxy") - - for key in env_keys: - value = os.environ.get(key, os.environ.get(key.upper(), "")).replace(" ", "") - if value: - proxy = urlparse(value) - auth = (unquote(proxy.username), unquote(proxy.password)) if proxy.username else None - return proxy.hostname, proxy.port, auth + env_key = "https_proxy" if is_secure else "http_proxy" + value = os.environ.get(env_key, os.environ.get(env_key.upper(), "")).replace(" ", "") + if value: + proxy = urlparse(value) + auth = (unquote(proxy.username), unquote(proxy.password)) if proxy.username else None + return proxy.hostname, proxy.port, auth return None, 0, None diff --git a/lib/websocket/_utils.py b/lib/websocket/_utils.py index fdcf345b..3db2d83b 100644 --- a/lib/websocket/_utils.py +++ b/lib/websocket/_utils.py @@ -2,7 +2,7 @@ _url.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ __all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code class NoLock: - def __enter__(self): + def __enter__(self) -> None: pass - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: pass @@ -33,7 +33,7 @@ try: # strings. from wsaccel.utf8validator import Utf8Validator - def _validate_utf8(utfbytes): + def _validate_utf8(utfbytes: bytes) -> bool: return Utf8Validator().validate(utfbytes)[0] except ImportError: @@ -63,7 +63,7 @@ except ImportError: 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,12,12,12,12,12, ] - def _decode(state, codep, ch): + def _decode(state: int, codep: int, ch: int) -> tuple: tp = _UTF8D[ch] codep = (ch & 0x3f) | (codep << 6) if ( @@ -72,7 +72,7 @@ except ImportError: return state, codep - def _validate_utf8(utfbytes): + def _validate_utf8(utfbytes: str or bytes) -> bool: state = _UTF8_ACCEPT codep = 0 for i in utfbytes: @@ -83,7 +83,7 @@ except ImportError: return True -def validate_utf8(utfbytes): +def validate_utf8(utfbytes: str or bytes) -> bool: """ validate utf8 byte string. utfbytes: utf byte string to check. @@ -92,13 +92,13 @@ def validate_utf8(utfbytes): return _validate_utf8(utfbytes) -def extract_err_message(exception): +def extract_err_message(exception: Exception) -> str or None: if exception.args: return exception.args[0] else: return None -def extract_error_code(exception): +def extract_error_code(exception: Exception) -> int or None: if exception.args and len(exception.args) > 1: return exception.args[0] if isinstance(exception.args[0], int) else None diff --git a/lib/websocket/_wsdump.py b/lib/websocket/_wsdump.py index 860ac342..d637ce2b 100644 --- a/lib/websocket/_wsdump.py +++ b/lib/websocket/_wsdump.py @@ -4,7 +4,7 @@ wsdump.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ except ImportError: pass -def get_encoding(): +def get_encoding() -> str: encoding = getattr(sys.stdin, "encoding", "") if not encoding: return "utf-8" @@ -51,7 +51,7 @@ ENCODING = get_encoding() class VAction(argparse.Action): - def __call__(self, parser, args, values, option_string=None): + def __call__(self, parser: argparse.Namespace, args: tuple, values: str, option_string: str = None) -> None: if values is None: values = "1" try: @@ -61,7 +61,7 @@ class VAction(argparse.Action): setattr(args, self.dest, values) -def parse_args(): +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool") parser.add_argument("url", metavar="ws_url", help="websocket url. ex. ws://echo.websocket.events/") @@ -93,7 +93,7 @@ def parse_args(): class RawInput: - def raw_input(self, prompt): + def raw_input(self, prompt: str = "") -> str: line = input(prompt) if ENCODING and ENCODING != "utf-8" and not isinstance(line, str): @@ -106,29 +106,29 @@ class RawInput: class InteractiveConsole(RawInput, code.InteractiveConsole): - def write(self, data): + def write(self, data: str) -> None: sys.stdout.write("\033[2K\033[E") # sys.stdout.write("\n") sys.stdout.write("\033[34m< " + data + "\033[39m") sys.stdout.write("\n> ") sys.stdout.flush() - def read(self): + def read(self) -> str: return self.raw_input("> ") class NonInteractive(RawInput): - def write(self, data): + def write(self, data: str) -> None: sys.stdout.write(data) sys.stdout.write("\n") sys.stdout.flush() - def read(self): + def read(self) -> str: return self.raw_input("") -def main(): +def main() -> None: start_time = time.time() args = parse_args() if args.verbose > 1: @@ -154,25 +154,25 @@ def main(): console = InteractiveConsole() print("Press Ctrl+C to quit") - def recv(): + def recv() -> tuple: try: frame = ws.recv_frame() except websocket.WebSocketException: - return websocket.ABNF.OPCODE_CLOSE, None + return websocket.ABNF.OPCODE_CLOSE, "" if not frame: - raise websocket.WebSocketException("Not a valid frame %s" % frame) + raise websocket.WebSocketException("Not a valid frame {frame}".format(frame=frame)) elif frame.opcode in OPCODE_DATA: return frame.opcode, frame.data elif frame.opcode == websocket.ABNF.OPCODE_CLOSE: ws.send_close() - return frame.opcode, None + return frame.opcode, "" elif frame.opcode == websocket.ABNF.OPCODE_PING: ws.pong(frame.data) return frame.opcode, frame.data return frame.opcode, frame.data - def recv_ws(): + def recv_ws() -> None: while True: opcode, data = recv() msg = None @@ -193,7 +193,7 @@ def main(): data = repr(data) if args.verbose: - msg = "%s: %s" % (websocket.ABNF.OPCODE_MAP.get(opcode), data) + msg = "{opcode}: {data}".format(opcode=websocket.ABNF.OPCODE_MAP.get(opcode), data=data) else: msg = data diff --git a/lib/websocket/tests/echo-server.py b/lib/websocket/tests/echo-server.py index 08d108ab..42dd9e43 100644 --- a/lib/websocket/tests/echo-server.py +++ b/lib/websocket/tests/echo-server.py @@ -6,7 +6,7 @@ import asyncio import websockets import os -LOCAL_WS_SERVER_PORT = os.environ.get('LOCAL_WS_SERVER_PORT', '8765') +LOCAL_WS_SERVER_PORT = int(os.environ.get('LOCAL_WS_SERVER_PORT', '8765')) async def echo(websocket, path): diff --git a/lib/websocket/tests/test_abnf.py b/lib/websocket/tests/test_abnf.py index 7c9d89d8..dbf9b636 100644 --- a/lib/websocket/tests/test_abnf.py +++ b/lib/websocket/tests/test_abnf.py @@ -8,7 +8,7 @@ import unittest test_abnf.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/lib/websocket/tests/test_app.py b/lib/websocket/tests/test_app.py index ac563c6e..ff90a0aa 100644 --- a/lib/websocket/tests/test_app.py +++ b/lib/websocket/tests/test_app.py @@ -11,7 +11,7 @@ import unittest test_app.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -112,25 +112,6 @@ class WebSocketAppTest(unittest.TestCase): teardown = app.run_forever() self.assertEqual(teardown, False) - @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") - def testRunForeverTeardownExceptionalExit(self): - """ The WebSocketApp.run_forever() method should return `True` when the application ends with an exception. - It should also invoke the `on_error` callback before exiting. - """ - - def break_it(): - # Deliberately break the WebSocketApp by closing the inner socket. - app.sock.close() - - def on_error(_, err): - WebSocketAppTest.on_error_data = str(err) - - app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT, on_error=on_error) - threading.Timer(interval=0.2, function=break_it).start() - teardown = app.run_forever(ping_timeout=0.1) - self.assertEqual(teardown, True) - self.assertTrue(len(WebSocketAppTest.on_error_data) > 0) - @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") def testSockMaskKey(self): """ A WebSocketApp should forward the received mask_key function down @@ -310,8 +291,8 @@ class WebSocketAppTest(unittest.TestCase): app.run_forever(ping_interval=2, ping_timeout=1, reconnect=3) self.assertEqual(pong_count, 2) - self.assertIsInstance(exc, ValueError) - self.assertEqual(str(exc), "Invalid file object: None") + self.assertIsInstance(exc, ws.WebSocketTimeoutException) + self.assertEqual(str(exc), "ping/pong timed out") if __name__ == "__main__": diff --git a/lib/websocket/tests/test_cookiejar.py b/lib/websocket/tests/test_cookiejar.py index 559b2e00..8f835e9e 100644 --- a/lib/websocket/tests/test_cookiejar.py +++ b/lib/websocket/tests/test_cookiejar.py @@ -5,7 +5,7 @@ from websocket._cookiejar import SimpleCookieJar test_cookiejar.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/lib/websocket/tests/test_http.py b/lib/websocket/tests/test_http.py index ffcbde25..d7de29d3 100644 --- a/lib/websocket/tests/test_http.py +++ b/lib/websocket/tests/test_http.py @@ -13,7 +13,7 @@ import socket test_http.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/lib/websocket/tests/test_url.py b/lib/websocket/tests/test_url.py index 7e155fd1..a74dd766 100644 --- a/lib/websocket/tests/test_url.py +++ b/lib/websocket/tests/test_url.py @@ -8,7 +8,7 @@ from websocket._url import get_proxy_info, parse_url, _is_address_in_network, _i test_url.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -254,6 +254,24 @@ class ProxyInfoTest(unittest.TestCase): os.environ["https_proxy"] = "http://localhost2:3128/" self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, None)) + os.environ["http_proxy"] = "" + os.environ["https_proxy"] = "http://localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", None, None)) + self.assertEqual(get_proxy_info("echo.websocket.events", False), (None, 0, None)) + os.environ["http_proxy"] = "" + os.environ["https_proxy"] = "http://localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, None)) + self.assertEqual(get_proxy_info("echo.websocket.events", False), (None, 0, None)) + + os.environ["http_proxy"] = "http://localhost/" + os.environ["https_proxy"] = "" + self.assertEqual(get_proxy_info("echo.websocket.events", True), (None, 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + os.environ["https_proxy"] = "" + self.assertEqual(get_proxy_info("echo.websocket.events", True), (None, 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, None)) + os.environ["http_proxy"] = "http://a:b@localhost/" self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, ("a", "b"))) os.environ["http_proxy"] = "http://a:b@localhost:3128/" diff --git a/lib/websocket/tests/test_websocket.py b/lib/websocket/tests/test_websocket.py index d47d73e5..a140066e 100644 --- a/lib/websocket/tests/test_websocket.py +++ b/lib/websocket/tests/test_websocket.py @@ -15,7 +15,7 @@ from base64 import decodebytes as base64decode test_websocket.py websocket - WebSocket client library for Python -Copyright 2022 engn33r +Copyright 2023 engn33r Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/requirements.txt b/requirements.txt index 0ac5222b..09b22544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ tzdata==2023.3 tzlocal==4.2 urllib3==2.0.4 webencodings==0.5.1 -websocket-client==1.5.1 +websocket-client==1.6.2 xmltodict==0.13.0 zipp==3.15.0