Bump websocket-client from 1.5.1 to 1.6.2 (#2122)

* Bump websocket-client from 1.5.1 to 1.6.2

Bumps [websocket-client](https://github.com/websocket-client/websocket-client) from 1.5.1 to 1.6.2.
- [Release notes](https://github.com/websocket-client/websocket-client/releases)
- [Changelog](https://github.com/websocket-client/websocket-client/blob/master/ChangeLog)
- [Commits](https://github.com/websocket-client/websocket-client/compare/v1.5.1...v1.6.2)

---
updated-dependencies:
- dependency-name: websocket-client
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update websocket-client==1.6.2

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2023-08-23 21:45:28 -07:00 committed by GitHub
parent c93f470371
commit eac78a3047
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 295 additions and 260 deletions

View file

@ -2,7 +2,7 @@
__init__.py __init__.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 ._logging import *
from ._socket import * from ._socket import *
__version__ = "1.5.1" __version__ = "1.6.2"

View file

@ -11,7 +11,7 @@ from threading import Lock
_abnf.py _abnf.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -33,14 +33,14 @@ try:
# Note that wsaccel is unmaintained. # Note that wsaccel is unmaintained.
from wsaccel.xormask import XorMaskerSimple from wsaccel.xormask import XorMaskerSimple
def _mask(_m, _d): def _mask(_m, _d) -> bytes:
return XorMaskerSimple(_m).process(_d) return XorMaskerSimple(_m).process(_d)
except ImportError: except ImportError:
# wsaccel is not available, use websocket-client _mask() # wsaccel is not available, use websocket-client _mask()
native_byteorder = sys.byteorder 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) datalen = len(data_value)
data_value = int.from_bytes(data_value, native_byteorder) data_value = int.from_bytes(data_value, native_byteorder)
mask_value = int.from_bytes(mask_value * (datalen // 4) + mask_value[: datalen % 4], 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_16 = 1 << 16
LENGTH_63 = 1 << 63 LENGTH_63 = 1 << 63
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, def __init__(self, fin: int = 0, rsv1: int = 0, rsv2: int = 0, rsv3: int = 0,
opcode=OPCODE_TEXT, mask=1, data=""): opcode: int = OPCODE_TEXT, mask: int = 1, data: str or bytes = "") -> None:
""" """
Constructor for ABNF. Please check RFC for arguments. Constructor for ABNF. Please check RFC for arguments.
""" """
@ -147,7 +147,7 @@ class ABNF:
self.data = data self.data = data
self.get_mask_key = os.urandom 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. Validate the ABNF frame.
@ -187,19 +187,19 @@ class ABNF:
+ " data=" + str(self.data) + " data=" + str(self.data)
@staticmethod @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. Create frame to send text, binary and other data.
Parameters Parameters
---------- ----------
data: <type> data: str
data to send. This is string value(byte array). data to send. This is string value(byte array).
If opcode is OPCODE_TEXT and this value is unicode, If opcode is OPCODE_TEXT and this value is unicode,
data value is converted into unicode string, automatically. data value is converted into unicode string, automatically.
opcode: <type> opcode: int
operation code. please see OPCODE_XXX. operation code. please see OPCODE_MAP.
fin: <type> fin: int
fin flag. if set to 0, create continue fragmentation. fin flag. if set to 0, create continue fragmentation.
""" """
if opcode == ABNF.OPCODE_TEXT and isinstance(data, str): if opcode == ABNF.OPCODE_TEXT and isinstance(data, str):
@ -237,7 +237,7 @@ class ABNF:
mask_key = self.get_mask_key(4) mask_key = self.get_mask_key(4)
return frame_header + self._get_masked(mask_key) 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) s = ABNF.mask(mask_key, self.data)
if isinstance(mask_key, str): if isinstance(mask_key, str):
@ -246,7 +246,7 @@ class ABNF:
return mask_key + s return mask_key + s
@staticmethod @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 Mask or unmask data. Just do xor for each byte
@ -273,7 +273,7 @@ class frame_buffer:
_HEADER_MASK_INDEX = 5 _HEADER_MASK_INDEX = 5
_HEADER_LENGTH_INDEX = 6 _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.recv = recv_fn
self.skip_utf8_validation = skip_utf8_validation self.skip_utf8_validation = skip_utf8_validation
# Buffers over the packets from the layer beneath until desired amount # Buffers over the packets from the layer beneath until desired amount
@ -282,7 +282,7 @@ class frame_buffer:
self.clear() self.clear()
self.lock = Lock() self.lock = Lock()
def clear(self): def clear(self) -> None:
self.header = None self.header = None
self.length = None self.length = None
self.mask = None self.mask = None
@ -290,7 +290,7 @@ class frame_buffer:
def has_received_header(self) -> bool: def has_received_header(self) -> bool:
return self.header is None return self.header is None
def recv_header(self): def recv_header(self) -> None:
header = self.recv_strict(2) header = self.recv_strict(2)
b1 = header[0] b1 = header[0]
fin = b1 >> 7 & 1 fin = b1 >> 7 & 1
@ -304,7 +304,7 @@ class frame_buffer:
self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits) 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: if not self.header:
return False return False
return self.header[frame_buffer._HEADER_MASK_INDEX] return self.header[frame_buffer._HEADER_MASK_INDEX]
@ -312,7 +312,7 @@ class frame_buffer:
def has_received_length(self) -> bool: def has_received_length(self) -> bool:
return self.length is None return self.length is None
def recv_length(self): def recv_length(self) -> None:
bits = self.header[frame_buffer._HEADER_LENGTH_INDEX] bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]
length_bits = bits & 0x7f length_bits = bits & 0x7f
if length_bits == 0x7e: if length_bits == 0x7e:
@ -327,10 +327,10 @@ class frame_buffer:
def has_received_mask(self) -> bool: def has_received_mask(self) -> bool:
return self.mask is None 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 "" self.mask = self.recv_strict(4) if self.has_mask() else ""
def recv_frame(self): def recv_frame(self) -> ABNF:
with self.lock: with self.lock:
# Header # Header
@ -386,20 +386,20 @@ class frame_buffer:
class continuous_frame: 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.fire_cont_frame = fire_cont_frame
self.skip_utf8_validation = skip_utf8_validation self.skip_utf8_validation = skip_utf8_validation
self.cont_data = None self.cont_data = None
self.recving_frames = 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: if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
raise WebSocketProtocolException("Illegal frame") raise WebSocketProtocolException("Illegal frame")
if self.recving_frames and \ if self.recving_frames and \
frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
raise WebSocketProtocolException("Illegal frame") raise WebSocketProtocolException("Illegal frame")
def add(self, frame): def add(self, frame: ABNF) -> None:
if self.cont_data: if self.cont_data:
self.cont_data[1] += frame.data self.cont_data[1] += frame.data
else: else:
@ -410,10 +410,10 @@ class continuous_frame:
if frame.fin: if frame.fin:
self.recving_frames = None 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 return frame.fin or self.fire_cont_frame
def extract(self, frame): def extract(self, frame: ABNF) -> list:
data = self.cont_data data = self.cont_data
self.cont_data = None self.cont_data = None
frame.data = data[1] frame.data = data[1]

View file

@ -4,6 +4,9 @@ import sys
import threading import threading
import time import time
import traceback import traceback
import socket
from typing import Callable, Any
from . import _logging from . import _logging
from ._abnf import ABNF from ._abnf import ABNF
@ -15,7 +18,7 @@ from ._exceptions import *
_app.py _app.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -35,7 +38,7 @@ __all__ = ["WebSocketApp"]
RECONNECT = 0 RECONNECT = 0
def setReconnect(reconnectInterval): def setReconnect(reconnectInterval: int) -> None:
global RECONNECT global RECONNECT
RECONNECT = reconnectInterval RECONNECT = reconnectInterval
@ -44,37 +47,40 @@ class DispatcherBase:
""" """
DispatcherBase DispatcherBase
""" """
def __init__(self, app, ping_timeout): def __init__(self, app: Any, ping_timeout: float) -> None:
self.app = app self.app = app
self.ping_timeout = ping_timeout self.ping_timeout = ping_timeout
def timeout(self, seconds, callback): def timeout(self, seconds: int, callback: Callable) -> None:
time.sleep(seconds) time.sleep(seconds)
callback() callback()
def reconnect(self, seconds, reconnector): def reconnect(self, seconds: int, reconnector: Callable) -> None:
try: 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) time.sleep(seconds)
reconnector(reconnecting=True) reconnector(reconnecting=True)
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
_logging.info("User exited %s" % (e,)) _logging.info("User exited {err}".format(err=e))
raise e
class Dispatcher(DispatcherBase): class Dispatcher(DispatcherBase):
""" """
Dispatcher Dispatcher
""" """
def read(self, sock, read_callback, check_callback): def read(self, sock: socket.socket, read_callback: Callable, check_callback: Callable) -> None:
while self.app.keep_running:
sel = selectors.DefaultSelector() sel = selectors.DefaultSelector()
sel.register(self.app.sock.sock, selectors.EVENT_READ) sel.register(self.app.sock.sock, selectors.EVENT_READ)
try:
while self.app.keep_running:
r = sel.select(self.ping_timeout) r = sel.select(self.ping_timeout)
if r: if r:
if not read_callback(): if not read_callback():
break break
check_callback() check_callback()
finally:
sel.close() sel.close()
@ -82,24 +88,26 @@ class SSLDispatcher(DispatcherBase):
""" """
SSLDispatcher SSLDispatcher
""" """
def read(self, sock, read_callback, 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: while self.app.keep_running:
r = self.select() r = self.select(sock, sel)
if r: if r:
if not read_callback(): if not read_callback():
break break
check_callback() check_callback()
finally:
sel.close()
def select(self): def select(self, sock, sel:selectors.DefaultSelector):
sock = self.app.sock.sock sock = self.app.sock.sock
if sock.pending(): if sock.pending():
return [sock,] return [sock,]
sel = selectors.DefaultSelector()
sel.register(sock, selectors.EVENT_READ)
r = sel.select(self.ping_timeout) r = sel.select(self.ping_timeout)
sel.close()
if len(r) > 0: if len(r) > 0:
return r[0][0] return r[0][0]
@ -109,20 +117,20 @@ class WrappedDispatcher:
""" """
WrappedDispatcher WrappedDispatcher
""" """
def __init__(self, app, ping_timeout, dispatcher): def __init__(self, app, ping_timeout: float, dispatcher: Dispatcher) -> None:
self.app = app self.app = app
self.ping_timeout = ping_timeout self.ping_timeout = ping_timeout
self.dispatcher = dispatcher self.dispatcher = dispatcher
dispatcher.signal(2, dispatcher.abort) # keyboard interrupt 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.dispatcher.read(sock, read_callback)
self.ping_timeout and self.timeout(self.ping_timeout, check_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) self.dispatcher.timeout(seconds, callback)
def reconnect(self, seconds, reconnector): def reconnect(self, seconds: int, reconnector: Callable) -> None:
self.timeout(seconds, reconnector) self.timeout(seconds, reconnector)
@ -131,14 +139,14 @@ class WebSocketApp:
Higher level of APIs are provided. The interface is like JavaScript WebSocket object. Higher level of APIs are provided. The interface is like JavaScript WebSocket object.
""" """
def __init__(self, url, header=None, def __init__(self, url: str, header: list or dict or Callable = None,
on_open=None, on_message=None, on_error=None, on_open: Callable = None, on_message: Callable = None, on_error: Callable = None,
on_close=None, on_ping=None, on_pong=None, on_close: Callable = None, on_ping: Callable = None, on_pong: Callable = None,
on_cont_message=None, on_cont_message: Callable = None,
keep_running=True, get_mask_key=None, cookie=None, keep_running: bool = True, get_mask_key: Callable = None, cookie: str = None,
subprotocols=None, subprotocols: list = None,
on_data=None, on_data: Callable = None,
socket=None): socket: socket.socket = None) -> None:
""" """
WebSocketApp initialization WebSocketApp initialization
@ -146,8 +154,11 @@ class WebSocketApp:
---------- ----------
url: str url: str
Websocket url. Websocket url.
header: list or dict header: list or dict or Callable
Custom header for websocket handshake. 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 on_open: function
Callback object which is called at opening websocket. Callback object which is called at opening websocket.
on_open has one argument. on_open has one argument.
@ -222,8 +233,10 @@ class WebSocketApp:
self.subprotocols = subprotocols self.subprotocols = subprotocols
self.prepared_socket = socket self.prepared_socket = socket
self.has_errored = False 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 send message
@ -240,7 +253,7 @@ class WebSocketApp:
raise WebSocketConnectionClosedException( raise WebSocketConnectionClosedException(
"Connection is already closed.") "Connection is already closed.")
def close(self, **kwargs): def close(self, **kwargs) -> None:
""" """
Close websocket connection. Close websocket connection.
""" """
@ -249,41 +262,41 @@ class WebSocketApp:
self.sock.close(**kwargs) self.sock.close(**kwargs)
self.sock = None self.sock = None
def _start_ping_thread(self): def _start_ping_thread(self) -> None:
self.last_ping_tm = self.last_pong_tm = 0 self.last_ping_tm = self.last_pong_tm = 0
self.stop_ping = threading.Event() self.stop_ping = threading.Event()
self.ping_thread = threading.Thread(target=self._send_ping) self.ping_thread = threading.Thread(target=self._send_ping)
self.ping_thread.daemon = True self.ping_thread.daemon = True
self.ping_thread.start() self.ping_thread.start()
def _stop_ping_thread(self): def _stop_ping_thread(self) -> None:
if self.stop_ping: if self.stop_ping:
self.stop_ping.set() self.stop_ping.set()
if self.ping_thread and self.ping_thread.is_alive(): if self.ping_thread and self.ping_thread.is_alive():
self.ping_thread.join(3) self.ping_thread.join(3)
self.last_ping_tm = self.last_pong_tm = 0 self.last_ping_tm = self.last_pong_tm = 0
def _send_ping(self): def _send_ping(self) -> None:
if self.stop_ping.wait(self.ping_interval): if self.stop_ping.wait(self.ping_interval) or self.keep_running is False:
return 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: if self.sock:
self.last_ping_tm = time.time() self.last_ping_tm = time.time()
try: try:
_logging.debug("Sending ping") _logging.debug("Sending ping")
self.sock.ping(self.ping_payload) self.sock.ping(self.ping_payload)
except Exception as ex: except Exception as e:
_logging.debug("Failed to send ping: %s", ex) _logging.debug("Failed to send ping: {err}".format(err=e))
def run_forever(self, sockopt=None, sslopt=None, def run_forever(self, sockopt: tuple = None, sslopt: dict = None,
ping_interval=0, ping_timeout=None, ping_interval: float = 0, ping_timeout: float or None = None,
ping_payload="", ping_payload: str = "",
http_proxy_host=None, http_proxy_port=None, http_proxy_host: str = None, http_proxy_port: int or str = None,
http_no_proxy=None, http_proxy_auth=None, http_no_proxy: list = None, http_proxy_auth: tuple = None,
http_proxy_timeout=None, http_proxy_timeout: float = None,
skip_utf8_validation=False, skip_utf8_validation: bool = False,
host=None, origin=None, dispatcher=None, host: str = None, origin: str = None, dispatcher: Dispatcher = None,
suppress_origin=False, proxy_type=None, reconnect=None): suppress_origin: bool = False, proxy_type: str = None, reconnect: int = None) -> bool:
""" """
Run event loop for WebSocket framework. Run event loop for WebSocket framework.
@ -358,7 +371,7 @@ class WebSocketApp:
self.ping_payload = ping_payload self.ping_payload = ping_payload
self.keep_running = True self.keep_running = True
def teardown(close_frame=None): def teardown(close_frame: ABNF = None):
""" """
Tears down the connection. Tears down the connection.
@ -369,6 +382,13 @@ class WebSocketApp:
with the statusCode and reason from the provided frame. 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._stop_ping_thread()
self.keep_running = False self.keep_running = False
if self.sock: if self.sock:
@ -380,7 +400,7 @@ class WebSocketApp:
# Finally call the callback AFTER all teardown is complete # Finally call the callback AFTER all teardown is complete
self._callback(self.on_close, close_status_code, close_reason) 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: if reconnecting and self.sock:
self.sock.shutdown() self.sock.shutdown()
@ -392,8 +412,11 @@ class WebSocketApp:
self.sock.settimeout(getdefaulttimeout()) self.sock.settimeout(getdefaulttimeout())
try: try:
header = self.header() if callable(self.header) else self.header
self.sock.connect( 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_host=http_proxy_host,
http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy, http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy,
http_proxy_auth=http_proxy_auth, http_proxy_timeout=http_proxy_timeout, 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: except (WebSocketConnectionClosedException, ConnectionRefusedError, KeyboardInterrupt, SystemExit, Exception) as e:
handleDisconnect(e, reconnecting) handleDisconnect(e, reconnecting)
def read(): def read() -> bool:
if not self.keep_running: if not self.keep_running:
return teardown() return teardown()
@ -445,7 +468,7 @@ class WebSocketApp:
return True return True
def check(): def check() -> bool:
if (self.ping_timeout): if (self.ping_timeout):
has_timeout_expired = time.time() - self.last_ping_tm > 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 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") raise WebSocketTimeoutException("ping/pong timed out")
return True return True
def handleDisconnect(e, reconnecting=False): def handleDisconnect(e: Exception, reconnecting: bool = False) -> bool:
self.has_errored = True self.has_errored = True
self._stop_ping_thread() self._stop_ping_thread()
if not reconnecting: if not reconnecting:
@ -469,26 +492,34 @@ class WebSocketApp:
raise raise
if reconnect: if reconnect:
_logging.info("%s - reconnect" % e) _logging.info("{err} - reconnect".format(err=e))
if custom_dispatcher: 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) dispatcher.reconnect(reconnect, setSock)
else: else:
_logging.error("%s - goodbye" % e) _logging.error("{err} - goodbye".format(err=e))
teardown() teardown()
custom_dispatcher = bool(dispatcher) custom_dispatcher = bool(dispatcher)
dispatcher = self.create_dispatcher(ping_timeout, dispatcher, parse_url(self.url)[3]) dispatcher = self.create_dispatcher(ping_timeout, dispatcher, parse_url(self.url)[3])
try:
setSock() setSock()
if not custom_dispatcher and reconnect: if not custom_dispatcher and reconnect:
while self.keep_running: while self.keep_running:
_logging.debug("Calling dispatcher reconnect [%s frames in stack]" % len(inspect.stack())) _logging.debug("Calling dispatcher reconnect [{frame_count} frames in stack]".format(frame_count=len(inspect.stack())))
dispatcher.reconnect(reconnect, setSock) 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 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 if dispatcher: # If custom dispatcher is set, use WrappedDispatcher
return WrappedDispatcher(self, ping_timeout, dispatcher) return WrappedDispatcher(self, ping_timeout, dispatcher)
timeout = ping_timeout or 10 timeout = ping_timeout or 10
@ -497,7 +528,7 @@ class WebSocketApp:
return Dispatcher(self, timeout) 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 _get_close_args extracts the close code and reason from the close body
if it exists (RFC6455 says WebSocket Connection Close Code is optional) 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 # Most likely reached this because len(close_frame_data.data) < 2
return [None, None] return [None, None]
def _callback(self, callback, *args): def _callback(self, callback, *args) -> None:
if callback: if callback:
try: try:
callback(self, *args) callback(self, *args)
except Exception as e: 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: if self.on_error:
self.on_error(self, e) self.on_error(self, e)

View file

@ -4,7 +4,7 @@ import http.cookies
_cookiejar.py _cookiejar.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -21,10 +21,10 @@ limitations under the License.
class SimpleCookieJar: class SimpleCookieJar:
def __init__(self): def __init__(self) -> None:
self.jar = dict() self.jar = dict()
def add(self, set_cookie): def add(self, set_cookie: str) -> None:
if set_cookie: if set_cookie:
simpleCookie = http.cookies.SimpleCookie(set_cookie) simpleCookie = http.cookies.SimpleCookie(set_cookie)
@ -37,7 +37,7 @@ class SimpleCookieJar:
cookie.update(simpleCookie) cookie.update(simpleCookie)
self.jar[domain.lower()] = cookie self.jar[domain.lower()] = cookie
def set(self, set_cookie): def set(self, set_cookie: str) -> None:
if set_cookie: if set_cookie:
simpleCookie = http.cookies.SimpleCookie(set_cookie) simpleCookie = http.cookies.SimpleCookie(set_cookie)
@ -48,7 +48,7 @@ class SimpleCookieJar:
domain = "." + domain domain = "." + domain
self.jar[domain.lower()] = simpleCookie self.jar[domain.lower()] = simpleCookie
def get(self, host): def get(self, host: str) -> str:
if not host: if not host:
return "" return ""

View file

@ -17,7 +17,7 @@ from ._utils import *
_core.py _core.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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, def __init__(self, get_mask_key=None, sockopt=None, sslopt=None,
fire_cont_frame=False, enable_multithread=True, fire_cont_frame: bool = False, enable_multithread: bool = True,
skip_utf8_validation=False, **_): skip_utf8_validation: bool = False, **_):
""" """
Initialize WebSocket object. Initialize WebSocket object.
@ -133,7 +133,7 @@ class WebSocket:
""" """
self.get_mask_key = func self.get_mask_key = func
def gettimeout(self): def gettimeout(self) -> float:
""" """
Get the websocket timeout (in seconds) as an int or float Get the websocket timeout (in seconds) as an int or float
@ -144,7 +144,7 @@ class WebSocket:
""" """
return self.sock_opt.timeout return self.sock_opt.timeout
def settimeout(self, timeout): def settimeout(self, timeout: float):
""" """
Set the timeout to the websocket. Set the timeout to the websocket.
@ -265,7 +265,7 @@ class WebSocket:
self.sock = None self.sock = None
raise 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. Send the data as string.
@ -282,7 +282,7 @@ class WebSocket:
frame = ABNF.create_frame(payload, opcode) frame = ABNF.create_frame(payload, opcode)
return self.send_frame(frame) return self.send_frame(frame)
def send_frame(self, frame): def send_frame(self, frame) -> int:
""" """
Send the data frame. Send the data frame.
@ -313,7 +313,7 @@ class WebSocket:
return length return length
def send_binary(self, payload): def send_binary(self, payload: bytes) -> int:
""" """
Send a binary message (OPCODE_BINARY). Send a binary message (OPCODE_BINARY).
@ -324,7 +324,7 @@ class WebSocket:
""" """
return self.send(payload, ABNF.OPCODE_BINARY) return self.send(payload, ABNF.OPCODE_BINARY)
def ping(self, payload=""): def ping(self, payload: str or bytes = ""):
""" """
Send ping data. Send ping data.
@ -337,7 +337,7 @@ class WebSocket:
payload = payload.encode("utf-8") payload = payload.encode("utf-8")
self.send(payload, ABNF.OPCODE_PING) self.send(payload, ABNF.OPCODE_PING)
def pong(self, payload=""): def pong(self, payload: str or bytes = ""):
""" """
Send pong data. Send pong data.
@ -350,7 +350,7 @@ class WebSocket:
payload = payload.encode("utf-8") payload = payload.encode("utf-8")
self.send(payload, ABNF.OPCODE_PONG) self.send(payload, ABNF.OPCODE_PONG)
def recv(self): def recv(self) -> str or bytes:
""" """
Receive string data(byte array) from the server. Receive string data(byte array) from the server.
@ -367,7 +367,7 @@ class WebSocket:
else: else:
return '' return ''
def recv_data(self, control_frame=False): def recv_data(self, control_frame: bool = False) -> tuple:
""" """
Receive data with operation code. Receive data with operation code.
@ -385,7 +385,7 @@ class WebSocket:
opcode, frame = self.recv_data_frame(control_frame) opcode, frame = self.recv_data_frame(control_frame)
return opcode, frame.data 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. Receive data with operation code.
@ -411,7 +411,7 @@ class WebSocket:
# handle error: # handle error:
# 'NoneType' object has no attribute 'opcode' # 'NoneType' object has no attribute 'opcode'
raise WebSocketProtocolException( 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): elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
self.cont_frame.validate(frame) self.cont_frame.validate(frame)
self.cont_frame.add(frame) self.cont_frame.add(frame)
@ -444,7 +444,7 @@ class WebSocket:
""" """
return self.frame_buffer.recv_frame() 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. Send close data to the server.
@ -460,14 +460,14 @@ class WebSocket:
self.connected = False self.connected = False
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) 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 Close Websocket object
Parameters Parameters
---------- ----------
status: int status: int
Status code to send. See STATUS_XXX. Status code to send. See VALID_CLOSE_STATUS in ABNF.
reason: bytes reason: bytes
The reason to close in UTF-8. The reason to close in UTF-8.
timeout: int or float timeout: int or float
@ -521,7 +521,7 @@ class WebSocket:
self.sock = None self.sock = None
self.connected = False self.connected = False
def _send(self, data): def _send(self, data: str or bytes):
return send(self.sock, data) return send(self.sock, data)
def _recv(self, bufsize): def _recv(self, bufsize):
@ -535,7 +535,7 @@ class WebSocket:
raise 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. Connect to url and return websocket object.

View file

@ -2,7 +2,7 @@
_exceptions.py _exceptions.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. WebSocketBadStatusException will be raised when we get bad handshake status code.
""" """
def __init__(self, message, status_code, status_message=None, resp_headers=None): def __init__(self, message: str, status_code: int, status_message=None, resp_headers=None, resp_body=None):
msg = message % (status_code, status_message) super().__init__(message)
super().__init__(msg)
self.status_code = status_code self.status_code = status_code
self.resp_headers = resp_headers self.resp_headers = resp_headers
self.resp_body = resp_body
class WebSocketAddressException(WebSocketException): class WebSocketAddressException(WebSocketException):

View file

@ -2,7 +2,7 @@
_handshake.py _handshake.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -40,14 +40,14 @@ CookieJar = SimpleCookieJar()
class handshake_response: class handshake_response:
def __init__(self, status, headers, subprotocol): def __init__(self, status: int, headers: dict, subprotocol):
self.status = status self.status = status
self.headers = headers self.headers = headers
self.subprotocol = subprotocol self.subprotocol = subprotocol
CookieJar.add(headers.get("set-cookie")) 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) headers, key = _get_handshake_headers(resource, url, hostname, port, options)
header_str = "\r\n".join(headers) header_str = "\r\n".join(headers)
@ -64,7 +64,7 @@ def handshake(sock, url, hostname, port, resource, **options):
return handshake_response(status, resp, subproto) return handshake_response(status, resp, subproto)
def _pack_hostname(hostname): def _pack_hostname(hostname: str) -> str:
# IPv6 address # IPv6 address
if ':' in hostname: if ':' in hostname:
return '[' + hostname + ']' return '[' + hostname + ']'
@ -72,41 +72,41 @@ def _pack_hostname(hostname):
return 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 = [ headers = [
"GET %s HTTP/1.1" % resource, "GET {resource} HTTP/1.1".format(resource=resource),
"Upgrade: websocket" "Upgrade: websocket"
] ]
if port == 80 or port == 443: if port == 80 or port == 443:
hostport = _pack_hostname(host) hostport = _pack_hostname(host)
else: else:
hostport = "%s:%d" % (_pack_hostname(host), port) hostport = "{h}:{p}".format(h=_pack_hostname(host), p=port)
if options.get("host"): if options.get("host"):
headers.append("Host: %s" % options["host"]) headers.append("Host: {h}".format(h=options["host"]))
else: else:
headers.append("Host: %s" % hostport) headers.append("Host: {hp}".format(hp=hostport))
# scheme indicates whether http or https is used in Origin # 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 # The same approach is used in parse_url of _url.py to set default port
scheme, url = url.split(":", 1) scheme, url = url.split(":", 1)
if not options.get("suppress_origin"): if not options.get("suppress_origin"):
if "origin" in options and options["origin"] is not None: 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": elif scheme == "wss":
headers.append("Origin: https://%s" % hostport) headers.append("Origin: https://{hp}".format(hp=hostport))
else: else:
headers.append("Origin: http://%s" % hostport) headers.append("Origin: http://{hp}".format(hp=hostport))
key = _create_sec_websocket_key() key = _create_sec_websocket_key()
# Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
if not options.get('header') or 'Sec-WebSocket-Key' not in options['header']: 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: else:
key = options['header']['Sec-WebSocket-Key'] key = options['header']['Sec-WebSocket-Key']
if not options.get('header') or 'Sec-WebSocket-Version' not in options['header']: 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'): if not options.get('connection'):
headers.append('Connection: Upgrade') headers.append('Connection: Upgrade')
@ -115,7 +115,7 @@ def _get_handshake_headers(resource, url, host, port, options):
subprotocols = options.get("subprotocols") subprotocols = options.get("subprotocols")
if 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") header = options.get("header")
if header: if header:
@ -133,18 +133,21 @@ def _get_handshake_headers(resource, url, host, port, options):
cookie = "; ".join(filter(None, [server_cookie, client_cookie])) cookie = "; ".join(filter(None, [server_cookie, client_cookie]))
if cookie: if cookie:
headers.append("Cookie: %s" % cookie) headers.append("Cookie: {cookie}".format(cookie=cookie))
headers.append("")
headers.append("")
headers.extend(("", ""))
return headers, key 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) status, resp_headers, status_message = read_headers(sock)
if status not in success_statuses: 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 return status, resp_headers
@ -154,7 +157,7 @@ _HEADERS_TO_CHECK = {
} }
def _validate(headers, key, subprotocols): def _validate(headers, key: str, subprotocols):
subproto = None subproto = None
for k, v in _HEADERS_TO_CHECK.items(): for k, v in _HEADERS_TO_CHECK.items():
r = headers.get(k, None) r = headers.get(k, None)
@ -189,6 +192,6 @@ def _validate(headers, key, subprotocols):
return False, None return False, None
def _create_sec_websocket_key(): def _create_sec_websocket_key() -> str:
randomness = os.urandom(16) randomness = os.urandom(16)
return base64encode(randomness).decode('utf-8').strip() return base64encode(randomness).decode('utf-8').strip()

View file

@ -2,7 +2,7 @@
_http.py _http.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 errno
import os import os
import socket import socket
import sys
from ._exceptions import * from ._exceptions import *
from ._logging import * from ._logging import *
@ -69,7 +68,7 @@ class proxy_info:
self.proxy_protocol = "http" self.proxy_protocol = "http"
def _start_proxied_socket(url, options, proxy): def _start_proxied_socket(url: str, options, proxy):
if not HAVE_PYTHON_SOCKS: if not HAVE_PYTHON_SOCKS:
raise WebSocketException("Python Socks is needed for SOCKS proxying but is not available") 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) 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 _start_proxied_socket() only for socks4 or socks5 proxy
# Use _tunnel() for http proxy # Use _tunnel() for http proxy
# TODO: Use python-socks for http protocol also, to standardize flow # 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) context = sslopt.get('context', None)
if not context: if not context:
context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_TLS_CLIENT)) 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: if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
cafile = sslopt.get('ca_certs', None) cafile = sslopt.get('ca_certs', None)
@ -275,8 +279,8 @@ def _ssl_socket(sock, user_sslopt, hostname):
def _tunnel(sock, host, port, auth): def _tunnel(sock, host, port, auth):
debug("Connecting proxy...") debug("Connecting proxy...")
connect_header = "CONNECT %s:%d HTTP/1.1\r\n" % (host, port) connect_header = "CONNECT {h}:{p} HTTP/1.1\r\n".format(h=host, p=port)
connect_header += "Host: %s:%d\r\n" % (host, port) connect_header += "Host: {h}:{p}\r\n".format(h=host, p=port)
# TODO: support digest auth. # TODO: support digest auth.
if auth and auth[0]: if auth and auth[0]:
@ -284,7 +288,7 @@ def _tunnel(sock, host, port, auth):
if auth[1]: if auth[1]:
auth_str += ":" + auth[1] auth_str += ":" + auth[1]
encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '') 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" connect_header += "\r\n"
dump("request header", connect_header) dump("request header", connect_header)
@ -297,7 +301,7 @@ def _tunnel(sock, host, port, auth):
if status != 200: if status != 200:
raise WebSocketProxyException( raise WebSocketProxyException(
"failed CONNECT via proxy status: %r" % status) "failed CONNECT via proxy status: {status}".format(status=status))
return sock return sock

View file

@ -4,7 +4,7 @@ import logging
_logging.py _logging.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -24,7 +24,7 @@ try:
from logging import NullHandler from logging import NullHandler
except ImportError: except ImportError:
class NullHandler(logging.Handler): class NullHandler(logging.Handler):
def emit(self, record): def emit(self, record) -> None:
pass pass
_logger.addHandler(NullHandler()) _logger.addHandler(NullHandler())
@ -35,7 +35,9 @@ __all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace",
"isEnabledForError", "isEnabledForDebug", "isEnabledForTrace"] "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. Turn on/off the traceability.
@ -51,41 +53,41 @@ def enableTrace(traceable, handler=logging.StreamHandler(), level="DEBUG"):
_logger.setLevel(getattr(logging, level)) _logger.setLevel(getattr(logging, level))
def dump(title, message): def dump(title: str, message: str) -> None:
if _traceEnabled: if _traceEnabled:
_logger.debug("--- " + title + " ---") _logger.debug("--- " + title + " ---")
_logger.debug(message) _logger.debug(message)
_logger.debug("-----------------------") _logger.debug("-----------------------")
def error(msg): def error(msg: str) -> None:
_logger.error(msg) _logger.error(msg)
def warning(msg): def warning(msg: str) -> None:
_logger.warning(msg) _logger.warning(msg)
def debug(msg): def debug(msg: str) -> None:
_logger.debug(msg) _logger.debug(msg)
def info(msg): def info(msg: str) -> None:
_logger.info(msg) _logger.info(msg)
def trace(msg): def trace(msg: str) -> None:
if _traceEnabled: if _traceEnabled:
_logger.debug(msg) _logger.debug(msg)
def isEnabledForError(): def isEnabledForError() -> bool:
return _logger.isEnabledFor(logging.ERROR) return _logger.isEnabledFor(logging.ERROR)
def isEnabledForDebug(): def isEnabledForDebug() -> bool:
return _logger.isEnabledFor(logging.DEBUG) return _logger.isEnabledFor(logging.DEBUG)
def isEnabledForTrace(): def isEnabledForTrace() -> bool:
return _traceEnabled return _traceEnabled

View file

@ -10,7 +10,7 @@ from ._utils import *
_socket.py _socket.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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: class sock_opt:
def __init__(self, sockopt, sslopt): def __init__(self, sockopt: list, sslopt: dict) -> None:
if sockopt is None: if sockopt is None:
sockopt = [] sockopt = []
if sslopt is None: if sslopt is None:
@ -53,7 +53,7 @@ class sock_opt:
self.timeout = None self.timeout = None
def setdefaulttimeout(timeout): def setdefaulttimeout(timeout: int or float) -> None:
""" """
Set the global timeout setting to connect. Set the global timeout setting to connect.
@ -66,7 +66,7 @@ def setdefaulttimeout(timeout):
_default_timeout = timeout _default_timeout = timeout
def getdefaulttimeout(): def getdefaulttimeout() -> int or float:
""" """
Get default timeout Get default timeout
@ -78,7 +78,7 @@ def getdefaulttimeout():
return _default_timeout return _default_timeout
def recv(sock, bufsize): def recv(sock: socket.socket, bufsize: int) -> bytes:
if not sock: if not sock:
raise WebSocketConnectionClosedException("socket is already closed.") raise WebSocketConnectionClosedException("socket is already closed.")
@ -125,7 +125,7 @@ def recv(sock, bufsize):
return bytes_ return bytes_
def recv_line(sock): def recv_line(sock: socket.socket) -> bytes:
line = [] line = []
while True: while True:
c = recv(sock, 1) c = recv(sock, 1)
@ -135,7 +135,7 @@ def recv_line(sock):
return b''.join(line) return b''.join(line)
def send(sock, data): def send(sock: socket.socket, data: bytes) -> int:
if isinstance(data, str): if isinstance(data, str):
data = data.encode('utf-8') data = data.encode('utf-8')

View file

@ -2,7 +2,7 @@
_ssl_compat.py _ssl_compat.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -8,7 +8,7 @@ from urllib.parse import unquote, urlparse
_url.py _url.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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"] __all__ = ["parse_url", "get_proxy_info"]
def parse_url(url): def parse_url(url: str) -> tuple:
""" """
parse url and the result is tuple of parse url and the result is tuple of
(hostname, port, resource path and the flag of secure mode) (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"] DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
def _is_ip_address(addr): def _is_ip_address(addr: str) -> bool:
try: try:
socket.inet_aton(addr) socket.inet_aton(addr)
except socket.error: except socket.error:
@ -84,7 +84,7 @@ def _is_ip_address(addr):
return True return True
def _is_subnet_address(hostname): def _is_subnet_address(hostname: str) -> bool:
try: try:
addr, netmask = hostname.split("/") addr, netmask = hostname.split("/")
return _is_ip_address(addr) and 0 <= int(netmask) < 32 return _is_ip_address(addr) and 0 <= int(netmask) < 32
@ -92,7 +92,7 @@ def _is_subnet_address(hostname):
return False 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] ipaddr = struct.unpack('!I', socket.inet_aton(ip))[0]
netaddr, netmask = net.split('/') netaddr, netmask = net.split('/')
netaddr = struct.unpack('!I', socket.inet_aton(netaddr))[0] netaddr = struct.unpack('!I', socket.inet_aton(netaddr))[0]
@ -101,7 +101,7 @@ def _is_address_in_network(ip, net):
return ipaddr & netmask == netaddr 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: if not no_proxy:
v = os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(" ", "") v = os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(" ", "")
if v: if v:
@ -122,8 +122,8 @@ def _is_no_proxy_host(hostname, no_proxy):
def get_proxy_info( def get_proxy_info(
hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None, hostname: str, is_secure: bool, proxy_host: str = None, proxy_port: int = 0, proxy_auth: tuple = None,
no_proxy=None, proxy_type='http'): no_proxy: list = None, proxy_type: str = 'http') -> tuple:
""" """
Try to retrieve proxy host and port from environment Try to retrieve proxy host and port from environment
if not provided in options. if not provided in options.
@ -137,14 +137,14 @@ def get_proxy_info(
Websocket server name. Websocket server name.
is_secure: bool is_secure: bool
Is the connection secure? (wss) looks for "https_proxy" in env Is the connection secure? (wss) looks for "https_proxy" in env
before falling back to "http_proxy" instead of "http_proxy"
proxy_host: str proxy_host: str
http proxy host name. http proxy host name.
http_proxy_port: str or int proxy_port: str or int
http proxy port. http proxy port.
http_no_proxy: list no_proxy: list
Whitelisted host names that don't use the proxy. 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. HTTP proxy auth information. Tuple of username and password. Default is None.
proxy_type: str proxy_type: str
Specify the proxy protocol (http, socks4, socks4a, socks5, socks5h). Default is "http". Specify the proxy protocol (http, socks4, socks4a, socks5, socks5h). Default is "http".
@ -158,12 +158,8 @@ def get_proxy_info(
auth = proxy_auth auth = proxy_auth
return proxy_host, port, auth return proxy_host, port, auth
env_keys = ["http_proxy"] env_key = "https_proxy" if is_secure else "http_proxy"
if is_secure: value = os.environ.get(env_key, os.environ.get(env_key.upper(), "")).replace(" ", "")
env_keys.insert(0, "https_proxy")
for key in env_keys:
value = os.environ.get(key, os.environ.get(key.upper(), "")).replace(" ", "")
if value: if value:
proxy = urlparse(value) proxy = urlparse(value)
auth = (unquote(proxy.username), unquote(proxy.password)) if proxy.username else None auth = (unquote(proxy.username), unquote(proxy.password)) if proxy.username else None

View file

@ -2,7 +2,7 @@
_url.py _url.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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: class NoLock:
def __enter__(self): def __enter__(self) -> None:
pass pass
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback) -> None:
pass pass
@ -33,7 +33,7 @@ try:
# strings. # strings.
from wsaccel.utf8validator import Utf8Validator from wsaccel.utf8validator import Utf8Validator
def _validate_utf8(utfbytes): def _validate_utf8(utfbytes: bytes) -> bool:
return Utf8Validator().validate(utfbytes)[0] return Utf8Validator().validate(utfbytes)[0]
except ImportError: 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,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, ] 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] tp = _UTF8D[ch]
codep = (ch & 0x3f) | (codep << 6) if ( codep = (ch & 0x3f) | (codep << 6) if (
@ -72,7 +72,7 @@ except ImportError:
return state, codep return state, codep
def _validate_utf8(utfbytes): def _validate_utf8(utfbytes: str or bytes) -> bool:
state = _UTF8_ACCEPT state = _UTF8_ACCEPT
codep = 0 codep = 0
for i in utfbytes: for i in utfbytes:
@ -83,7 +83,7 @@ except ImportError:
return True return True
def validate_utf8(utfbytes): def validate_utf8(utfbytes: str or bytes) -> bool:
""" """
validate utf8 byte string. validate utf8 byte string.
utfbytes: utf byte string to check. utfbytes: utf byte string to check.
@ -92,13 +92,13 @@ def validate_utf8(utfbytes):
return _validate_utf8(utfbytes) return _validate_utf8(utfbytes)
def extract_err_message(exception): def extract_err_message(exception: Exception) -> str or None:
if exception.args: if exception.args:
return exception.args[0] return exception.args[0]
else: else:
return None return None
def extract_error_code(exception): def extract_error_code(exception: Exception) -> int or None:
if exception.args and len(exception.args) > 1: if exception.args and len(exception.args) > 1:
return exception.args[0] if isinstance(exception.args[0], int) else None return exception.args[0] if isinstance(exception.args[0], int) else None

View file

@ -4,7 +4,7 @@
wsdump.py wsdump.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -37,7 +37,7 @@ except ImportError:
pass pass
def get_encoding(): def get_encoding() -> str:
encoding = getattr(sys.stdin, "encoding", "") encoding = getattr(sys.stdin, "encoding", "")
if not encoding: if not encoding:
return "utf-8" return "utf-8"
@ -51,7 +51,7 @@ ENCODING = get_encoding()
class VAction(argparse.Action): 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: if values is None:
values = "1" values = "1"
try: try:
@ -61,7 +61,7 @@ class VAction(argparse.Action):
setattr(args, self.dest, values) setattr(args, self.dest, values)
def parse_args(): def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool") parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool")
parser.add_argument("url", metavar="ws_url", parser.add_argument("url", metavar="ws_url",
help="websocket url. ex. ws://echo.websocket.events/") help="websocket url. ex. ws://echo.websocket.events/")
@ -93,7 +93,7 @@ def parse_args():
class RawInput: class RawInput:
def raw_input(self, prompt): def raw_input(self, prompt: str = "") -> str:
line = input(prompt) line = input(prompt)
if ENCODING and ENCODING != "utf-8" and not isinstance(line, str): if ENCODING and ENCODING != "utf-8" and not isinstance(line, str):
@ -106,29 +106,29 @@ class RawInput:
class InteractiveConsole(RawInput, code.InteractiveConsole): 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("\033[2K\033[E")
# sys.stdout.write("\n") # sys.stdout.write("\n")
sys.stdout.write("\033[34m< " + data + "\033[39m") sys.stdout.write("\033[34m< " + data + "\033[39m")
sys.stdout.write("\n> ") sys.stdout.write("\n> ")
sys.stdout.flush() sys.stdout.flush()
def read(self): def read(self) -> str:
return self.raw_input("> ") return self.raw_input("> ")
class NonInteractive(RawInput): class NonInteractive(RawInput):
def write(self, data): def write(self, data: str) -> None:
sys.stdout.write(data) sys.stdout.write(data)
sys.stdout.write("\n") sys.stdout.write("\n")
sys.stdout.flush() sys.stdout.flush()
def read(self): def read(self) -> str:
return self.raw_input("") return self.raw_input("")
def main(): def main() -> None:
start_time = time.time() start_time = time.time()
args = parse_args() args = parse_args()
if args.verbose > 1: if args.verbose > 1:
@ -154,25 +154,25 @@ def main():
console = InteractiveConsole() console = InteractiveConsole()
print("Press Ctrl+C to quit") print("Press Ctrl+C to quit")
def recv(): def recv() -> tuple:
try: try:
frame = ws.recv_frame() frame = ws.recv_frame()
except websocket.WebSocketException: except websocket.WebSocketException:
return websocket.ABNF.OPCODE_CLOSE, None return websocket.ABNF.OPCODE_CLOSE, ""
if not frame: 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: elif frame.opcode in OPCODE_DATA:
return frame.opcode, frame.data return frame.opcode, frame.data
elif frame.opcode == websocket.ABNF.OPCODE_CLOSE: elif frame.opcode == websocket.ABNF.OPCODE_CLOSE:
ws.send_close() ws.send_close()
return frame.opcode, None return frame.opcode, ""
elif frame.opcode == websocket.ABNF.OPCODE_PING: elif frame.opcode == websocket.ABNF.OPCODE_PING:
ws.pong(frame.data) ws.pong(frame.data)
return frame.opcode, frame.data return frame.opcode, frame.data
return frame.opcode, frame.data return frame.opcode, frame.data
def recv_ws(): def recv_ws() -> None:
while True: while True:
opcode, data = recv() opcode, data = recv()
msg = None msg = None
@ -193,7 +193,7 @@ def main():
data = repr(data) data = repr(data)
if args.verbose: 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: else:
msg = data msg = data

View file

@ -6,7 +6,7 @@ import asyncio
import websockets import websockets
import os 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): async def echo(websocket, path):

View file

@ -8,7 +8,7 @@ import unittest
test_abnf.py test_abnf.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -11,7 +11,7 @@ import unittest
test_app.py test_app.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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() teardown = app.run_forever()
self.assertEqual(teardown, False) 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") @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
def testSockMaskKey(self): def testSockMaskKey(self):
""" A WebSocketApp should forward the received mask_key function down """ 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) app.run_forever(ping_interval=2, ping_timeout=1, reconnect=3)
self.assertEqual(pong_count, 2) self.assertEqual(pong_count, 2)
self.assertIsInstance(exc, ValueError) self.assertIsInstance(exc, ws.WebSocketTimeoutException)
self.assertEqual(str(exc), "Invalid file object: None") self.assertEqual(str(exc), "ping/pong timed out")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -5,7 +5,7 @@ from websocket._cookiejar import SimpleCookieJar
test_cookiejar.py test_cookiejar.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -13,7 +13,7 @@ import socket
test_http.py test_http.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -8,7 +8,7 @@ from websocket._url import get_proxy_info, parse_url, _is_address_in_network, _i
test_url.py test_url.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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/" 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", 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/" os.environ["http_proxy"] = "http://a:b@localhost/"
self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, ("a", "b"))) self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, ("a", "b")))
os.environ["http_proxy"] = "http://a:b@localhost:3128/" os.environ["http_proxy"] = "http://a:b@localhost:3128/"

View file

@ -15,7 +15,7 @@ from base64 import decodebytes as base64decode
test_websocket.py test_websocket.py
websocket - WebSocket client library for Python websocket - WebSocket client library for Python
Copyright 2022 engn33r Copyright 2023 engn33r
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -47,7 +47,7 @@ tzdata==2023.3
tzlocal==4.2 tzlocal==4.2
urllib3==2.0.4 urllib3==2.0.4
webencodings==0.5.1 webencodings==0.5.1
websocket-client==1.5.1 websocket-client==1.6.2
xmltodict==0.13.0 xmltodict==0.13.0
zipp==3.15.0 zipp==3.15.0