From aa127ecbdaa36fc19ce7617f80d4dc66d30d8b0d Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 14 Oct 2021 22:36:24 -0700 Subject: [PATCH] Update websocket-client-1.2.1 --- lib/websocket/__init__.py | 27 +- lib/websocket/_abnf.py | 166 ++++----- lib/websocket/_app.py | 308 +++++++++------- lib/websocket/_cookiejar.py | 45 ++- lib/websocket/_core.py | 367 +++++++++++-------- lib/websocket/_exceptions.py | 40 +-- lib/websocket/_handshake.py | 73 ++-- lib/websocket/_http.py | 167 ++++----- lib/websocket/_logging.py | 40 ++- lib/websocket/_socket.py | 68 ++-- lib/websocket/_ssl_compat.py | 40 +-- lib/websocket/_url.py | 100 +++--- lib/websocket/_utils.py | 29 +- lib/websocket/tests/data/header03.txt | 7 + lib/websocket/tests/echo-server.py | 18 + lib/websocket/tests/test_abnf.py | 92 +++++ lib/websocket/tests/test_app.py | 178 ++++++++++ lib/websocket/tests/test_cookiejar.py | 68 ++-- lib/websocket/tests/test_http.py | 177 ++++++++++ lib/websocket/tests/test_url.py | 303 ++++++++++++++++ lib/websocket/tests/test_websocket.py | 488 ++++++++------------------ 21 files changed, 1756 insertions(+), 1045 deletions(-) create mode 100644 lib/websocket/tests/data/header03.txt create mode 100644 lib/websocket/tests/echo-server.py create mode 100644 lib/websocket/tests/test_abnf.py create mode 100644 lib/websocket/tests/test_app.py create mode 100644 lib/websocket/tests/test_http.py create mode 100644 lib/websocket/tests/test_url.py diff --git a/lib/websocket/__init__.py b/lib/websocket/__init__.py index 605f76cd..a9fa4634 100644 --- a/lib/websocket/__init__.py +++ b/lib/websocket/__init__.py @@ -1,23 +1,20 @@ """ +__init__.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ from ._abnf import * from ._app import WebSocketApp @@ -26,4 +23,4 @@ from ._exceptions import * from ._logging import * from ._socket import * -__version__ = "0.57.0" +__version__ = "1.2.1" diff --git a/lib/websocket/_abnf.py b/lib/websocket/_abnf.py index a0000fa1..6a4d4907 100644 --- a/lib/websocket/_abnf.py +++ b/lib/websocket/_abnf.py @@ -1,59 +1,53 @@ """ + +""" + +""" +_abnf.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ import array import os import struct - -import six +import sys from ._exceptions import * from ._utils import validate_utf8 from threading import Lock try: - if six.PY3: - import numpy - else: - numpy = None -except ImportError: - numpy = None + # If wsaccel is available, use compiled routines to mask data. + # wsaccel only provides around a 10% speed boost compared + # to the websocket-client _mask() implementation. + # Note that wsaccel is unmaintained. + from wsaccel.xormask import XorMaskerSimple -try: - # If wsaccel is available we use compiled routines to mask data. - if not numpy: - from wsaccel.xormask import XorMaskerSimple - - def _mask(_m, _d): - return XorMaskerSimple(_m).process(_d) -except ImportError: - # wsaccel is not available, we rely on python implementations. def _mask(_m, _d): - for i in range(len(_d)): - _d[i] ^= _m[i % 4] + return XorMaskerSimple(_m).process(_d) - if six.PY3: - return _d.tobytes() - else: - return _d.tostring() +except ImportError: + # wsaccel is not available, use websocket-client _mask() + native_byteorder = sys.byteorder + + def _mask(mask_value, data_value): + 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) + return (data_value ^ mask_value).to_bytes(datalen, native_byteorder) __all__ = [ @@ -105,7 +99,7 @@ VALID_CLOSE_STATUS = ( class ABNF(object): """ ABNF frame class. - see http://tools.ietf.org/html/rfc5234 + See http://tools.ietf.org/html/rfc5234 and http://tools.ietf.org/html/rfc6455#section-5.2 """ @@ -139,8 +133,7 @@ class ABNF(object): def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, opcode=OPCODE_TEXT, mask=1, data=""): """ - Constructor for ABNF. - please check RFC for arguments. + Constructor for ABNF. Please check RFC for arguments. """ self.fin = fin self.rsv1 = rsv1 @@ -155,7 +148,10 @@ class ABNF(object): def validate(self, skip_utf8_validation=False): """ - validate the ABNF frame. + Validate the ABNF frame. + + Parameters + ---------- skip_utf8_validation: skip utf8 validation. """ if self.rsv1 or self.rsv2 or self.rsv3: @@ -176,8 +172,7 @@ class ABNF(object): if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]): raise WebSocketProtocolException("Invalid close frame.") - code = 256 * \ - six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2]) + code = 256 * self.data[0] + self.data[1] if not self._is_valid_close_status(code): raise WebSocketProtocolException("Invalid close opcode.") @@ -193,24 +188,27 @@ class ABNF(object): @staticmethod def create_frame(data, opcode, fin=1): """ - create frame to send text, binary and other data. + Create frame to send text, binary and other data. - data: data to send. This is string value(byte array). - if opcode is OPCODE_TEXT and this value is unicode, + Parameters + ---------- + data: + 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: fin flag. if set to 0, create continue fragmentation. + opcode: + operation code. please see OPCODE_XXX. + fin: + fin flag. if set to 0, create continue fragmentation. """ - if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type): + if opcode == ABNF.OPCODE_TEXT and isinstance(data, str): data = data.encode("utf-8") # mask must be set if send data from client return ABNF(fin, 0, 0, 0, opcode, 1, data) def format(self): """ - format this object to string(byte array) to send data to server. + Format this object to string(byte array) to send data to server. """ if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): raise ValueError("not 0 or 1") @@ -220,19 +218,16 @@ class ABNF(object): if length >= ABNF.LENGTH_63: raise ValueError("data is too long") - frame_header = chr(self.fin << 7 - | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 - | self.opcode) + frame_header = chr(self.fin << 7 | + self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 | + self.opcode).encode('latin-1') if length < ABNF.LENGTH_7: - frame_header += chr(self.mask << 7 | length) - frame_header = six.b(frame_header) + frame_header += chr(self.mask << 7 | length).encode('latin-1') elif length < ABNF.LENGTH_16: - frame_header += chr(self.mask << 7 | 0x7e) - frame_header = six.b(frame_header) + frame_header += chr(self.mask << 7 | 0x7e).encode('latin-1') frame_header += struct.pack("!H", length) else: - frame_header += chr(self.mask << 7 | 0x7f) - frame_header = six.b(frame_header) + frame_header += chr(self.mask << 7 | 0x7f).encode('latin-1') frame_header += struct.pack("!Q", length) if not self.mask: @@ -244,7 +239,7 @@ class ABNF(object): def _get_masked(self, mask_key): s = ABNF.mask(mask_key, self.data) - if isinstance(mask_key, six.text_type): + if isinstance(mask_key, str): mask_key = mask_key.encode('utf-8') return mask_key + s @@ -252,36 +247,25 @@ class ABNF(object): @staticmethod def mask(mask_key, data): """ - mask or unmask data. Just do xor for each byte + Mask or unmask data. Just do xor for each byte - mask_key: 4 byte string(byte). - - data: data to mask/unmask. + Parameters + ---------- + mask_key: + 4 byte string. + data: + data to mask/unmask. """ if data is None: data = "" - if isinstance(mask_key, six.text_type): - mask_key = six.b(mask_key) + if isinstance(mask_key, str): + mask_key = mask_key.encode('latin-1') - if isinstance(data, six.text_type): - data = six.b(data) + if isinstance(data, str): + data = data.encode('latin-1') - if numpy: - origlen = len(data) - _mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0] - - # We need data to be a multiple of four... - data += bytes(" " * (4 - (len(data) % 4)), "us-ascii") - a = numpy.frombuffer(data, dtype="uint32") - masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32") - if len(data) > origlen: - return masked.tobytes()[:origlen] - return masked.tobytes() - else: - _m = array.array("B", mask_key) - _d = array.array("B", data) - return _mask(_m, _d) + return _mask(array.array("B", mask_key), array.array("B", data)) class frame_buffer(object): @@ -308,20 +292,12 @@ class frame_buffer(object): def recv_header(self): header = self.recv_strict(2) b1 = header[0] - - if six.PY2: - b1 = ord(b1) - fin = b1 >> 7 & 1 rsv1 = b1 >> 6 & 1 rsv2 = b1 >> 5 & 1 rsv3 = b1 >> 4 & 1 opcode = b1 & 0xf b2 = header[1] - - if six.PY2: - b2 = ord(b2) - has_mask = b2 >> 7 & 1 length_bits = b2 & 0x7f @@ -385,7 +361,7 @@ class frame_buffer(object): return frame def recv_strict(self, bufsize): - shortage = bufsize - sum(len(x) for x in self.recv_buffer) + shortage = bufsize - sum(map(len, self.recv_buffer)) while shortage > 0: # Limit buffer size that we pass to socket.recv() to avoid # fragmenting the heap -- the number of bytes recv() actually @@ -397,7 +373,7 @@ class frame_buffer(object): self.recv_buffer.append(bytes_) shortage -= len(bytes_) - unified = six.b("").join(self.recv_buffer) + unified = bytes("", 'utf-8').join(self.recv_buffer) if shortage == 0: self.recv_buffer = [] diff --git a/lib/websocket/_app.py b/lib/websocket/_app.py index e4e9f99c..61925bad 100644 --- a/lib/websocket/_app.py +++ b/lib/websocket/_app.py @@ -1,37 +1,30 @@ """ + +""" + +""" +_app.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ - -""" -WebSocketApp provides higher level APIs. -""" -import inspect -import select +import selectors import sys import threading import time import traceback - -import six - from ._abnf import ABNF from ._core import WebSocket, getdefaulttimeout from ._exceptions import * @@ -40,21 +33,32 @@ from . import _logging __all__ = ["WebSocketApp"] + class Dispatcher: + """ + Dispatcher + """ def __init__(self, app, ping_timeout): self.app = app self.ping_timeout = ping_timeout def read(self, sock, read_callback, check_callback): while self.app.keep_running: - r, w, e = select.select( - (self.app.sock.sock, ), (), (), self.ping_timeout) + 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() + sel.close() + class SSLDispatcher: + """ + SSLDispatcher + """ def __init__(self, app, ping_timeout): self.app = app self.ping_timeout = ping_timeout @@ -72,14 +76,19 @@ class SSLDispatcher: if sock.pending(): return [sock,] - r, w, e = select.select((sock, ), (), (), self.ping_timeout) - return r + 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] class WebSocketApp(object): """ - 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, @@ -90,39 +99,60 @@ class WebSocketApp(object): subprotocols=None, on_data=None): """ - url: websocket url. - header: custom header for websocket handshake. - on_open: callable object which is called at opening websocket. - this function has one argument. The argument is this class object. - on_message: callable object which is called when received data. - on_message has 2 arguments. - The 1st argument is this class object. - The 2nd argument is utf-8 string which we get from the server. - on_error: callable object which is called when we get error. - on_error has 2 arguments. - The 1st argument is this class object. - The 2nd argument is exception object. - on_close: callable object which is called when closed the connection. - this function has one argument. The argument is this class object. - on_cont_message: callback object which is called when receive continued - frame data. - on_cont_message has 3 arguments. - The 1st argument is this class object. - The 2nd argument is utf-8 string which we get from the server. - The 3rd argument is continue flag. if 0, the data continue - to next frame data - on_data: callback object which is called when a message received. - This is called before on_message or on_cont_message, - and then on_message or on_cont_message is called. - on_data has 4 argument. - The 1st argument is this class object. - The 2nd argument is utf-8 string which we get from the server. - The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came. - The 4th argument is continue flag. if 0, the data continue - keep_running: this parameter is obsolete and ignored. - get_mask_key: a callable to produce new mask keys, - see the WebSocket.set_mask_key's docstring for more information - subprotocols: array of available sub protocols. default is None. + WebSocketApp initialization + + Parameters + ---------- + url: str + Websocket url. + header: list or dict + Custom header for websocket handshake. + on_open: function + Callback object which is called at opening websocket. + on_open has one argument. + The 1st argument is this class object. + on_message: function + Callback object which is called when received data. + on_message has 2 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 data received from the server. + on_error: function + Callback object which is called when we get error. + on_error has 2 arguments. + The 1st argument is this class object. + The 2nd argument is exception object. + on_close: function + Callback object which is called when connection is closed. + on_close has 3 arguments. + The 1st argument is this class object. + The 2nd argument is close_status_code. + The 3rd argument is close_msg. + on_cont_message: function + Callback object which is called when a continuation + frame is received. + on_cont_message has 3 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is continue flag. if 0, the data continue + to next frame data + on_data: function + Callback object which is called when a message received. + This is called before on_message or on_cont_message, + and then on_message or on_cont_message is called. + on_data has 4 argument. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came. + The 4th argument is continue flag. If 0, the data continue + keep_running: bool + This parameter is obsolete and ignored. + get_mask_key: function + A callable function to get new mask keys, see the + WebSocket.set_mask_key's docstring for more information. + cookie: str + Cookie value. + subprotocols: list + List of available sub protocols. Default is None. """ self.url = url self.header = header if header is not None else [] @@ -145,10 +175,15 @@ class WebSocketApp(object): def send(self, data, opcode=ABNF.OPCODE_TEXT): """ - send message. - data: message to send. If you set opcode to OPCODE_TEXT, - data must be utf-8 string or unicode. - opcode: operation code of data. default is OPCODE_TEXT. + send message + + Parameters + ---------- + data: str + Message to send. If you set opcode to OPCODE_TEXT, + data must be utf-8 string or unicode. + opcode: int + Operation code of data. Default is OPCODE_TEXT. """ if not self.sock or self.sock.send(data, opcode) == 0: @@ -157,58 +192,79 @@ class WebSocketApp(object): def close(self, **kwargs): """ - close websocket connection. + Close websocket connection. """ self.keep_running = False if self.sock: self.sock.close(**kwargs) self.sock = None - def _send_ping(self, interval, event): + def _send_ping(self, interval, event, payload): while not event.wait(interval): self.last_ping_tm = time.time() if self.sock: try: - self.sock.ping() + self.sock.ping(payload) except Exception as ex: _logging.warning("send_ping routine terminated: {}".format(ex)) break 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, skip_utf8_validation=False, host=None, origin=None, dispatcher=None, suppress_origin=False, proxy_type=None): """ - run event loop for WebSocket framework. - This loop is infinite loop and is alive during websocket is available. - sockopt: values for socket.setsockopt. + Run event loop for WebSocket framework. + + This loop is an infinite loop and is alive while websocket is available. + + Parameters + ---------- + sockopt: tuple + Values for socket.setsockopt. sockopt must be tuple and each element is argument of sock.setsockopt. - sslopt: ssl socket optional dict. - ping_interval: automatically send "ping" command - every specified period(second) - if set to 0, not send automatically. - ping_timeout: timeout(second) if the pong message is not received. - http_proxy_host: http proxy host name. - http_proxy_port: http proxy port. If not set, set to 80. - http_no_proxy: host names, which doesn't use proxy. - skip_utf8_validation: skip utf8 validation. - host: update host header. - origin: update origin header. - dispatcher: customize reading data from socket. - suppress_origin: suppress outputting origin header. + sslopt: dict + Optional dict object for ssl socket option. + ping_interval: int or float + Automatically send "ping" command + every specified period (in seconds). + If set to 0, no ping is sent periodically. + ping_timeout: int or float + Timeout (in seconds) if the pong message is not received. + ping_payload: str + Payload message to send with each ping. + http_proxy_host: str + HTTP proxy host name. + http_proxy_port: int or str + HTTP proxy port. If not set, set to 80. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + skip_utf8_validation: bool + skip utf8 validation. + host: str + update host header. + origin: str + update origin header. + dispatcher: Dispatcher object + customize reading data from socket. + suppress_origin: bool + suppress outputting origin header. Returns ------- - False if caught KeyboardInterrupt - True if other exception was raised during a loop + teardown: bool + False if caught KeyboardInterrupt, True if other exception was raised during a loop """ if ping_timeout is not None and ping_timeout <= 0: - ping_timeout = None + raise WebSocketException("Ensure ping_timeout > 0") + if ping_interval is not None and ping_interval < 0: + raise WebSocketException("Ensure ping_interval >= 0") if ping_timeout and ping_interval and ping_interval <= ping_timeout: raise WebSocketException("Ensure ping_interval > ping_timeout") if not sockopt: @@ -225,26 +281,33 @@ class WebSocketApp(object): def teardown(close_frame=None): """ Tears down the connection. - If close_frame is set, we will invoke the on_close handler with the - statusCode and reason from there. + + Parameters + ---------- + close_frame: ABNF frame + If close_frame is set, the on_close handler is invoked + with the statusCode and reason from the provided frame. """ - if thread and thread.isAlive(): + + if thread and thread.is_alive(): event.set() thread.join() self.keep_running = False if self.sock: self.sock.close() - close_args = self._get_close_args( - close_frame.data if close_frame else None) - self._callback(self.on_close, *close_args) + close_status_code, close_reason = self._get_close_args( + close_frame if close_frame else None) self.sock = None + # Finally call the callback AFTER all teardown is complete + self._callback(self.on_close, close_status_code, close_reason) + try: self.sock = WebSocket( self.get_mask_key, sockopt=sockopt, sslopt=sslopt, fire_cont_frame=self.on_cont_message is not None, skip_utf8_validation=skip_utf8_validation, - enable_multithread=True if ping_interval else False) + enable_multithread=True) self.sock.settimeout(getdefaulttimeout()) self.sock.connect( self.url, header=self.header, cookie=self.cookie, @@ -261,8 +324,8 @@ class WebSocketApp(object): if ping_interval: event = threading.Event() thread = threading.Thread( - target=self._send_ping, args=(ping_interval, event)) - thread.setDaemon(True) + target=self._send_ping, args=(ping_interval, event, ping_payload)) + thread.daemon = True thread.start() def read(): @@ -284,7 +347,7 @@ class WebSocketApp(object): frame.data, frame.fin) else: data = frame.data - if six.PY3 and op_code == ABNF.OPCODE_TEXT: + if op_code == ABNF.OPCODE_TEXT: data = data.decode("utf-8") self._callback(self.on_data, data, frame.opcode, True) self._callback(self.on_message, data) @@ -297,9 +360,9 @@ class WebSocketApp(object): has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0 has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout - if (self.last_ping_tm - and has_timeout_expired - and (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)): + if (self.last_ping_tm and + has_timeout_expired and + (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)): raise WebSocketTimeoutException("ping/pong timed out") return True @@ -319,34 +382,31 @@ class WebSocketApp(object): return Dispatcher(self, timeout) - def _get_close_args(self, data): - """ this functions extracts the code, reason from the close body - if they exists, and if the self.on_close except three arguments """ - # if the on_close callback is "old", just return empty list - if sys.version_info < (3, 0): - if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3: - return [] + def _get_close_args(self, close_frame): + """ + _get_close_args extracts the close code and reason from the close body + if it exists (RFC6455 says WebSocket Connection Close Code is optional) + """ + # Need to catch the case where close_frame is None + # Otherwise the following if statement causes an error + if not self.on_close or not close_frame: + return [None, None] + + # Extract close frame status code + if close_frame.data and len(close_frame.data) >= 2: + close_status_code = 256 * close_frame.data[0] + close_frame.data[1] + reason = close_frame.data[2:].decode('utf-8') + return [close_status_code, reason] else: - if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3: - return [] - - if data and len(data) >= 2: - code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2]) - reason = data[2:].decode('utf-8') - return [code, reason] - - return [None, None] + # Most likely reached this because len(close_frame_data.data) < 2 + return [None, None] def _callback(self, callback, *args): if callback: try: - if inspect.ismethod(callback): - callback(*args) - else: - callback(self, *args) + callback(self, *args) except Exception as e: _logging.error("error from callback {}: {}".format(callback, e)) - if _logging.isEnabledForDebug(): - _, _, tb = sys.exc_info() - traceback.print_tb(tb) + if self.on_error: + self.on_error(self, e) diff --git a/lib/websocket/_cookiejar.py b/lib/websocket/_cookiejar.py index 3efeb0fd..dcf5031a 100644 --- a/lib/websocket/_cookiejar.py +++ b/lib/websocket/_cookiejar.py @@ -1,7 +1,26 @@ -try: - import Cookie -except: - import http.cookies as Cookie +""" + +""" + +""" +_cookiejar.py +websocket - WebSocket client library for Python + +Copyright 2021 engn33r + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import http.cookies class SimpleCookieJar(object): @@ -10,26 +29,20 @@ class SimpleCookieJar(object): def add(self, set_cookie): if set_cookie: - try: - simpleCookie = Cookie.SimpleCookie(set_cookie) - except: - simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + simpleCookie = http.cookies.SimpleCookie(set_cookie) for k, v in simpleCookie.items(): domain = v.get("domain") if domain: if not domain.startswith("."): domain = "." + domain - cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie() + cookie = self.jar.get(domain) if self.jar.get(domain) else http.cookies.SimpleCookie() cookie.update(simpleCookie) self.jar[domain.lower()] = cookie def set(self, set_cookie): if set_cookie: - try: - simpleCookie = Cookie.SimpleCookie(set_cookie) - except: - simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + simpleCookie = http.cookies.SimpleCookie(set_cookie) for k, v in simpleCookie.items(): domain = v.get("domain") @@ -48,5 +61,7 @@ class SimpleCookieJar(object): if host.endswith(domain) or host == domain[1:]: cookies.append(self.jar.get(domain)) - return "; ".join(filter(None, ["%s=%s" % (k, v.value) for cookie in filter(None, sorted(cookies)) for k, v in - sorted(cookie.items())])) + return "; ".join(filter( + None, sorted( + ["%s=%s" % (k, v.value) for cookie in filter(None, cookies) for k, v in cookie.items()] + ))) diff --git a/lib/websocket/_core.py b/lib/websocket/_core.py index 418aafc4..f92f8a60 100644 --- a/lib/websocket/_core.py +++ b/lib/websocket/_core.py @@ -1,33 +1,32 @@ """ +_core.py +==================================== +WebSocket Python client +""" + +""" +_core.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ -from __future__ import print_function - import socket import struct import threading import time -import six - # websocket modules from ._abnf import * from ._exceptions import * @@ -40,21 +39,12 @@ from ._utils import * __all__ = ['WebSocket', 'create_connection'] -""" -websocket python client. -========================= - -This version support only hybi-13. -Please see http://tools.ietf.org/html/rfc6455 for protocol. -""" - class WebSocket(object): """ Low level WebSocket interface. - This class is based on - The WebSocket protocol draft-hixie-thewebsocketprotocol-76 - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 + + This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 `_ We can connect to the websocket server and send/receive data. The following example is an echo client. @@ -67,21 +57,34 @@ class WebSocket(object): 'Hello, Server' >>> ws.close() - get_mask_key: a callable to produce new mask keys, see the set_mask_key - function's docstring for more details - sockopt: values for socket.setsockopt. + Parameters + ---------- + get_mask_key: func + A callable function to get new mask keys, see the + WebSocket.set_mask_key's docstring for more information. + sockopt: tuple + Values for socket.setsockopt. sockopt must be tuple and each element is argument of sock.setsockopt. - sslopt: dict object for ssl socket option. - fire_cont_frame: fire recv event for each cont frame. default is False - enable_multithread: if set to True, lock send method. - skip_utf8_validation: skip utf8 validation. + sslopt: dict + Optional dict object for ssl socket options. + fire_cont_frame: bool + Fire recv event for each cont frame. Default is False. + enable_multithread: bool + If set to True, lock send method. + skip_utf8_validation: bool + Skip utf8 validation. """ def __init__(self, get_mask_key=None, sockopt=None, sslopt=None, - fire_cont_frame=False, enable_multithread=False, + fire_cont_frame=False, enable_multithread=True, skip_utf8_validation=False, **_): """ Initialize WebSocket object. + + Parameters + ---------- + sslopt: dict + Optional dict object for ssl socket options. """ self.sock_opt = sock_opt(sockopt, sslopt) self.handshake_response = None @@ -119,19 +122,27 @@ class WebSocket(object): def set_mask_key(self, func): """ - set function to create musk key. You can customize mask key generator. + Set function to create mask key. You can customize mask key generator. Mainly, this is for testing purpose. - func: callable object. the func takes 1 argument as integer. - The argument means length of mask key. - This func must return string(byte array), - which length is argument specified. + Parameters + ---------- + func: func + callable object. the func takes 1 argument as integer. + The argument means length of mask key. + This func must return string(byte array), + which length is argument specified. """ self.get_mask_key = func def gettimeout(self): """ - Get the websocket timeout(second). + Get the websocket timeout (in seconds) as an int or float + + Returns + ---------- + timeout: int or float + returns timeout value (in seconds). This value could be either float/integer. """ return self.sock_opt.timeout @@ -139,7 +150,10 @@ class WebSocket(object): """ Set the timeout to the websocket. - timeout: timeout time(second). + Parameters + ---------- + timeout: int or float + timeout time (in seconds). This value could be either float/integer. """ self.sock_opt.timeout = timeout if self.sock: @@ -149,7 +163,7 @@ class WebSocket(object): def getsubprotocol(self): """ - get subprotocol + Get subprotocol """ if self.handshake_response: return self.handshake_response.subprotocol @@ -160,7 +174,7 @@ class WebSocket(object): def getstatus(self): """ - get handshake status + Get handshake status """ if self.handshake_response: return self.handshake_response.status @@ -171,7 +185,7 @@ class WebSocket(object): def getheaders(self): """ - get handshake response header + Get handshake response header """ if self.handshake_response: return self.handshake_response.headers @@ -179,7 +193,10 @@ class WebSocket(object): return None def is_ssl(self): - return isinstance(self.sock, ssl.SSLSocket) + try: + return isinstance(self.sock, ssl.SSLSocket) + except: + return False headers = property(getheaders) @@ -195,29 +212,39 @@ class WebSocket(object): ... header=["User-Agent: MyProgram", ... "x-custom: header"]) - timeout: socket timeout time. This value is integer. - if you set None for this value, - it means "use default_timeout value" - - options: "header" -> custom http header list or dict. - "cookie" -> cookie value. - "origin" -> custom origin url. - "suppress_origin" -> suppress outputting origin header. - "host" -> custom host header string. - "http_proxy_host" - http proxy host name. - "http_proxy_port" - http proxy port. If not set, set to 80. - "http_no_proxy" - host names, which doesn't use proxy. - "http_proxy_auth" - http proxy auth information. - tuple of username and password. - default is None - "redirect_limit" -> number of redirects to follow. - "subprotocols" - array of available sub protocols. - default is None. - "socket" - pre-initialized stream socket. - + Parameters + ---------- + header: list or dict + Custom http header list or dict. + cookie: str + Cookie value. + origin: str + Custom origin url. + connection: str + Custom connection header value. + Default value "Upgrade" set in _handshake.py + suppress_origin: bool + Suppress outputting origin header. + host: str + Custom host header string. + timeout: int or float + Socket timeout time. This value is an integer or float. + If you set None for this value, it means "use default_timeout value" + http_proxy_host: str + HTTP proxy host name. + http_proxy_port: str or int + HTTP proxy port. Default is 80. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + http_proxy_auth: tuple + HTTP proxy auth information. Tuple of username and password. Default is None. + redirect_limit: int + Number of redirects to follow. + subprotocols: list + List of available subprotocols. Default is None. + socket: socket + Pre-initialized stream socket. """ - # FIXME: "subprotocols" are getting lost, not passed down - # FIXME: "header", "cookie", "origin" and "host" too self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout) self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), options.pop('socket', None)) @@ -228,8 +255,8 @@ class WebSocket(object): if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES: url = self.handshake_response.headers['location'] self.sock.close() - self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), - options.pop('socket', None)) + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) self.handshake_response = handshake(self.sock, *addrs, **options) self.connected = True except: @@ -242,11 +269,14 @@ class WebSocket(object): """ Send the data as string. - payload: Payload must be utf-8 string or unicode, - if the opcode is OPCODE_TEXT. - Otherwise, it must be string(byte array) - - opcode: operation code to send. Please see OPCODE_XXX. + Parameters + ---------- + payload: str + Payload must be utf-8 string or unicode, + If the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array). + opcode: int + Operation code (opcode) to send. """ frame = ABNF.create_frame(payload, opcode) @@ -256,8 +286,6 @@ class WebSocket(object): """ Send the data frame. - frame: frame data created by ABNF.create_frame - >>> ws = create_connection("ws://echo.websocket.org/") >>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT) >>> ws.send_frame(frame) @@ -266,14 +294,18 @@ class WebSocket(object): >>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1) >>> ws.send_frame(frame) + Parameters + ---------- + frame: ABNF frame + frame data created by ABNF.create_frame """ if self.get_mask_key: frame.get_mask_key = self.get_mask_key data = frame.format() length = len(data) if (isEnabledForTrace()): - trace("send: " + repr(data)) - + trace("++Sent raw: " + repr(data)) + trace("++Sent decoded: " + frame.__str__()) with self.lock: while data: l = self._send(data) @@ -286,21 +318,27 @@ class WebSocket(object): def ping(self, payload=""): """ - send ping data. + Send ping data. - payload: data payload to send server. + Parameters + ---------- + payload: str + data payload to send server. """ - if isinstance(payload, six.text_type): + if isinstance(payload, str): payload = payload.encode("utf-8") self.send(payload, ABNF.OPCODE_PING) - def pong(self, payload): + def pong(self, payload=""): """ - send pong data. + Send pong data. - payload: data payload to send server. + Parameters + ---------- + payload: str + data payload to send server. """ - if isinstance(payload, six.text_type): + if isinstance(payload, str): payload = payload.encode("utf-8") self.send(payload, ABNF.OPCODE_PONG) @@ -308,11 +346,13 @@ class WebSocket(object): """ Receive string data(byte array) from the server. - return value: string(byte array) value. + Returns + ---------- + data: string (byte array) value. """ with self.readlock: opcode, data = self.recv_data() - if six.PY3 and opcode == ABNF.OPCODE_TEXT: + if opcode == ABNF.OPCODE_TEXT: return data.decode("utf-8") elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY: return data @@ -323,10 +363,16 @@ class WebSocket(object): """ Receive data with operation code. - control_frame: a boolean flag indicating whether to return control frame - data, defaults to False + Parameters + ---------- + control_frame: bool + a boolean flag indicating whether to return control frame + data, defaults to False - return value: tuple of operation code and string(byte array) value. + Returns + ------- + opcode, frame.data: tuple + tuple of operation code and string(byte array) value. """ opcode, frame = self.recv_data_frame(control_frame) return opcode, frame.data @@ -335,13 +381,22 @@ class WebSocket(object): """ Receive data with operation code. - control_frame: a boolean flag indicating whether to return control frame - data, defaults to False + Parameters + ---------- + control_frame: bool + a boolean flag indicating whether to return control frame + data, defaults to False - return value: tuple of operation code and string(byte array) value. + Returns + ------- + frame.opcode, frame: tuple + tuple of operation code and string(byte array) value. """ while True: frame = self.recv_frame() + if (isEnabledForTrace()): + trace("++Rcv raw: " + repr(frame.format())) + trace("++Rcv decoded: " + frame.__str__()) if not frame: # handle error: # 'NoneType' object has no attribute 'opcode' @@ -371,34 +426,42 @@ class WebSocket(object): def recv_frame(self): """ - receive data as frame from server. + Receive data as frame from server. - return value: ABNF frame object. + Returns + ------- + self.frame_buffer.recv_frame(): ABNF frame object """ return self.frame_buffer.recv_frame() - def send_close(self, status=STATUS_NORMAL, reason=six.b("")): + def send_close(self, status=STATUS_NORMAL, reason=bytes('', encoding='utf-8')): """ - send close data to the server. + Send close data to the server. - status: status code to send. see STATUS_XXX. - - reason: the reason to close. This must be string or bytes. + Parameters + ---------- + status: int + Status code to send. See STATUS_XXX. + reason: str or bytes + The reason to close. This must be string or bytes. """ if status < 0 or status >= ABNF.LENGTH_16: raise ValueError("code is invalid range") self.connected = False self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) - def close(self, status=STATUS_NORMAL, reason=six.b(""), timeout=3): + def close(self, status=STATUS_NORMAL, reason=bytes('', encoding='utf-8'), timeout=3): """ Close Websocket object - status: status code to send. see STATUS_XXX. - - reason: the reason to close. This must be string. - - timeout: timeout until receive a close frame. + Parameters + ---------- + status: int + Status code to send. See STATUS_XXX. + reason: bytes + The reason to close. + timeout: int or float + Timeout until receive a close frame. If None, it will wait forever until receive a close frame. """ if self.connected: @@ -407,8 +470,7 @@ class WebSocket(object): try: self.connected = False - self.send(struct.pack('!H', status) + - reason, ABNF.OPCODE_CLOSE) + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) sock_timeout = self.sock.gettimeout() self.sock.settimeout(timeout) start_time = time.time() @@ -419,7 +481,9 @@ class WebSocket(object): continue if isEnabledForError(): recv_status = struct.unpack("!H", frame.data[0:2])[0] - if recv_status != STATUS_NORMAL: + if recv_status >= 3000 and recv_status <= 4999: + debug("close status: " + repr(recv_status)) + elif recv_status != STATUS_NORMAL: error("close status: " + repr(recv_status)) break except: @@ -439,7 +503,9 @@ class WebSocket(object): self.sock.shutdown(socket.SHUT_RDWR) def shutdown(self): - """close socket, immediately.""" + """ + close socket, immediately. + """ if self.sock: self.sock.close() self.sock = None @@ -461,12 +527,12 @@ class WebSocket(object): def create_connection(url, timeout=None, class_=WebSocket, **options): """ - connect to url and return websocket object. + Connect to url and return websocket object. Connect to url and return the WebSocket object. Passing optional timeout parameter will set the timeout on the socket. If no timeout is supplied, - the global default timeout setting returned by getdefauttimeout() is used. + the global default timeout setting returned by getdefaulttimeout() is used. You can customize using 'options'. If you set "header" list object, you can set your own custom header. @@ -474,38 +540,53 @@ def create_connection(url, timeout=None, class_=WebSocket, **options): ... header=["User-Agent: MyProgram", ... "x-custom: header"]) - - timeout: socket timeout time. This value is integer. - if you set None for this value, - it means "use default_timeout value" - - class_: class to instantiate when creating the connection. It has to implement - settimeout and connect. It's __init__ should be compatible with - WebSocket.__init__, i.e. accept all of it's kwargs. - options: "header" -> custom http header list or dict. - "cookie" -> cookie value. - "origin" -> custom origin url. - "suppress_origin" -> suppress outputting origin header. - "host" -> custom host header string. - "http_proxy_host" - http proxy host name. - "http_proxy_port" - http proxy port. If not set, set to 80. - "http_no_proxy" - host names, which doesn't use proxy. - "http_proxy_auth" - http proxy auth information. - tuple of username and password. - default is None - "enable_multithread" -> enable lock for multithread. - "redirect_limit" -> number of redirects to follow. - "sockopt" -> socket options - "sslopt" -> ssl option - "subprotocols" - array of available sub protocols. - default is None. - "skip_utf8_validation" - skip utf8 validation. - "socket" - pre-initialized stream socket. + Parameters + ---------- + class_: class + class to instantiate when creating the connection. It has to implement + settimeout and connect. It's __init__ should be compatible with + WebSocket.__init__, i.e. accept all of it's kwargs. + header: list or dict + custom http header list or dict. + cookie: str + Cookie value. + origin: str + custom origin url. + suppress_origin: bool + suppress outputting origin header. + host: str + custom host header string. + timeout: int or float + socket timeout time. This value could be either float/integer. + If set to None, it uses the default_timeout value. + http_proxy_host: str + HTTP proxy host name. + http_proxy_port: str or int + HTTP proxy port. If not set, set to 80. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + http_proxy_auth: tuple + HTTP proxy auth information. tuple of username and password. Default is None. + enable_multithread: bool + Enable lock for multithread. + redirect_limit: int + Number of redirects to follow. + sockopt: tuple + Values for socket.setsockopt. + sockopt must be a tuple and each element is an argument of sock.setsockopt. + sslopt: dict + Optional dict object for ssl socket options. + subprotocols: list + List of available subprotocols. Default is None. + skip_utf8_validation: bool + Skip utf8 validation. + socket: socket + Pre-initialized stream socket. """ sockopt = options.pop("sockopt", []) sslopt = options.pop("sslopt", {}) fire_cont_frame = options.pop("fire_cont_frame", False) - enable_multithread = options.pop("enable_multithread", False) + enable_multithread = options.pop("enable_multithread", True) skip_utf8_validation = options.pop("skip_utf8_validation", False) websock = class_(sockopt=sockopt, sslopt=sslopt, fire_cont_frame=fire_cont_frame, diff --git a/lib/websocket/_exceptions.py b/lib/websocket/_exceptions.py index 20707902..2d5b0535 100644 --- a/lib/websocket/_exceptions.py +++ b/lib/websocket/_exceptions.py @@ -1,48 +1,44 @@ """ +Define WebSocket exceptions +""" + +""" +_exceptions.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA - -""" - - -""" -define websocket exceptions +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ class WebSocketException(Exception): """ - websocket exception class. + WebSocket exception class. """ pass class WebSocketProtocolException(WebSocketException): """ - If the websocket protocol is invalid, this exception will be raised. + If the WebSocket protocol is invalid, this exception will be raised. """ pass class WebSocketPayloadException(WebSocketException): """ - If the websocket payload is invalid, this exception will be raised. + If the WebSocket payload is invalid, this exception will be raised. """ pass diff --git a/lib/websocket/_handshake.py b/lib/websocket/_handshake.py index 7476a072..da1a8d44 100644 --- a/lib/websocket/_handshake.py +++ b/lib/websocket/_handshake.py @@ -1,57 +1,34 @@ """ +_handshake.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ import hashlib import hmac import os - -import six - +from base64 import encodebytes as base64encode +from http import client as HTTPStatus from ._cookiejar import SimpleCookieJar from ._exceptions import * from ._http import * from ._logging import * from ._socket import * -if hasattr(six, 'PY3') and six.PY3: - from base64 import encodebytes as base64encode -else: - from base64 import encodestring as base64encode - -if hasattr(six, 'PY3') and six.PY3: - if hasattr(six, 'PY34') and six.PY34: - from http import client as HTTPStatus - else: - from http import HTTPStatus -else: - import httplib as HTTPStatus - __all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"] -if hasattr(hmac, "compare_digest"): - compare_digest = hmac.compare_digest -else: - def compare_digest(s1, s2): - return s1 == s2 - # websocket supported version. VERSION = 13 @@ -94,6 +71,7 @@ def _pack_hostname(hostname): return hostname + def _get_handshake_headers(resource, host, port, options): headers = [ "GET %s HTTP/1.1" % resource, @@ -115,19 +93,19 @@ def _get_handshake_headers(resource, host, port, options): headers.append("Origin: http://%s" % hostport) key = _create_sec_websocket_key() - + # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified - if not 'header' in options or 'Sec-WebSocket-Key' not in options['header']: + if 'header' not in options or 'Sec-WebSocket-Key' not in options['header']: key = _create_sec_websocket_key() headers.append("Sec-WebSocket-Key: %s" % key) else: key = options['header']['Sec-WebSocket-Key'] - if not 'header' in options or 'Sec-WebSocket-Version' not in options['header']: + if 'header' not in options or 'Sec-WebSocket-Version' not in options['header']: headers.append("Sec-WebSocket-Version: %s" % VERSION) - if not 'connection' in options or options['connection'] is None: - headers.append('Connection: upgrade') + if 'connection' not in options or options['connection'] is None: + headers.append('Connection: Upgrade') else: headers.append(options['connection']) @@ -178,27 +156,28 @@ def _validate(headers, key, subprotocols): r = headers.get(k, None) if not r: return False, None - r = r.lower() - if v != r: + r = [x.strip().lower() for x in r.split(',')] + if v not in r: return False, None if subprotocols: - subproto = headers.get("sec-websocket-protocol", None).lower() - if not subproto or subproto not in [s.lower() for s in subprotocols]: + subproto = headers.get("sec-websocket-protocol", None) + if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]: error("Invalid subprotocol: " + str(subprotocols)) return False, None + subproto = subproto.lower() result = headers.get("sec-websocket-accept", None) if not result: return False, None result = result.lower() - if isinstance(result, six.text_type): + if isinstance(result, str): result = result.encode('utf-8') value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8') hashed = base64encode(hashlib.sha1(value).digest()).strip().lower() - success = compare_digest(hashed, result) + success = hmac.compare_digest(hashed, result) if success: return True, subproto diff --git a/lib/websocket/_http.py b/lib/websocket/_http.py index a8777de6..9ddf01d0 100644 --- a/lib/websocket/_http.py +++ b/lib/websocket/_http.py @@ -1,109 +1,118 @@ """ +_http.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ import errno import os import socket import sys -import six - from ._exceptions import * from ._logging import * from ._socket import* from ._ssl_compat import * from ._url import * -if six.PY3: - from base64 import encodebytes as base64encode -else: - from base64 import encodestring as base64encode +from base64 import encodebytes as base64encode __all__ = ["proxy_info", "connect", "read_headers"] try: - import socks - ProxyConnectionError = socks.ProxyConnectionError - HAS_PYSOCKS = True + from python_socks.sync import Proxy + from python_socks._errors import * + from python_socks._types import ProxyType + HAVE_PYTHON_SOCKS = True except: - class ProxyConnectionError(BaseException): + HAVE_PYTHON_SOCKS = False + + class ProxyError(Exception): pass - HAS_PYSOCKS = False + + class ProxyTimeoutError(Exception): + pass + + class ProxyConnectionError(Exception): + pass + class proxy_info(object): def __init__(self, **options): - self.type = options.get("proxy_type") or "http" - if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']): - raise ValueError("proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'") - self.host = options.get("http_proxy_host", None) - if self.host: - self.port = options.get("http_proxy_port", 0) + self.proxy_host = options.get("http_proxy_host", None) + if self.proxy_host: + self.proxy_port = options.get("http_proxy_port", 0) self.auth = options.get("http_proxy_auth", None) self.no_proxy = options.get("http_no_proxy", None) + self.proxy_protocol = options.get("proxy_type", "http") + # Note: If timeout not specified, default python-socks timeout is 60 seconds + self.proxy_timeout = options.get("timeout", None) + if self.proxy_protocol not in ['http', 'socks4', 'socks4a', 'socks5', 'socks5h']: + raise ProxyError("Only http, socks4, socks5 proxy protocols are supported") else: - self.port = 0 + self.proxy_port = 0 self.auth = None self.no_proxy = None + self.proxy_protocol = "http" -def _open_proxied_socket(url, options, proxy): +def _start_proxied_socket(url, options, proxy): + if not HAVE_PYTHON_SOCKS: + raise WebSocketException("Python Socks is needed for SOCKS proxying but is not available") + hostname, port, resource, is_secure = parse_url(url) - if not HAS_PYSOCKS: - raise WebSocketException("PySocks module not found.") - - ptype = socks.SOCKS5 - rdns = False - if proxy.type == "socks4": - ptype = socks.SOCKS4 - if proxy.type == "http": - ptype = socks.HTTP - if proxy.type[-1] == "h": + if proxy.proxy_protocol == "socks5": + rdns = False + proxy_type = ProxyType.SOCKS5 + if proxy.proxy_protocol == "socks4": + rdns = False + proxy_type = ProxyType.SOCKS4 + # socks5h and socks4a send DNS through proxy + if proxy.proxy_protocol == "socks5h": rdns = True + proxy_type = ProxyType.SOCKS5 + if proxy.proxy_protocol == "socks4a": + rdns = True + proxy_type = ProxyType.SOCKS4 - sock = socks.create_connection( - (hostname, port), - proxy_type = ptype, - proxy_addr = proxy.host, - proxy_port = proxy.port, - proxy_rdns = rdns, - proxy_username = proxy.auth[0] if proxy.auth else None, - proxy_password = proxy.auth[1] if proxy.auth else None, - timeout = options.timeout, - socket_options = DEFAULT_SOCKET_OPTION + options.sockopt - ) + ws_proxy = Proxy.create( + proxy_type=proxy_type, + host=proxy.proxy_host, + port=int(proxy.proxy_port), + username=proxy.auth[0] if proxy.auth else None, + password=proxy.auth[1] if proxy.auth else None, + rdns=rdns) - if is_secure: - if HAVE_SSL: - sock = _ssl_socket(sock, options.sslopt, hostname) - else: - raise WebSocketException("SSL not available.") + sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout) + + if is_secure and HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + elif is_secure: + raise WebSocketException("SSL not available.") return sock, (hostname, port, resource) def connect(url, options, proxy, socket): - if proxy.host and not socket and not (proxy.type == 'http'): - return _open_proxied_socket(url, options, proxy) + # 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 + if proxy.proxy_host and not socket and not (proxy.proxy_protocol == "http"): + return _start_proxied_socket(url, options, proxy) hostname, port, resource, is_secure = parse_url(url) @@ -137,7 +146,7 @@ def connect(url, options, proxy, socket): def _get_addrinfo_list(hostname, port, is_secure, proxy): phost, pport, pauth = get_proxy_info( - hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy) + hostname, is_secure, proxy.proxy_host, proxy.proxy_port, proxy.auth, proxy.no_proxy) try: # when running on windows 10, getaddrinfo without socktype returns a socktype 0. # This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0` @@ -174,10 +183,6 @@ def _open_socket(addrinfo_list, sockopt, timeout): while not err: try: sock.connect(address) - except ProxyConnectionError as error: - err = WebSocketProxyException(str(error)) - err.remote_ip = str(address[0]) - continue except socket.error as error: error.remote_ip = str(address[0]) try: @@ -190,6 +195,8 @@ def _open_socket(addrinfo_list, sockopt, timeout): err = error continue else: + if sock: + sock.close() raise error else: break @@ -203,12 +210,8 @@ def _open_socket(addrinfo_list, sockopt, timeout): return sock -def _can_use_sni(): - return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2) - - def _wrap_sni_socket(sock, sslopt, hostname, check_hostname): - context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23)) + context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_TLS)) if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: cafile = sslopt.get('ca_certs', None) @@ -250,21 +253,18 @@ def _ssl_socket(sock, user_sslopt, hostname): certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE') if certPath and os.path.isfile(certPath) \ - and user_sslopt.get('ca_certs', None) is None \ - and user_sslopt.get('ca_cert', None) is None: + and user_sslopt.get('ca_certs', None) is None: sslopt['ca_certs'] = certPath elif certPath and os.path.isdir(certPath) \ and user_sslopt.get('ca_cert_path', None) is None: sslopt['ca_cert_path'] = certPath + if sslopt.get('server_hostname', None): + hostname = sslopt['server_hostname'] + check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop( 'check_hostname', True) - - if _can_use_sni(): - sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname) - else: - sslopt.pop('check_hostname', True) - sock = ssl.wrap_socket(sock, **sslopt) + sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname) if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname: match_hostname(sock.getpeercert(), hostname) @@ -274,7 +274,9 @@ def _ssl_socket(sock, user_sslopt, hostname): def _tunnel(sock, host, port, auth): debug("Connecting proxy...") - connect_header = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port) + connect_header = "CONNECT %s:%d HTTP/1.1\r\n" % (host, port) + connect_header += "Host: %s:%d\r\n" % (host, port) + # TODO: support digest auth. if auth and auth[0]: auth_str = auth[0] @@ -321,7 +323,10 @@ def read_headers(sock): kv = line.split(":", 1) if len(kv) == 2: key, value = kv - headers[key.lower()] = value.strip() + if key.lower() == "set-cookie" and headers.get("set-cookie"): + headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip() + else: + headers[key.lower()] = value.strip() else: raise WebSocketException("Invalid header") diff --git a/lib/websocket/_logging.py b/lib/websocket/_logging.py index c9477789..480d43b0 100644 --- a/lib/websocket/_logging.py +++ b/lib/websocket/_logging.py @@ -1,23 +1,24 @@ """ + +""" + +""" +_logging.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ import logging @@ -37,11 +38,14 @@ __all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace", "isEnabledForError", "isEnabledForDebug", "isEnabledForTrace"] -def enableTrace(traceable, handler = logging.StreamHandler()): +def enableTrace(traceable, handler=logging.StreamHandler()): """ - turn on/off the traceability. + Turn on/off the traceability. - traceable: boolean value. if set True, traceability is enabled. + Parameters + ---------- + traceable: bool + If set to True, traceability is enabled. """ global _traceEnabled _traceEnabled = traceable @@ -49,6 +53,7 @@ def enableTrace(traceable, handler = logging.StreamHandler()): _logger.addHandler(handler) _logger.setLevel(logging.DEBUG) + def dump(title, message): if _traceEnabled: _logger.debug("--- " + title + " ---") @@ -80,5 +85,6 @@ def isEnabledForError(): def isEnabledForDebug(): return _logger.isEnabledFor(logging.DEBUG) + def isEnabledForTrace(): return _traceEnabled diff --git a/lib/websocket/_socket.py b/lib/websocket/_socket.py index 7be39138..eb573d4e 100644 --- a/lib/websocket/_socket.py +++ b/lib/websocket/_socket.py @@ -1,31 +1,29 @@ """ + +""" + +""" +_socket.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ import errno -import select +import selectors import socket -import six -import sys - from ._exceptions import * from ._ssl_compat import * from ._utils import * @@ -62,7 +60,10 @@ def setdefaulttimeout(timeout): """ Set the global timeout setting to connect. - timeout: default socket timeout time. This value is second. + Parameters + ---------- + timeout: int or float + default socket timeout time (in seconds) """ global _default_timeout _default_timeout = timeout @@ -70,7 +71,12 @@ def setdefaulttimeout(timeout): def getdefaulttimeout(): """ - Return the global timeout setting(second) to connect. + Get default timeout + + Returns + ---------- + _default_timeout: int or float + Return the global timeout setting (in seconds) to connect. """ return _default_timeout @@ -91,7 +97,12 @@ def recv(sock, bufsize): if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK: raise - r, w, e = select.select((sock, ), (), (), sock.gettimeout()) + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ) + + r = sel.select(sock.gettimeout()) + sel.close() + if r: return sock.recv(bufsize) @@ -112,7 +123,7 @@ def recv(sock, bufsize): if not bytes_: raise WebSocketConnectionClosedException( - "Connection is already closed.") + "Connection to remote host was lost.") return bytes_ @@ -122,13 +133,13 @@ def recv_line(sock): while True: c = recv(sock, 1) line.append(c) - if c == six.b("\n"): + if c == b'\n': break - return six.b("").join(line) + return b''.join(line) def send(sock, data): - if isinstance(data, six.text_type): + if isinstance(data, str): data = data.encode('utf-8') if not sock: @@ -146,7 +157,12 @@ def send(sock, data): if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK: raise - r, w, e = select.select((), (sock, ), (), sock.gettimeout()) + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_WRITE) + + w = sel.select(sock.gettimeout()) + sel.close() + if w: return sock.send(data) diff --git a/lib/websocket/_ssl_compat.py b/lib/websocket/_ssl_compat.py index 96cd173e..9e5460c2 100644 --- a/lib/websocket/_ssl_compat.py +++ b/lib/websocket/_ssl_compat.py @@ -1,23 +1,20 @@ """ +_ssl_compat.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ __all__ = ["HAVE_SSL", "ssl", "SSLError", "SSLWantReadError", "SSLWantWriteError"] @@ -26,20 +23,14 @@ try: from ssl import SSLError from ssl import SSLWantReadError from ssl import SSLWantWriteError + HAVE_CONTEXT_CHECK_HOSTNAME = False if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'): HAVE_CONTEXT_CHECK_HOSTNAME = True - else: - HAVE_CONTEXT_CHECK_HOSTNAME = False - if hasattr(ssl, "match_hostname"): - from ssl import match_hostname - else: - from backports.ssl_match_hostname import match_hostname - __all__.append("match_hostname") - __all__.append("HAVE_CONTEXT_CHECK_HOSTNAME") + __all__.append("HAVE_CONTEXT_CHECK_HOSTNAME") HAVE_SSL = True except ImportError: - # dummy class of SSLError for ssl none-support environment. + # dummy class of SSLError for environment without ssl support class SSLError(Exception): pass @@ -49,6 +40,5 @@ except ImportError: class SSLWantWriteError(Exception): pass - ssl = lambda: None - + ssl = None HAVE_SSL = False diff --git a/lib/websocket/_url.py b/lib/websocket/_url.py index a394fc34..f2a55019 100644 --- a/lib/websocket/_url.py +++ b/lib/websocket/_url.py @@ -1,30 +1,30 @@ """ + +""" +""" +_url.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ import os import socket import struct -from six.moves.urllib.parse import urlparse +from urllib.parse import unquote, urlparse __all__ = ["parse_url", "get_proxy_info"] @@ -35,14 +35,17 @@ def parse_url(url): parse url and the result is tuple of (hostname, port, resource path and the flag of secure mode) - url: url string. + Parameters + ---------- + url: str + url string. """ if ":" not in url: raise ValueError("url is invalid") scheme, url = url.split(":", 1) - parsed = urlparse(url, scheme="ws") + parsed = urlparse(url, scheme="http") if parsed.hostname: hostname = parsed.hostname else: @@ -94,25 +97,31 @@ def _is_subnet_address(hostname): def _is_address_in_network(ip, net): - ipaddr = struct.unpack('I', socket.inet_aton(ip))[0] - netaddr, bits = net.split('/') - netmask = struct.unpack('I', socket.inet_aton(netaddr))[0] & ((2 << int(bits) - 1) - 1) - return ipaddr & netmask == netmask + ipaddr = struct.unpack('!I', socket.inet_aton(ip))[0] + netaddr, netmask = net.split('/') + netaddr = struct.unpack('!I', socket.inet_aton(netaddr))[0] + + netmask = (0xFFFFFFFF << (32 - int(netmask))) & 0xFFFFFFFF + return ipaddr & netmask == netaddr def _is_no_proxy_host(hostname, no_proxy): if not no_proxy: - v = os.environ.get("no_proxy", "").replace(" ", "") + v = os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(" ", "") if v: no_proxy = v.split(",") if not no_proxy: no_proxy = DEFAULT_NO_PROXY_HOST + if '*' in no_proxy: + return True if hostname in no_proxy: return True - elif _is_ip_address(hostname): + if _is_ip_address(hostname): return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)]) - + for domain in [domain for domain in no_proxy if domain.startswith('.')]: + if hostname.endswith(domain): + return True return False @@ -120,27 +129,30 @@ def get_proxy_info( hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None, no_proxy=None, proxy_type='http'): """ - try to retrieve proxy host and port from environment + Try to retrieve proxy host and port from environment if not provided in options. - result is (proxy_host, proxy_port, proxy_auth). + Result is (proxy_host, proxy_port, proxy_auth). proxy_auth is tuple of username and password - of proxy authentication information. + of proxy authentication information. - hostname: websocket server name. - - is_secure: is the connection secure? (wss) - looks for "https_proxy" in env - before falling back to "http_proxy" - - options: "http_proxy_host" - http proxy host name. - "http_proxy_port" - http proxy port. - "http_no_proxy" - host names, which doesn't use proxy. - "http_proxy_auth" - http proxy auth information. - tuple of username and password. - default is None - "proxy_type" - if set to "socks5" PySocks wrapper - will be used in place of a http proxy. - default is "http" + Parameters + ---------- + hostname: str + Websocket server name. + is_secure: bool + Is the connection secure? (wss) looks for "https_proxy" in env + before falling back to "http_proxy" + proxy_host: str + http proxy host name. + http_proxy_port: str or int + http proxy port. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + http_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". + Use socks4a or socks5h if you want to send DNS requests through the proxy. """ if _is_no_proxy_host(hostname, no_proxy): return None, 0, None @@ -155,10 +167,10 @@ def get_proxy_info( env_keys.insert(0, "https_proxy") for key in env_keys: - value = os.environ.get(key, None) + value = os.environ.get(key, os.environ.get(key.upper(), "")).replace(" ", "") if value: proxy = urlparse(value) - auth = (proxy.username, proxy.password) if proxy.username else None + 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 32ee12ee..feed027e 100644 --- a/lib/websocket/_utils.py +++ b/lib/websocket/_utils.py @@ -1,26 +1,21 @@ """ +_url.py websocket - WebSocket client library for Python -Copyright (C) 2010 Hiroki Ohtani(liris) +Copyright 2021 engn33r - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1335 USA + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ -import six - __all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code"] @@ -81,8 +76,6 @@ except ImportError: state = _UTF8_ACCEPT codep = 0 for i in utfbytes: - if six.PY2: - i = ord(i) state, codep = _decode(state, codep, i) if state == _UTF8_REJECT: return False diff --git a/lib/websocket/tests/data/header03.txt b/lib/websocket/tests/data/header03.txt new file mode 100644 index 00000000..030e13a8 --- /dev/null +++ b/lib/websocket/tests/data/header03.txt @@ -0,0 +1,7 @@ +HTTP/1.1 101 WebSocket Protocol Handshake +Connection: Upgrade, Keep-Alive +Upgrade: WebSocket +Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0= +Set-Cookie: Token=ABCDE +some_header: something + diff --git a/lib/websocket/tests/echo-server.py b/lib/websocket/tests/echo-server.py new file mode 100644 index 00000000..8736def4 --- /dev/null +++ b/lib/websocket/tests/echo-server.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +# From https://github.com/aaugustin/websockets/blob/main/example/echo.py + +import asyncio +import websockets + + +async def echo(websocket, path): + async for message in websocket: + await websocket.send(message) + + +async def main(): + async with websockets.serve(echo, "localhost", 8765): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/lib/websocket/tests/test_abnf.py b/lib/websocket/tests/test_abnf.py new file mode 100644 index 00000000..68282fef --- /dev/null +++ b/lib/websocket/tests/test_abnf.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# +""" +test_abnf.py +websocket - WebSocket client library for Python + +Copyright 2021 engn33r + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os +import websocket as ws +from websocket._abnf import * +import sys +import unittest +sys.path[0:0] = [""] + + +class ABNFTest(unittest.TestCase): + + def testInit(self): + a = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING) + self.assertEqual(a.fin, 0) + self.assertEqual(a.rsv1, 0) + self.assertEqual(a.rsv2, 0) + self.assertEqual(a.rsv3, 0) + self.assertEqual(a.opcode, 9) + self.assertEqual(a.data, '') + a_bad = ABNF(0,1,0,0, opcode=77) + self.assertEqual(a_bad.rsv1, 1) + self.assertEqual(a_bad.opcode, 77) + + def testValidate(self): + a_invalid_ping = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING) + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_invalid_ping.validate, skip_utf8_validation=False) + a_bad_rsv_value = ABNF(0,1,0,0, opcode=ABNF.OPCODE_TEXT) + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_rsv_value.validate, skip_utf8_validation=False) + a_bad_opcode = ABNF(0,0,0,0, opcode=77) + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_opcode.validate, skip_utf8_validation=False) + a_bad_close_frame = ABNF(0,0,0,0, opcode=ABNF.OPCODE_CLOSE, data=b'\x01') + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_close_frame.validate, skip_utf8_validation=False) + a_bad_close_frame_2 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_CLOSE, data=b'\x01\x8a\xaa\xff\xdd') + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_close_frame_2.validate, skip_utf8_validation=False) + a_bad_close_frame_3 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_CLOSE, data=b'\x03\xe7') + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_close_frame_3.validate, skip_utf8_validation=True) + + def testMask(self): + abnf_none_data = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING, mask=1, data=None) + bytes_val = bytes("aaaa", 'utf-8') + self.assertEqual(abnf_none_data._get_masked(bytes_val), bytes_val) + abnf_str_data = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING, mask=1, data="a") + self.assertEqual(abnf_str_data._get_masked(bytes_val), b'aaaa\x00') + + def testFormat(self): + abnf_bad_rsv_bits = ABNF(2,0,0,0, opcode=ABNF.OPCODE_TEXT) + self.assertRaises(ValueError, abnf_bad_rsv_bits.format) + abnf_bad_opcode = ABNF(0,0,0,0, opcode=5) + self.assertRaises(ValueError, abnf_bad_opcode.format) + abnf_length_10 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_TEXT, data="abcdefghij") + self.assertEqual(b'\x01', abnf_length_10.format()[0].to_bytes(1, 'big')) + self.assertEqual(b'\x8a', abnf_length_10.format()[1].to_bytes(1, 'big')) + self.assertEqual("fin=0 opcode=1 data=abcdefghij", abnf_length_10.__str__()) + abnf_length_20 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_BINARY, data="abcdefghijabcdefghij") + self.assertEqual(b'\x02', abnf_length_20.format()[0].to_bytes(1, 'big')) + self.assertEqual(b'\x94', abnf_length_20.format()[1].to_bytes(1, 'big')) + abnf_no_mask = ABNF(0,0,0,0, opcode=ABNF.OPCODE_TEXT, mask=0, data=b'\x01\x8a\xcc') + self.assertEqual(b'\x01\x03\x01\x8a\xcc', abnf_no_mask.format()) + + def testFrameBuffer(self): + fb = frame_buffer(0, True) + self.assertEqual(fb.recv, 0) + self.assertEqual(fb.skip_utf8_validation, True) + fb.clear + self.assertEqual(fb.header, None) + self.assertEqual(fb.length, None) + self.assertEqual(fb.mask, None) + self.assertEqual(fb.has_mask(), False) + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/websocket/tests/test_app.py b/lib/websocket/tests/test_app.py new file mode 100644 index 00000000..d81b06f5 --- /dev/null +++ b/lib/websocket/tests/test_app.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +""" +test_app.py +websocket - WebSocket client library for Python + +Copyright 2021 engn33r + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os +import os.path +import websocket as ws +import sys +import ssl +import unittest +sys.path[0:0] = [""] + +# Skip test to access the internet. +TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1' +TRACEABLE = True + + +class WebSocketAppTest(unittest.TestCase): + + class NotSetYet(object): + """ A marker class for signalling that a value hasn't been set yet. + """ + + def setUp(self): + ws.enableTrace(TRACEABLE) + + WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() + WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() + WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() + + def tearDown(self): + WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() + WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() + WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testKeepRunning(self): + """ A WebSocketApp should keep running as long as its self.keep_running + is not False (in the boolean context). + """ + + def on_open(self, *args, **kwargs): + """ Set the keep_running flag for later inspection and immediately + close the connection. + """ + self.send("hello!") + WebSocketAppTest.keep_running_open = self.keep_running + self.keep_running = False + + def on_message(wsapp, message): + print(message) + self.close() + + def on_close(self, *args, **kwargs): + """ Set the keep_running flag for the test to use. + """ + WebSocketAppTest.keep_running_close = self.keep_running + + app = ws.WebSocketApp('ws://127.0.0.1:8765', on_open=on_open, on_close=on_close, on_message=on_message) + app.run_forever() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSockMaskKey(self): + """ A WebSocketApp should forward the received mask_key function down + to the actual socket. + """ + + def my_mask_key_func(): + return "\x00\x00\x00\x00" + + app = ws.WebSocketApp('wss://stream.meetup.com/2/rsvps', get_mask_key=my_mask_key_func) + + # if numpy is installed, this assertion fail + # Note: We can't use 'is' for comparing the functions directly, need to use 'id'. + self.assertEqual(id(app.get_mask_key), id(my_mask_key_func)) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testInvalidPingIntervalPingTimeout(self): + """ Test exception handling if ping_interval < ping_timeout + """ + + def on_ping(app, msg): + print("Got a ping!") + app.close() + + def on_pong(app, msg): + print("Got a pong! No need to respond") + app.close() + + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', on_ping=on_ping, on_pong=on_pong) + self.assertRaises(ws.WebSocketException, app.run_forever, ping_interval=1, ping_timeout=2, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testPingInterval(self): + """ Test WebSocketApp proper ping functionality + """ + + def on_ping(app, msg): + print("Got a ping!") + app.close() + + def on_pong(app, msg): + print("Got a pong! No need to respond") + app.close() + + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', on_ping=on_ping, on_pong=on_pong) + app.run_forever(ping_interval=2, ping_timeout=1, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testOpcodeClose(self): + """ Test WebSocketApp close opcode + """ + + app = ws.WebSocketApp('wss://tsock.us1.twilio.com/v3/wsconnect') + app.run_forever(ping_interval=2, ping_timeout=1, ping_payload="Ping payload") + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testOpcodeBinary(self): + """ Test WebSocketApp binary opcode + """ + + app = ws.WebSocketApp('streaming.vn.teslamotors.com/streaming/') + app.run_forever(ping_interval=2, ping_timeout=1, ping_payload="Ping payload") + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testBadPingInterval(self): + """ A WebSocketApp handling of negative ping_interval + """ + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1') + self.assertRaises(ws.WebSocketException, app.run_forever, ping_interval=-5, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testBadPingTimeout(self): + """ A WebSocketApp handling of negative ping_timeout + """ + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1') + self.assertRaises(ws.WebSocketException, app.run_forever, ping_timeout=-3, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testCloseStatusCode(self): + """ Test extraction of close frame status code and close reason in WebSocketApp + """ + def on_close(wsapp, close_status_code, close_msg): + print("on_close reached") + + app = ws.WebSocketApp('wss://tsock.us1.twilio.com/v3/wsconnect', on_close=on_close) + closeframe = ws.ABNF(opcode=ws.ABNF.OPCODE_CLOSE, data=b'\x03\xe8no-init-from-client') + self.assertEqual([1000, 'no-init-from-client'], app._get_close_args(closeframe)) + + closeframe = ws.ABNF(opcode=ws.ABNF.OPCODE_CLOSE, data=b'') + self.assertEqual([None, None], app._get_close_args(closeframe)) + + app2 = ws.WebSocketApp('wss://tsock.us1.twilio.com/v3/wsconnect') + closeframe = ws.ABNF(opcode=ws.ABNF.OPCODE_CLOSE, data=b'') + self.assertEqual([None, None], app2._get_close_args(closeframe)) + + self.assertRaises(ws.WebSocketConnectionClosedException, app.send, data="test if connection is closed") + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/websocket/tests/test_cookiejar.py b/lib/websocket/tests/test_cookiejar.py index c40a00bd..69258ac0 100644 --- a/lib/websocket/tests/test_cookiejar.py +++ b/lib/websocket/tests/test_cookiejar.py @@ -1,12 +1,29 @@ +""" + +""" + +""" +test_cookiejar.py +websocket - WebSocket client library for Python + +Copyright 2021 engn33r + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" import unittest from websocket._cookiejar import SimpleCookieJar -try: - import Cookie -except: - import http.cookies as Cookie - class CookieJarTest(unittest.TestCase): def testAdd(self): @@ -29,24 +46,25 @@ class CookieJarTest(unittest.TestCase): cookie_jar = SimpleCookieJar() cookie_jar.add("a=b; c=d; domain=abc") - self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get(None), "") cookie_jar = SimpleCookieJar() cookie_jar.add("a=b; c=d; domain=abc") cookie_jar.add("e=f; domain=abc") - self.assertEquals(cookie_jar.get("abc"), "a=b; c=d; e=f") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f") cookie_jar = SimpleCookieJar() cookie_jar.add("a=b; c=d; domain=abc") cookie_jar.add("e=f; domain=.abc") - self.assertEquals(cookie_jar.get("abc"), "a=b; c=d; e=f") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f") cookie_jar = SimpleCookieJar() cookie_jar.add("a=b; c=d; domain=abc") cookie_jar.add("e=f; domain=xyz") - self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") - self.assertEquals(cookie_jar.get("xyz"), "e=f") - self.assertEquals(cookie_jar.get("something"), "") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get("xyz"), "e=f") + self.assertEqual(cookie_jar.get("something"), "") def testSet(self): cookie_jar = SimpleCookieJar() @@ -64,35 +82,35 @@ class CookieJarTest(unittest.TestCase): cookie_jar = SimpleCookieJar() cookie_jar.set("a=b; c=d; domain=abc") - self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") cookie_jar = SimpleCookieJar() cookie_jar.set("a=b; c=d; domain=abc") cookie_jar.set("e=f; domain=abc") - self.assertEquals(cookie_jar.get("abc"), "e=f") + self.assertEqual(cookie_jar.get("abc"), "e=f") cookie_jar = SimpleCookieJar() cookie_jar.set("a=b; c=d; domain=abc") cookie_jar.set("e=f; domain=.abc") - self.assertEquals(cookie_jar.get("abc"), "e=f") + self.assertEqual(cookie_jar.get("abc"), "e=f") cookie_jar = SimpleCookieJar() cookie_jar.set("a=b; c=d; domain=abc") cookie_jar.set("e=f; domain=xyz") - self.assertEquals(cookie_jar.get("abc"), "a=b; c=d") - self.assertEquals(cookie_jar.get("xyz"), "e=f") - self.assertEquals(cookie_jar.get("something"), "") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get("xyz"), "e=f") + self.assertEqual(cookie_jar.get("something"), "") def testGet(self): cookie_jar = SimpleCookieJar() cookie_jar.set("a=b; c=d; domain=abc.com") - self.assertEquals(cookie_jar.get("abc.com"), "a=b; c=d") - self.assertEquals(cookie_jar.get("x.abc.com"), "a=b; c=d") - self.assertEquals(cookie_jar.get("abc.com.es"), "") - self.assertEquals(cookie_jar.get("xabc.com"), "") + self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("abc.com.es"), "") + self.assertEqual(cookie_jar.get("xabc.com"), "") cookie_jar.set("a=b; c=d; domain=.abc.com") - self.assertEquals(cookie_jar.get("abc.com"), "a=b; c=d") - self.assertEquals(cookie_jar.get("x.abc.com"), "a=b; c=d") - self.assertEquals(cookie_jar.get("abc.com.es"), "") - self.assertEquals(cookie_jar.get("xabc.com"), "") + self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("abc.com.es"), "") + self.assertEqual(cookie_jar.get("xabc.com"), "") diff --git a/lib/websocket/tests/test_http.py b/lib/websocket/tests/test_http.py new file mode 100644 index 00000000..e978bdd8 --- /dev/null +++ b/lib/websocket/tests/test_http.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# +""" +test_http.py +websocket - WebSocket client library for Python + +Copyright 2021 engn33r + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os +import os.path +import websocket as ws +from websocket._http import proxy_info, read_headers, _start_proxied_socket, _tunnel, _get_addrinfo_list, connect +import sys +import unittest +import ssl +import websocket +import socket + +try: + from python_socks.sync import Proxy + from python_socks._errors import * +except: + from websocket._http import ProxyError, ProxyTimeoutError, ProxyConnectionError + +sys.path[0:0] = [""] + +# Skip test to access the internet. +TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1' +TEST_WITH_PROXY = os.environ.get('TEST_WITH_PROXY', '0') == '1' + + +class SockMock(object): + def __init__(self): + self.data = [] + self.sent = [] + + def add_packet(self, data): + self.data.append(data) + + def gettimeout(self): + return None + + def recv(self, bufsize): + if self.data: + e = self.data.pop(0) + if isinstance(e, Exception): + raise e + if len(e) > bufsize: + self.data.insert(0, e[bufsize:]) + return e[:bufsize] + + def send(self, data): + self.sent.append(data) + return len(data) + + def close(self): + pass + + +class HeaderSockMock(SockMock): + + def __init__(self, fname): + SockMock.__init__(self) + path = os.path.join(os.path.dirname(__file__), fname) + with open(path, "rb") as f: + self.add_packet(f.read()) + + +class OptsList(): + + def __init__(self): + self.timeout = 1 + self.sockopt = [] + self.sslopt = {"cert_reqs": ssl.CERT_NONE} + + +class HttpTest(unittest.TestCase): + + def testReadHeader(self): + status, header, status_message = read_headers(HeaderSockMock("data/header01.txt")) + self.assertEqual(status, 101) + self.assertEqual(header["connection"], "Upgrade") + # header02.txt is intentionally malformed + self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt")) + + def testTunnel(self): + self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock("data/header01.txt"), "example.com", 80, ("username", "password")) + self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock("data/header02.txt"), "example.com", 80, ("username", "password")) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testConnect(self): + # Not currently testing an actual proxy connection, so just check whether proxy errors are raised. This requires internet for a DNS lookup + if ws._http.HAVE_PYTHON_SOCKS: + # Need this check, otherwise case where python_socks is not installed triggers + # websocket._exceptions.WebSocketException: Python Socks is needed for SOCKS proxying but is not available + self.assertRaises(ProxyTimeoutError, _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4", timeout=1)) + self.assertRaises(ProxyTimeoutError, _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4a", timeout=1)) + self.assertRaises(ProxyTimeoutError, _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5", timeout=1)) + self.assertRaises(ProxyTimeoutError, _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5h", timeout=1)) + self.assertRaises(ProxyConnectionError, connect, "wss://example.com", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=9999, proxy_type="socks4", timeout=1), None) + + self.assertRaises(TypeError, _get_addrinfo_list, None, 80, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http")) + self.assertRaises(TypeError, _get_addrinfo_list, None, 80, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http")) + self.assertRaises(socket.timeout, connect, "wss://google.com", OptsList(), proxy_info(http_proxy_host="8.8.8.8", http_proxy_port=9999, proxy_type="http", timeout=1), None) + self.assertEqual( + connect("wss://google.com", OptsList(), proxy_info(http_proxy_host="8.8.8.8", http_proxy_port=8080, proxy_type="http"), True), + (True, ("google.com", 443, "/"))) + # The following test fails on Mac OS with a gaierror, not an OverflowError + # self.assertRaises(OverflowError, connect, "wss://example.com", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=99999, proxy_type="socks4", timeout=2), False) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + @unittest.skipUnless(TEST_WITH_PROXY, "This test requires a HTTP proxy to be running on port 8899") + def testProxyConnect(self): + ws = websocket.WebSocket() + ws.connect("ws://127.0.0.1:8765", http_proxy_host="127.0.0.1", http_proxy_port="8899", proxy_type="http") + ws.send("Hello, Server") + server_response = ws.recv() + self.assertEqual(server_response, "Hello, Server") + # self.assertEqual(_start_proxied_socket("wss://api.bitfinex.com/ws/2", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8899", proxy_type="http"))[1], ("api.bitfinex.com", 443, '/ws/2')) + self.assertEqual(_get_addrinfo_list("api.bitfinex.com", 443, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8899", proxy_type="http")), + (socket.getaddrinfo("127.0.0.1", 8899, 0, socket.SOCK_STREAM, socket.SOL_TCP), True, None)) + self.assertEqual(connect("wss://api.bitfinex.com/ws/2", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=8899, proxy_type="http"), None)[1], ("api.bitfinex.com", 443, '/ws/2')) + # TODO: Test SOCKS4 and SOCK5 proxies with unit tests + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSSLopt(self): + ssloptions = { + "cert_reqs": ssl.CERT_NONE, + "check_hostname": False, + "server_hostname": "ServerName", + "ssl_version": ssl.PROTOCOL_TLS, + "ciphers": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:\ + TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:\ + ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:\ + ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:\ + DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:\ + ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:\ + ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:\ + DHE-RSA-AES256-SHA256:ECDHE-ECDSA-AES128-SHA256:\ + ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:\ + ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA", + "ecdh_curve": "prime256v1" + } + ws_ssl1 = websocket.WebSocket(sslopt=ssloptions) + ws_ssl1.connect("wss://api.bitfinex.com/ws/2") + ws_ssl1.send("Hello") + ws_ssl1.close() + + ws_ssl2 = websocket.WebSocket(sslopt={"check_hostname": True}) + ws_ssl2.connect("wss://api.bitfinex.com/ws/2") + ws_ssl2.close + + def testProxyInfo(self): + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").proxy_protocol, "http") + self.assertRaises(ProxyError, proxy_info, http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="badval") + self.assertEqual(proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="http").proxy_host, "example.com") + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").proxy_port, "8080") + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").auth, None) + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http", http_proxy_auth=("my_username123", "my_pass321")).auth[0], "my_username123") + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http", http_proxy_auth=("my_username123", "my_pass321")).auth[1], "my_pass321") + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/websocket/tests/test_url.py b/lib/websocket/tests/test_url.py new file mode 100644 index 00000000..dc24bb8b --- /dev/null +++ b/lib/websocket/tests/test_url.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- +# +""" +test_url.py +websocket - WebSocket client library for Python + +Copyright 2021 engn33r + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import sys +import os +import unittest +sys.path[0:0] = [""] +from websocket._url import get_proxy_info, parse_url, _is_address_in_network, _is_no_proxy_host + + +class UrlTest(unittest.TestCase): + + def test_address_in_network(self): + self.assertTrue(_is_address_in_network('127.0.0.1', '127.0.0.0/8')) + self.assertTrue(_is_address_in_network('127.1.0.1', '127.0.0.0/8')) + self.assertFalse(_is_address_in_network('127.1.0.1', '127.0.0.0/24')) + + def testParseUrl(self): + p = parse_url("ws://www.example.com/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com/r/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("wss://www.example.com:8080/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + p = parse_url("wss://www.example.com:8080/r?key=value") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r?key=value") + self.assertEqual(p[3], True) + + self.assertRaises(ValueError, parse_url, "http://www.example.com/r") + + p = parse_url("ws://[2a03:4000:123:83::3]/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://[2a03:4000:123:83::3]:8080/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("wss://[2a03:4000:123:83::3]/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 443) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + p = parse_url("wss://[2a03:4000:123:83::3]:8080/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + +class IsNoProxyHostTest(unittest.TestCase): + def setUp(self): + self.no_proxy = os.environ.get("no_proxy", None) + if "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def tearDown(self): + if self.no_proxy: + os.environ["no_proxy"] = self.no_proxy + elif "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def testMatchAll(self): + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['*'])) + self.assertTrue(_is_no_proxy_host("192.168.0.1", ['*'])) + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['other.websocket.org', '*'])) + os.environ['no_proxy'] = '*' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + self.assertTrue(_is_no_proxy_host("192.168.0.1", None)) + os.environ['no_proxy'] = 'other.websocket.org, *' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + + def testIpAddress(self): + self.assertTrue(_is_no_proxy_host("127.0.0.1", ['127.0.0.1'])) + self.assertFalse(_is_no_proxy_host("127.0.0.2", ['127.0.0.1'])) + self.assertTrue(_is_no_proxy_host("127.0.0.1", ['other.websocket.org', '127.0.0.1'])) + self.assertFalse(_is_no_proxy_host("127.0.0.2", ['other.websocket.org', '127.0.0.1'])) + os.environ['no_proxy'] = '127.0.0.1' + self.assertTrue(_is_no_proxy_host("127.0.0.1", None)) + self.assertFalse(_is_no_proxy_host("127.0.0.2", None)) + os.environ['no_proxy'] = 'other.websocket.org, 127.0.0.1' + self.assertTrue(_is_no_proxy_host("127.0.0.1", None)) + self.assertFalse(_is_no_proxy_host("127.0.0.2", None)) + + def testIpAddressInRange(self): + self.assertTrue(_is_no_proxy_host("127.0.0.1", ['127.0.0.0/8'])) + self.assertTrue(_is_no_proxy_host("127.0.0.2", ['127.0.0.0/8'])) + self.assertFalse(_is_no_proxy_host("127.1.0.1", ['127.0.0.0/24'])) + os.environ['no_proxy'] = '127.0.0.0/8' + self.assertTrue(_is_no_proxy_host("127.0.0.1", None)) + self.assertTrue(_is_no_proxy_host("127.0.0.2", None)) + os.environ['no_proxy'] = '127.0.0.0/24' + self.assertFalse(_is_no_proxy_host("127.1.0.1", None)) + + def testHostnameMatch(self): + self.assertTrue(_is_no_proxy_host("my.websocket.org", ['my.websocket.org'])) + self.assertTrue(_is_no_proxy_host("my.websocket.org", ['other.websocket.org', 'my.websocket.org'])) + self.assertFalse(_is_no_proxy_host("my.websocket.org", ['other.websocket.org'])) + os.environ['no_proxy'] = 'my.websocket.org' + self.assertTrue(_is_no_proxy_host("my.websocket.org", None)) + self.assertFalse(_is_no_proxy_host("other.websocket.org", None)) + os.environ['no_proxy'] = 'other.websocket.org, my.websocket.org' + self.assertTrue(_is_no_proxy_host("my.websocket.org", None)) + + def testHostnameMatchDomain(self): + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['.websocket.org'])) + self.assertTrue(_is_no_proxy_host("my.other.websocket.org", ['.websocket.org'])) + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['my.websocket.org', '.websocket.org'])) + self.assertFalse(_is_no_proxy_host("any.websocket.com", ['.websocket.org'])) + os.environ['no_proxy'] = '.websocket.org' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + self.assertTrue(_is_no_proxy_host("my.other.websocket.org", None)) + self.assertFalse(_is_no_proxy_host("any.websocket.com", None)) + os.environ['no_proxy'] = 'my.websocket.org, .websocket.org' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + + +class ProxyInfoTest(unittest.TestCase): + def setUp(self): + self.http_proxy = os.environ.get("http_proxy", None) + self.https_proxy = os.environ.get("https_proxy", None) + self.no_proxy = os.environ.get("no_proxy", None) + if "http_proxy" in os.environ: + del os.environ["http_proxy"] + if "https_proxy" in os.environ: + del os.environ["https_proxy"] + if "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def tearDown(self): + if self.http_proxy: + os.environ["http_proxy"] = self.http_proxy + elif "http_proxy" in os.environ: + del os.environ["http_proxy"] + + if self.https_proxy: + os.environ["https_proxy"] = self.https_proxy + elif "https_proxy" in os.environ: + del os.environ["https_proxy"] + + if self.no_proxy: + os.environ["no_proxy"] = self.no_proxy + elif "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def testProxyFromArgs(self): + self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost"), ("localhost", 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128), + ("localhost", 3128, None)) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost"), ("localhost", 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128), + ("localhost", 3128, None)) + + self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_auth=("a", "b")), + ("localhost", 0, ("a", "b"))) + self.assertEqual( + get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_auth=("a", "b")), + ("localhost", 0, ("a", "b"))) + self.assertEqual( + get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, + no_proxy=["example.com"], proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, + no_proxy=["echo.websocket.org"], proxy_auth=("a", "b")), + (None, 0, None)) + + def testProxyFromEnv(self): + os.environ["http_proxy"] = "http://localhost/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None)) + + os.environ["http_proxy"] = "http://localhost/" + os.environ["https_proxy"] = "http://localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + os.environ["https_proxy"] = "http://localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None)) + + os.environ["http_proxy"] = "http://localhost/" + os.environ["https_proxy"] = "http://localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + os.environ["https_proxy"] = "http://localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, None)) + + os.environ["http_proxy"] = "http://a:b@localhost/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://john%40example.com:P%40SSWORD@localhost:3128/" + os.environ["https_proxy"] = "http://john%40example.com:P%40SSWORD@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("john@example.com", "P@SSWORD"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + os.environ["no_proxy"] = "example1.com,example2.com" + self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.org" + self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None)) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "example1.com,example2.com, .websocket.org" + self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None)) + + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "127.0.0.0/8, 192.168.0.0/16" + self.assertEqual(get_proxy_info("127.0.0.1", False), (None, 0, None)) + self.assertEqual(get_proxy_info("192.168.1.1", False), (None, 0, None)) + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/websocket/tests/test_websocket.py b/lib/websocket/tests/test_websocket.py index 8b131bb6..f0c38ee4 100644 --- a/lib/websocket/tests/test_websocket.py +++ b/lib/websocket/tests/test_websocket.py @@ -1,34 +1,44 @@ # -*- coding: utf-8 -*- # +""" + +""" + +""" +test_websocket.py +websocket - WebSocket client library for Python + +Copyright 2021 engn33r + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" import sys sys.path[0:0] = [""] - import os import os.path import socket - -import six - -# websocket-client import websocket as ws from websocket._handshake import _create_sec_websocket_key, \ _validate as _validate_header from websocket._http import read_headers -from websocket._url import get_proxy_info, parse_url from websocket._utils import validate_utf8 +from base64 import decodebytes as base64decode -if six.PY3: - from base64 import decodebytes as base64decode -else: - from base64 import decodestring as base64decode - -if sys.version_info[0] == 2 and sys.version_info[1] < 7: - import unittest2 as unittest -else: - import unittest +import unittest try: + import ssl from ssl import SSLError except ImportError: # dummy class of SSLError for ssl none-support environment. @@ -37,9 +47,6 @@ except ImportError: # Skip test to access the internet. TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1' - -# Skip Secure WebSocket test. -TEST_SECURE_WS = True TRACEABLE = True @@ -97,102 +104,24 @@ class WebSocketTest(unittest.TestCase): self.assertEqual(ws.getdefaulttimeout(), 10) ws.setdefaulttimeout(None) - def testParseUrl(self): - p = parse_url("ws://www.example.com/r") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 80) - self.assertEqual(p[2], "/r") - self.assertEqual(p[3], False) - - p = parse_url("ws://www.example.com/r/") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 80) - self.assertEqual(p[2], "/r/") - self.assertEqual(p[3], False) - - p = parse_url("ws://www.example.com/") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 80) - self.assertEqual(p[2], "/") - self.assertEqual(p[3], False) - - p = parse_url("ws://www.example.com") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 80) - self.assertEqual(p[2], "/") - self.assertEqual(p[3], False) - - p = parse_url("ws://www.example.com:8080/r") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 8080) - self.assertEqual(p[2], "/r") - self.assertEqual(p[3], False) - - p = parse_url("ws://www.example.com:8080/") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 8080) - self.assertEqual(p[2], "/") - self.assertEqual(p[3], False) - - p = parse_url("ws://www.example.com:8080") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 8080) - self.assertEqual(p[2], "/") - self.assertEqual(p[3], False) - - p = parse_url("wss://www.example.com:8080/r") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 8080) - self.assertEqual(p[2], "/r") - self.assertEqual(p[3], True) - - p = parse_url("wss://www.example.com:8080/r?key=value") - self.assertEqual(p[0], "www.example.com") - self.assertEqual(p[1], 8080) - self.assertEqual(p[2], "/r?key=value") - self.assertEqual(p[3], True) - - self.assertRaises(ValueError, parse_url, "http://www.example.com/r") - - if sys.version_info[0] == 2 and sys.version_info[1] < 7: - return - - p = parse_url("ws://[2a03:4000:123:83::3]/r") - self.assertEqual(p[0], "2a03:4000:123:83::3") - self.assertEqual(p[1], 80) - self.assertEqual(p[2], "/r") - self.assertEqual(p[3], False) - - p = parse_url("ws://[2a03:4000:123:83::3]:8080/r") - self.assertEqual(p[0], "2a03:4000:123:83::3") - self.assertEqual(p[1], 8080) - self.assertEqual(p[2], "/r") - self.assertEqual(p[3], False) - - p = parse_url("wss://[2a03:4000:123:83::3]/r") - self.assertEqual(p[0], "2a03:4000:123:83::3") - self.assertEqual(p[1], 443) - self.assertEqual(p[2], "/r") - self.assertEqual(p[3], True) - - p = parse_url("wss://[2a03:4000:123:83::3]:8080/r") - self.assertEqual(p[0], "2a03:4000:123:83::3") - self.assertEqual(p[1], 8080) - self.assertEqual(p[2], "/r") - self.assertEqual(p[3], True) - def testWSKey(self): key = _create_sec_websocket_key() self.assertTrue(key != 24) - self.assertTrue(six.u("¥n") not in key) + self.assertTrue(str("¥n") not in key) + + def testNonce(self): + """ WebSocket key should be a random 16-byte nonce. + """ + key = _create_sec_websocket_key() + nonce = base64decode(key.encode("utf-8")) + self.assertEqual(16, len(nonce)) def testWsUtils(self): key = "c6b8hTg4EeGb2gQMztV1/g==" required_header = { "upgrade": "websocket", "connection": "upgrade", - "sec-websocket-accept": "Kxep+hNu9n51529fGidYu7a3wO0=", - } + "sec-websocket-accept": "Kxep+hNu9n51529fGidYu7a3wO0="} self.assertEqual(_validate_header(required_header, key, None), (True, None)) header = required_header.copy() @@ -216,18 +145,26 @@ class WebSocketTest(unittest.TestCase): header = required_header.copy() header["sec-websocket-protocol"] = "sub1" self.assertEqual(_validate_header(header, key, ["sub1", "sub2"]), (True, "sub1")) + # This case will print out a logging error using the error() function, but that is expected self.assertEqual(_validate_header(header, key, ["sub2", "sub3"]), (False, None)) header = required_header.copy() header["sec-websocket-protocol"] = "sUb1" self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (True, "sub1")) + header = required_header.copy() + # This case will print out a logging error using the error() function, but that is expected + self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (False, None)) def testReadHeader(self): status, header, status_message = read_headers(HeaderSockMock("data/header01.txt")) self.assertEqual(status, 101) self.assertEqual(header["connection"], "Upgrade") + status, header, status_message = read_headers(HeaderSockMock("data/header03.txt")) + self.assertEqual(status, 101) + self.assertEqual(header["connection"], "Upgrade, Keep-Alive") + HeaderSockMock("data/header02.txt") self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt")) @@ -237,71 +174,67 @@ class WebSocketTest(unittest.TestCase): sock.set_mask_key(create_mask_key) s = sock.sock = HeaderSockMock("data/header01.txt") sock.send("Hello") - self.assertEqual(s.sent[0], six.b("\x81\x85abcd)\x07\x0f\x08\x0e")) + self.assertEqual(s.sent[0], b'\x81\x85abcd)\x07\x0f\x08\x0e') sock.send("こんにちは") - self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc")) + self.assertEqual(s.sent[1], b'\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc') - sock.send(u"こんにちは") - self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc")) +# sock.send("x" * 5000) +# self.assertEqual(s.sent[1], b'\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc") - sock.send("x" * 127) + self.assertEqual(sock.send_binary(b'1111111111101'), 19) def testRecv(self): # TODO: add longer frame data sock = ws.WebSocket() s = sock.sock = SockMock() - something = six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc") + something = b'\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc' s.add_packet(something) data = sock.recv() self.assertEqual(data, "こんにちは") - s.add_packet(six.b("\x81\x85abcd)\x07\x0f\x08\x0e")) + s.add_packet(b'\x81\x85abcd)\x07\x0f\x08\x0e') data = sock.recv() self.assertEqual(data, "Hello") @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") def testIter(self): count = 2 - for _ in ws.create_connection('ws://stream.meetup.com/2/rsvps'): + for _ in ws.create_connection('wss://stream.meetup.com/2/rsvps'): count -= 1 if count == 0: break @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") def testNext(self): - sock = ws.create_connection('ws://stream.meetup.com/2/rsvps') + sock = ws.create_connection('wss://stream.meetup.com/2/rsvps') self.assertEqual(str, type(next(sock))) def testInternalRecvStrict(self): sock = ws.WebSocket() s = sock.sock = SockMock() - s.add_packet(six.b("foo")) + s.add_packet(b'foo') s.add_packet(socket.timeout()) - s.add_packet(six.b("bar")) + s.add_packet(b'bar') # s.add_packet(SSLError("The read operation timed out")) - s.add_packet(six.b("baz")) + s.add_packet(b'baz') with self.assertRaises(ws.WebSocketTimeoutException): sock.frame_buffer.recv_strict(9) - # if six.PY2: - # with self.assertRaises(ws.WebSocketTimeoutException): - # data = sock._recv_strict(9) - # else: # with self.assertRaises(SSLError): # data = sock._recv_strict(9) data = sock.frame_buffer.recv_strict(9) - self.assertEqual(data, six.b("foobarbaz")) + self.assertEqual(data, b'foobarbaz') with self.assertRaises(ws.WebSocketConnectionClosedException): sock.frame_buffer.recv_strict(1) def testRecvTimeout(self): sock = ws.WebSocket() s = sock.sock = SockMock() - s.add_packet(six.b("\x81")) + s.add_packet(b'\x81') s.add_packet(socket.timeout()) - s.add_packet(six.b("\x8dabcd\x29\x07\x0f\x08\x0e")) + s.add_packet(b'\x8dabcd\x29\x07\x0f\x08\x0e') s.add_packet(socket.timeout()) - s.add_packet(six.b("\x4e\x43\x33\x0e\x10\x0f\x00\x40")) + s.add_packet(b'\x4e\x43\x33\x0e\x10\x0f\x00\x40') with self.assertRaises(ws.WebSocketTimeoutException): sock.recv() with self.assertRaises(ws.WebSocketTimeoutException): @@ -315,9 +248,9 @@ class WebSocketTest(unittest.TestCase): sock = ws.WebSocket() s = sock.sock = SockMock() # OPCODE=TEXT, FIN=0, MSG="Brevity is " - s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + s.add_packet(b'\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') # OPCODE=CONT, FIN=1, MSG="the soul of wit" - s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17")) + s.add_packet(b'\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17') data = sock.recv() self.assertEqual(data, "Brevity is the soul of wit") with self.assertRaises(ws.WebSocketConnectionClosedException): @@ -327,21 +260,21 @@ class WebSocketTest(unittest.TestCase): sock = ws.WebSocket(fire_cont_frame=True) s = sock.sock = SockMock() # OPCODE=TEXT, FIN=0, MSG="Brevity is " - s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + s.add_packet(b'\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') # OPCODE=CONT, FIN=0, MSG="Brevity is " - s.add_packet(six.b("\x00\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + s.add_packet(b'\x00\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') # OPCODE=CONT, FIN=1, MSG="the soul of wit" - s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17")) + s.add_packet(b'\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17') _, data = sock.recv_data() - self.assertEqual(data, six.b("Brevity is ")) + self.assertEqual(data, b'Brevity is ') _, data = sock.recv_data() - self.assertEqual(data, six.b("Brevity is ")) + self.assertEqual(data, b'Brevity is ') _, data = sock.recv_data() - self.assertEqual(data, six.b("the soul of wit")) + self.assertEqual(data, b'the soul of wit') # OPCODE=CONT, FIN=0, MSG="Brevity is " - s.add_packet(six.b("\x80\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C")) + s.add_packet(b'\x80\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') with self.assertRaises(ws.WebSocketException): sock.recv_data() @@ -351,15 +284,13 @@ class WebSocketTest(unittest.TestCase): def testClose(self): sock = ws.WebSocket() - sock.sock = SockMock() sock.connected = True - sock.close() - self.assertEqual(sock.connected, False) + sock.close sock = ws.WebSocket() s = sock.sock = SockMock() sock.connected = True - s.add_packet(six.b('\x88\x80\x17\x98p\x84')) + s.add_packet(b'\x88\x80\x17\x98p\x84') sock.recv() self.assertEqual(sock.connected, False) @@ -367,20 +298,18 @@ class WebSocketTest(unittest.TestCase): sock = ws.WebSocket() s = sock.sock = SockMock() # OPCODE=CONT, FIN=1, MSG="the soul of wit" - s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17")) + s.add_packet(b'\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17') self.assertRaises(ws.WebSocketException, sock.recv) def testRecvWithProlongedFragmentation(self): sock = ws.WebSocket() s = sock.sock = SockMock() # OPCODE=TEXT, FIN=0, MSG="Once more unto the breach, " - s.add_packet(six.b("\x01\x9babcd.\x0c\x00\x01A\x0f\x0c\x16\x04B\x16\n\x15" - "\rC\x10\t\x07C\x06\x13\x07\x02\x07\tNC")) + s.add_packet(b'\x01\x9babcd.\x0c\x00\x01A\x0f\x0c\x16\x04B\x16\n\x15\rC\x10\t\x07C\x06\x13\x07\x02\x07\tNC') # OPCODE=CONT, FIN=0, MSG="dear friends, " - s.add_packet(six.b("\x00\x8eabcd\x05\x07\x02\x16A\x04\x11\r\x04\x0c\x07" - "\x17MB")) + s.add_packet(b'\x00\x8eabcd\x05\x07\x02\x16A\x04\x11\r\x04\x0c\x07\x17MB') # OPCODE=CONT, FIN=1, MSG="once more" - s.add_packet(six.b("\x80\x89abcd\x0e\x0c\x00\x01A\x0f\x0c\x16\x04")) + s.add_packet(b'\x80\x89abcd\x0e\x0c\x00\x01A\x0f\x0c\x16\x04') data = sock.recv() self.assertEqual( data, @@ -393,272 +322,135 @@ class WebSocketTest(unittest.TestCase): sock.set_mask_key(create_mask_key) s = sock.sock = SockMock() # OPCODE=TEXT, FIN=0, MSG="Too much " - s.add_packet(six.b("\x01\x89abcd5\r\x0cD\x0c\x17\x00\x0cA")) + s.add_packet(b'\x01\x89abcd5\r\x0cD\x0c\x17\x00\x0cA') # OPCODE=PING, FIN=1, MSG="Please PONG this" - s.add_packet(six.b("\x89\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17")) + s.add_packet(b'\x89\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17') # OPCODE=CONT, FIN=1, MSG="of a good thing" - s.add_packet(six.b("\x80\x8fabcd\x0e\x04C\x05A\x05\x0c\x0b\x05B\x17\x0c" - "\x08\x0c\x04")) + s.add_packet(b'\x80\x8fabcd\x0e\x04C\x05A\x05\x0c\x0b\x05B\x17\x0c\x08\x0c\x04') data = sock.recv() self.assertEqual(data, "Too much of a good thing") with self.assertRaises(ws.WebSocketConnectionClosedException): sock.recv() self.assertEqual( s.sent[0], - six.b("\x8a\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17")) + b'\x8a\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17') @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") def testWebSocket(self): - s = ws.create_connection("ws://echo.websocket.org/") + s = ws.create_connection("ws://127.0.0.1:8765") self.assertNotEqual(s, None) s.send("Hello, World") - result = s.recv() + result = s.next() + s.fileno() self.assertEqual(result, "Hello, World") - s.send(u"こにゃにゃちは、世界") + s.send("こにゃにゃちは、世界") result = s.recv() self.assertEqual(result, "こにゃにゃちは、世界") + self.assertRaises(ValueError, s.send_close, -1, "") s.close() @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") def testPingPong(self): - s = ws.create_connection("ws://echo.websocket.org/") + s = ws.create_connection("ws://127.0.0.1:8765") self.assertNotEqual(s, None) s.ping("Hello") s.pong("Hi") s.close() @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") - @unittest.skipUnless(TEST_SECURE_WS, "wss://echo.websocket.org doesn't work well.") - def testSecureWebSocket(self): - if 1: - import ssl - s = ws.create_connection("wss://echo.websocket.org/") - self.assertNotEqual(s, None) - self.assertTrue(isinstance(s.sock, ssl.SSLSocket)) - s.send("Hello, World") - result = s.recv() - self.assertEqual(result, "Hello, World") - s.send(u"こにゃにゃちは、世界") - result = s.recv() - self.assertEqual(result, "こにゃにゃちは、世界") - s.close() - #except: - # pass + def testSupportRedirect(self): + s = ws.WebSocket() + self.assertRaises(ws._exceptions.WebSocketBadStatusException, s.connect, "ws://google.com/") + # Need to find a URL that has a redirect code leading to a websocket @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") - def testWebSocketWihtCustomHeader(self): - s = ws.create_connection("ws://echo.websocket.org/", + def testSecureWebSocket(self): + import ssl + s = ws.create_connection("wss://api.bitfinex.com/ws/2") + self.assertNotEqual(s, None) + self.assertTrue(isinstance(s.sock, ssl.SSLSocket)) + self.assertEqual(s.getstatus(), 101) + self.assertNotEqual(s.getheaders(), None) + s.settimeout(10) + self.assertEqual(s.gettimeout(), 10) + self.assertEqual(s.getsubprotocol(), None) + s.abort() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testWebSocketWithCustomHeader(self): + s = ws.create_connection("ws://127.0.0.1:8765", headers={"User-Agent": "PythonWebsocketClient"}) self.assertNotEqual(s, None) s.send("Hello, World") result = s.recv() self.assertEqual(result, "Hello, World") + self.assertRaises(ValueError, s.close, -1, "") s.close() @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") def testAfterClose(self): - s = ws.create_connection("ws://echo.websocket.org/") + s = ws.create_connection("ws://127.0.0.1:8765") self.assertNotEqual(s, None) s.close() self.assertRaises(ws.WebSocketConnectionClosedException, s.send, "Hello") self.assertRaises(ws.WebSocketConnectionClosedException, s.recv) - def testNonce(self): - """ WebSocket key should be a random 16-byte nonce. - """ - key = _create_sec_websocket_key() - nonce = base64decode(key.encode("utf-8")) - self.assertEqual(16, len(nonce)) - - -class WebSocketAppTest(unittest.TestCase): - - class NotSetYet(object): - """ A marker class for signalling that a value hasn't been set yet. - """ - - def setUp(self): - ws.enableTrace(TRACEABLE) - - WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() - WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() - WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() - - def tearDown(self): - WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() - WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() - WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() - - @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") - def testKeepRunning(self): - """ A WebSocketApp should keep running as long as its self.keep_running - is not False (in the boolean context). - """ - - def on_open(self, *args, **kwargs): - """ Set the keep_running flag for later inspection and immediately - close the connection. - """ - WebSocketAppTest.keep_running_open = self.keep_running - - self.close() - - def on_close(self, *args, **kwargs): - """ Set the keep_running flag for the test to use. - """ - WebSocketAppTest.keep_running_close = self.keep_running - - app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, on_close=on_close) - app.run_forever() - - # if numpy is installed, this assertion fail - # self.assertFalse(isinstance(WebSocketAppTest.keep_running_open, - # WebSocketAppTest.NotSetYet)) - - # self.assertFalse(isinstance(WebSocketAppTest.keep_running_close, - # WebSocketAppTest.NotSetYet)) - - # self.assertEqual(True, WebSocketAppTest.keep_running_open) - # self.assertEqual(False, WebSocketAppTest.keep_running_close) - - @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") - def testSockMaskKey(self): - """ A WebSocketApp should forward the received mask_key function down - to the actual socket. - """ - - def my_mask_key_func(): - pass - - def on_open(self, *args, **kwargs): - """ Set the value so the test can use it later on and immediately - close the connection. - """ - WebSocketAppTest.get_mask_key_id = id(self.get_mask_key) - self.close() - - app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, get_mask_key=my_mask_key_func) - app.run_forever() - - # if numpu is installed, this assertion fail - # Note: We can't use 'is' for comparing the functions directly, need to use 'id'. - # self.assertEqual(WebSocketAppTest.get_mask_key_id, id(my_mask_key_func)) - class SockOptTest(unittest.TestCase): @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") def testSockOpt(self): sockopt = ((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),) - s = ws.create_connection("ws://echo.websocket.org", sockopt=sockopt) + s = ws.create_connection("ws://127.0.0.1:8765", sockopt=sockopt) self.assertNotEqual(s.sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY), 0) s.close() class UtilsTest(unittest.TestCase): def testUtf8Validator(self): - state = validate_utf8(six.b('\xf0\x90\x80\x80')) + state = validate_utf8(b'\xf0\x90\x80\x80') self.assertEqual(state, True) - state = validate_utf8(six.b('\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80edited')) + state = validate_utf8(b'\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80edited') self.assertEqual(state, False) - state = validate_utf8(six.b('')) + state = validate_utf8(b'') self.assertEqual(state, True) -class ProxyInfoTest(unittest.TestCase): - def setUp(self): - self.http_proxy = os.environ.get("http_proxy", None) - self.https_proxy = os.environ.get("https_proxy", None) - if "http_proxy" in os.environ: - del os.environ["http_proxy"] - if "https_proxy" in os.environ: - del os.environ["https_proxy"] +class HandshakeTest(unittest.TestCase): + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def test_http_SSL(self): + websock1 = ws.WebSocket(sslopt={"cert_chain": ssl.get_default_verify_paths().capath}, enable_multithread=False) + self.assertRaises(ValueError, + websock1.connect, "wss://api.bitfinex.com/ws/2") + websock2 = ws.WebSocket(sslopt={"certfile": "myNonexistentCertFile"}) + self.assertRaises(FileNotFoundError, + websock2.connect, "wss://api.bitfinex.com/ws/2") - def tearDown(self): - if self.http_proxy: - os.environ["http_proxy"] = self.http_proxy - elif "http_proxy" in os.environ: - del os.environ["http_proxy"] + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testManualHeaders(self): + websock3 = ws.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE, + "ca_certs": ssl.get_default_verify_paths().capath, + "ca_cert_path": ssl.get_default_verify_paths().openssl_cafile}) + self.assertRaises(ws._exceptions.WebSocketBadStatusException, + websock3.connect, "wss://api.bitfinex.com/ws/2", cookie="chocolate", + origin="testing_websockets.com", + host="echo.websocket.org/websocket-client-test", + subprotocols=["testproto"], + connection="Upgrade", + header={"CustomHeader1":"123", + "Cookie":"TestValue", + "Sec-WebSocket-Key":"k9kFAUWNAMmf5OEMfTlOEA==", + "Sec-WebSocket-Protocol":"newprotocol"}) - if self.https_proxy: - os.environ["https_proxy"] = self.https_proxy - elif "https_proxy" in os.environ: - del os.environ["https_proxy"] + def testIPv6(self): + websock2 = ws.WebSocket() + self.assertRaises(ValueError, websock2.connect, "2001:4860:4860::8888") - def testProxyFromArgs(self): - self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost"), ("localhost", 0, None)) - self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None)) - self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost"), ("localhost", 0, None)) - self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None)) - - self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_auth=("a", "b")), - ("localhost", 0, ("a", "b"))) - self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), - ("localhost", 3128, ("a", "b"))) - self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_auth=("a", "b")), - ("localhost", 0, ("a", "b"))) - self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), - ("localhost", 3128, ("a", "b"))) - - self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["example.com"], proxy_auth=("a", "b")), - ("localhost", 3128, ("a", "b"))) - self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["echo.websocket.org"], proxy_auth=("a", "b")), - (None, 0, None)) - - def testProxyFromEnv(self): - os.environ["http_proxy"] = "http://localhost/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None)) - os.environ["http_proxy"] = "http://localhost:3128/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None)) - - os.environ["http_proxy"] = "http://localhost/" - os.environ["https_proxy"] = "http://localhost2/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None)) - os.environ["http_proxy"] = "http://localhost:3128/" - os.environ["https_proxy"] = "http://localhost2:3128/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None)) - - os.environ["http_proxy"] = "http://localhost/" - os.environ["https_proxy"] = "http://localhost2/" - self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, None)) - os.environ["http_proxy"] = "http://localhost:3128/" - os.environ["https_proxy"] = "http://localhost2:3128/" - self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, None)) - - - os.environ["http_proxy"] = "http://a:b@localhost/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b"))) - os.environ["http_proxy"] = "http://a:b@localhost:3128/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b"))) - - os.environ["http_proxy"] = "http://a:b@localhost/" - os.environ["https_proxy"] = "http://a:b@localhost2/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b"))) - os.environ["http_proxy"] = "http://a:b@localhost:3128/" - os.environ["https_proxy"] = "http://a:b@localhost2:3128/" - self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b"))) - - os.environ["http_proxy"] = "http://a:b@localhost/" - os.environ["https_proxy"] = "http://a:b@localhost2/" - self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, ("a", "b"))) - os.environ["http_proxy"] = "http://a:b@localhost:3128/" - os.environ["https_proxy"] = "http://a:b@localhost2:3128/" - self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("a", "b"))) - - os.environ["http_proxy"] = "http://a:b@localhost/" - os.environ["https_proxy"] = "http://a:b@localhost2/" - os.environ["no_proxy"] = "example1.com,example2.com" - self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b"))) - os.environ["http_proxy"] = "http://a:b@localhost:3128/" - os.environ["https_proxy"] = "http://a:b@localhost2:3128/" - os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.org" - self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None)) - - os.environ["http_proxy"] = "http://a:b@localhost:3128/" - os.environ["https_proxy"] = "http://a:b@localhost2:3128/" - os.environ["no_proxy"] = "127.0.0.0/8, 192.168.0.0/16" - self.assertEqual(get_proxy_info("127.0.0.1", False), (None, 0, None)) - self.assertEqual(get_proxy_info("192.168.1.1", False), (None, 0, None)) + def testBadURLs(self): + websock3 = ws.WebSocket() + self.assertRaises(ValueError, websock3.connect, "ws//example.com") + self.assertRaises(ws.WebSocketAddressException, websock3.connect, "ws://example") + self.assertRaises(ValueError, websock3.connect, "example.com") if __name__ == "__main__":