diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..38ac371 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest tox tox-gh-actions CherryPy gevent tornado + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with tox + run: | + tox diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..7eef7cb --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,33 @@ +name: Release + +on: + pull_request: + branches-ignore: + - 'master' + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+rc[0-9]+' + +jobs: + release-to-pypi: + runs-on: ubuntu-24.04 + environment: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..f00bd00 --- /dev/null +++ b/.python-version @@ -0,0 +1,5 @@ +3.9.4 +3.8.9 +3.7.10 +3.6.13 +2.7.18 diff --git a/.travis.yml b/.travis.yml index d345f1d..8746ea4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,18 +2,22 @@ language: python python: - 2.7 - - 3.3 - 3.5 + - 3.6 + - 3.7 + - 3.8 + - 3.9 + before_install: - sudo apt-get install python-dev libevent-dev - pip install Cython install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -r requirements/py2kreqs.txt; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -r requirements/py2kreqs.txt; fi - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then pip install -r requirements/py3kreqs.txt; fi - python setup.py install -script: +script: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then py.test -v; fi - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then py.test -v; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index d26ed44..89bf158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ ## Unreleased [Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.1...master) +## [0.6.0](https://github.com/Lawouach/WebSocket-for-Python/tree/0.6.0) (2024-12-19) +[Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.1...0.6.0) +**Changes:** + + * Upgrade Python support to include 3.6, 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12 + * Drop support for Python 3.* < 3.6 (Python 2.7 remains) + * Fix tox and github action so they work nicely together + * Add a release workflow to Pypi which uses trusted publisher + ## [0.5.1](https://github.com/Lawouach/WebSocket-for-Python/tree/0.5.1) (2018-02-28) [Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.0...0.5.1) **Merged pull requests:** diff --git a/README.md b/README.md index 4645859..97c530f 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,6 @@ Read the [documentation](https://ws4py.readthedocs.org/en/latest/) for more info You can also join the [ws4py mailing-list](http://groups.google.com/group/ws4py) to discuss the library. -**WARNING**: This project is [on hiatus](https://opensource.guide/best-practices/#share-the-workload) -and [does not receive active maintainance](http://www.defuze.org/archives/409-ws4py-is-eager-for-a-new-maintainer.html). -Please be aware of this when deciding to rely on it as contributions may be slow -to make their way to a new release. If you feel like offering help in -maintaining it, [please let us know](https://groups.google.com/forum/#!forum/ws4py). ## Installation diff --git a/docs/sources/basics.rst b/docs/sources/basics.rst index 5d8d271..3bf587a 100644 --- a/docs/sources/basics.rst +++ b/docs/sources/basics.rst @@ -34,7 +34,7 @@ necessarily need a connected socket, in fact, you don't even need a socket at al >>> def data_source(): >>> yield TextMessage(u'hello world') - >>> from mock import MagicMock + >>> from unittest.mock import MagicMock >>> source = MagicMock(side_effect=data_source) >>> ws = EchoWebSocket(sock=source) >>> ws.send(u'hello there') diff --git a/example/basic/app.py b/example/basic/app.py index 7d0745c..c81dcf1 100644 --- a/example/basic/app.py +++ b/example/basic/app.py @@ -24,7 +24,7 @@ cur_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) index_path = os.path.join(cur_dir, 'index.html') -index_page = file(index_path, 'r').read() +index_page = open(index_path, 'r').read() class ChatWebSocketHandler(WebSocket): def received_message(self, m): diff --git a/example/droid_sensor.py b/example/droid_sensor.py index 7f9e8af..3699a50 100644 --- a/example/droid_sensor.py +++ b/example/droid_sensor.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import print_function + __doc__ = """ WebSocket client that pushes Android sensor metrics to the websocket server it is connected to. @@ -64,7 +66,7 @@ def run(self): continue c = lambda rad: rad * 360.0 / math.pi - print c(azimuth), c(pitch), c(roll), x, y, z + print(c(azimuth), c(pitch), c(roll), x, y, z) if self.client.terminated: break diff --git a/example/droid_sensor_cherrypy_server.py b/example/droid_sensor_cherrypy_server.py index b342540..8bd4d28 100644 --- a/example/droid_sensor_cherrypy_server.py +++ b/example/droid_sensor_cherrypy_server.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import print_function + import os.path import cherrypy @@ -8,7 +10,7 @@ class BroadcastWebSocketHandler(WebSocket): def received_message(self, m): cherrypy.engine.publish('websocket-broadcast', str(m)) - + class Root(object): @cherrypy.expose def display(self): @@ -61,7 +63,7 @@ def index(self): """ - + if __name__ == '__main__': cherrypy.config.update({ 'server.socket_host': '0.0.0.0', @@ -69,7 +71,7 @@ def index(self): 'tools.staticdir.root': os.path.abspath(os.path.join(os.path.dirname(__file__), 'static')) } ) - print os.path.abspath(os.path.join(__file__, 'static')) + print(os.path.abspath(os.path.join(__file__, 'static'))) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() diff --git a/example/mocking_data_source.py b/example/mocking_data_source.py index a830475..1fd7ae2 100644 --- a/example/mocking_data_source.py +++ b/example/mocking_data_source.py @@ -30,7 +30,7 @@ def recv(self, size): current_bytes = self.remaining_bytes[:size] self.remaining_bytes = self.remaining_bytes[size:] - if self.remaining_bytes is b'': + if self.remaining_bytes == b'': self.frame = None self.remaining_bytes = None diff --git a/example/websensors/templates/.cache/board.html.py b/example/websensors/templates/.cache/board.html.py index 5bd9f8b..3e73637 100644 --- a/example/websensors/templates/.cache/board.html.py +++ b/example/websensors/templates/.cache/board.html.py @@ -22,9 +22,9 @@ def render_body(context,**pageargs): # SOURCE LINE 1 __M_writer(u'\n\n\n\n \n \n \n \n Mobile remote control\n \n \n \n \n \n\n \n \n \n\n \n \n \n \n\n
\n\t \n\t \n
\n\n \n\t\n \n \n \n \n\n') return '' finally: diff --git a/example/websensors/templates/.cache/index.html.py b/example/websensors/templates/.cache/index.html.py index 28ac0d3..6f8f25a 100644 --- a/example/websensors/templates/.cache/index.html.py +++ b/example/websensors/templates/.cache/index.html.py @@ -21,9 +21,9 @@ def render_body(context,**pageargs): # SOURCE LINE 1 __M_writer(u'\n\n\n\n \n \n \n \n Shared drawing board\n \n \n \n \n \n\n \n \n\n \n \n \n \n\n
\n
\n

shared drawing board

\n
\n
\n\n
\n
\n\n \n\n
\n
\n\n \n\n \n \n \n\n') return '' finally: diff --git a/setup.py b/setup.py index c9452fa..4ff6ede 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,10 @@ from setuptools import setup except ImportError: from distutils.core import setup -from distutils.command.build_py import build_py +try: + from setuptools.command.build_py import build_py +except ImportError: + from distutils.command.build_py import build_py class buildfor2or3(build_py): def find_package_modules(self, package, package_dir): @@ -46,21 +49,24 @@ def find_package_modules(self, package, package_dir): download_url = "https://pypi.python.org/pypi/ws4py", packages = ['ws4py', 'ws4py.client', 'ws4py.server'], platforms = ["any"], - license = 'BSD', + license = 'BSD-3-Clause', long_description = "WebSocket client and server library for Python 2 and 3 as well as PyPy", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: CherryPy', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Communications', diff --git a/test/autobahn_test_servers.py b/test/autobahn_test_servers.py index 3e33f82..fdf8d5d 100644 --- a/test/autobahn_test_servers.py +++ b/test/autobahn_test_servers.py @@ -85,7 +85,7 @@ def run_python3_asyncio(host="127.0.0.1", port=9009): wsaccel.patch_ws4py() from ws4py.async_websocket import EchoWebSocket from ws4py.server.tulipserver import WebSocketProtocol - + loop = asyncio.get_event_loop() def start_server(): @@ -121,14 +121,14 @@ def run_autobahn_server(host="127.0.0.1", port=9003): from twisted.internet import reactor from autobahn.twisted.websocket import WebSocketServerProtocol, \ WebSocketServerFactory - + class MyServerProtocol(WebSocketServerProtocol): def onMessage(self, payload, isBinary): self.sendMessage(payload, isBinary) logger = logging.getLogger('autobahn_testsuite') logger.warning("Serving Autobahn server on %s:%s" % (host, port)) - + factory = WebSocketServerFactory("ws://%s:%d" % (host, port)) factory.protocol = MyServerProtocol @@ -142,7 +142,7 @@ def run_python_wsgi(host="127.0.0.1", port=9002): """ run_python_wsgi_async(host, port, False) -def run_python_wsgi_async(host="127.0.0.1", port=9010, async=True): +def run_python_wsgi_async(host="127.0.0.1", port=9010, async_=True): """ Runs wsgi server on python 2.x with async middleware" """ @@ -153,7 +153,7 @@ def run_python_wsgi_async(host="127.0.0.1", port=9010, async=True): from ws4py.server.wsgiutils import WebSocketWSGIApplication app = WebSocketWSGIApplication(handler_cls=EchoWebSocket) - if async: + if async_: def middleware(app): def later(environ, start_response): for part in app(environ, start_response): diff --git a/test/test_cherrypy.py b/test/test_cherrypy.py index 4d184de..326e5df 100644 --- a/test/test_cherrypy.py +++ b/test/test_cherrypy.py @@ -4,7 +4,10 @@ import time import unittest -from mock import MagicMock, call +try: + from unittest.mock import MagicMock, call +except ImportError: + from mock import MagicMock, call import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool diff --git a/test/test_client.py b/test/test_client.py index 7ad6a4b..bdb8459 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -4,8 +4,10 @@ import socket import time import unittest - -from mock import MagicMock, patch +try: + from unittest.mock import MagicMock, patch +except ImportError: + from mock import MagicMock, patch from ws4py import WS_KEY from ws4py.exc import HandshakeError @@ -104,6 +106,15 @@ def test_parse_wss_scheme_with_query_string(self): self.assertEqual(c.resource, "/?token=value") self.assertEqual(c.bind_addr, ("127.0.0.1", 443)) + def test_overriding_host_from_headers(self): + c = WebSocketBaseClient(url="wss://127.0.0.1", headers=[("Host", "example123.com")]) + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 443) + self.assertEqual(c.bind_addr, ("127.0.0.1", 443)) + for h in c.handshake_headers: + if h[0].lower() == "host": + self.assertEqual(h[1], "example123.com") + @patch('ws4py.client.socket') def test_connect_and_close(self, sock): diff --git a/test/test_logger.py b/test/test_logger.py index a39e56d..5e84624 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import logging +import logging import logging.handlers as handlers import os, os.path import unittest @@ -15,27 +15,31 @@ def clean_logger(): except KeyError: pass logger.removeHandler(handler) - + class WSTestLogger(unittest.TestCase): + LOG_FILE = './my.log' + def tearDown(self): clean_logger() - + if os.path.exists(self.LOG_FILE): + os.remove(self.LOG_FILE) + def test_named_logger(self): - logger = configure_logger(stdout=False, filepath='./my.log') + logger = configure_logger(stdout=False, filepath=self.LOG_FILE) logger = logging.getLogger('ws4py') self.assertEqual(logger.getEffectiveLevel(), logging.INFO) - + def test_level(self): - logger = configure_logger(stdout=True, filepath='./my.log', + logger = configure_logger(stdout=True, filepath=self.LOG_FILE, level=logging.DEBUG) self.assertEqual(logger.getEffectiveLevel(), logging.DEBUG) for handler in logger.handlers: self.assertEqual(handler.level, logging.DEBUG) - + def test_file_logger(self): - filepath = os.path.abspath('./my.log') + filepath = os.path.abspath(self.LOG_FILE) logger = configure_logger(stdout=False, filepath=filepath) for handler in logger.handlers: if isinstance(handler, handlers.RotatingFileHandler): diff --git a/test/test_manager.py b/test/test_manager.py index 126c714..9a8d347 100644 --- a/test/test_manager.py +++ b/test/test_manager.py @@ -8,7 +8,10 @@ except ImportError: from itertools import zip_longest -from mock import MagicMock, call, patch +try: + from unittest.mock import MagicMock, call, patch +except ImportError: + from mock import MagicMock, call, patch from ws4py.manager import WebSocketManager, SelectPoller,\ EPollPoller diff --git a/test/test_utils.py b/test/test_utils.py index 4f21afd..3e48aef 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,9 +1,14 @@ # -*- coding: utf-8 -*- import unittest +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock + from ws4py import format_addresses from ws4py.websocket import WebSocket -from mock import MagicMock + class WSUtilities(unittest.TestCase): def test_format_address(self): @@ -14,7 +19,7 @@ def test_format_address(self): log = format_addresses(ws) self.assertEqual(log, "[Local => 127.0.0.1:52300 | Remote => 127.0.0.1:4800]") - + if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() diff --git a/test/test_websocket.py b/test/test_websocket.py index 96fb384..29e451f 100644 --- a/test/test_websocket.py +++ b/test/test_websocket.py @@ -4,7 +4,13 @@ import socket import struct -from mock import MagicMock, call, patch +try: + from io import BytesIO + from unittest.mock import MagicMock, call, patch +except ImportError: + from StringIO import StringIO as BytesIO + from mock import MagicMock, call, patch + from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ @@ -177,6 +183,47 @@ def test_sending_ping(self): ws.ping("hello") m.sendall.assert_called_once_with(tm) + def test_spill_frame(self): + data = b"hello" + buf = BytesIO(data + b"spillover") + + sock = MagicMock() + sock._ssl = object() # for WebSocket._is_secure logic + sock.recv.side_effect = buf.read + sock.pending.side_effect = lambda: buf.tell() < len(buf.getvalue()) + + ws = WebSocket(sock=sock) + ws.stream = MagicMock() + + self.assertTrue(ws._is_secure) + + ws.reading_buffer_size = len(data) + ws.once() + + ws.stream.parser.send.assert_called_once_with(data) + + + @patch("ws4py.websocket.Heartbeat") + def test_run(self, mocker): + mocked_sock = MagicMock() + mocked_opened = MagicMock() + mocked_once = MagicMock(return_value=False) # False to break the loop + mocked_terminate = MagicMock() + + ws = WebSocket(sock=mocked_sock) + assert ws.sock_timeout is None + + with patch.multiple(ws, + opened=mocked_opened, + once=mocked_once, + terminate=mocked_terminate, + stream=MagicMock(), + ): + ws.run() + mocked_sock.settimeout.assert_called_with(None) + mocked_opened.assert_called() + mocked_once.assert_called() + mocked_terminate.assert_called() if __name__ == '__main__': suite = unittest.TestSuite() diff --git a/tox.ini b/tox.ini index 6621a19..525727c 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,21 @@ # and then run "tox" from this directory. [tox] -envlist = py27 +envlist = py27,py36,py37,py38,py39,py310,py311,py312 + +[gh-actions] +python = + 2.7: py27 + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 [testenv] -commands = python setup.py test +commands = pytest +deps = + py27: -r requirements/py2kreqs.txt + {py36,py37,py38,py39,py310,py311,py312}: -r requirements/py3kreqs.txt diff --git a/ws4py/__init__.py b/ws4py/__init__.py index 5f01c50..c36b838 100644 --- a/ws4py/__init__.py +++ b/ws4py/__init__.py @@ -30,7 +30,7 @@ import logging.handlers as handlers __author__ = "Sylvain Hellegouarch" -__version__ = "0.5.1" +__version__ = "0.6.0" __all__ = ['WS_KEY', 'WS_VERSION', 'configure_logger', 'format_addresses'] WS_KEY = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" diff --git a/ws4py/_asyncio_compat.py b/ws4py/_asyncio_compat.py new file mode 100644 index 0000000..baec0cd --- /dev/null +++ b/ws4py/_asyncio_compat.py @@ -0,0 +1,9 @@ +"""Provide compatibility over different versions of asyncio.""" + +import asyncio + +if hasattr(asyncio, "async"): + # Compatibility for Python 3.3 and older + ensure_future = getattr(asyncio, "async") +else: + ensure_future = asyncio.ensure_future \ No newline at end of file diff --git a/ws4py/async_websocket.py b/ws4py/async_websocket.py index 9e2a4c7..177f471 100644 --- a/ws4py/async_websocket.py +++ b/ws4py/async_websocket.py @@ -19,6 +19,7 @@ import types from ws4py.websocket import WebSocket as _WebSocket +from ws4py import _asyncio_compat from ws4py.messaging import Message __all__ = ['WebSocket', 'EchoWebSocket'] @@ -84,7 +85,7 @@ def close_connection(self): def closeit(): yield from self.proto.writer.drain() self.proto.writer.close() - asyncio.async(closeit()) + _asyncio_compat.ensure_future(closeit()) def _write(self, data): """ @@ -94,7 +95,7 @@ def _write(self, data): def sendit(data): self.proto.writer.write(data) yield from self.proto.writer.drain() - asyncio.async(sendit(data)) + _asyncio_compat.ensure_future(sendit(data)) @asyncio.coroutine def run(self): diff --git a/ws4py/client/__init__.py b/ws4py/client/__init__.py index 411638f..4bc7c1a 100644 --- a/ws4py/client/__init__.py +++ b/ws4py/client/__init__.py @@ -81,11 +81,6 @@ def __init__(self, url, protocols=None, extensions=None, self.exclude_headers = exclude_headers or [] self.exclude_headers = [x.lower() for x in self.exclude_headers] - if self.scheme == "wss": - # Prevent check_hostname requires server_hostname (ref #187) - if "cert_reqs" not in self.ssl_options: - self.ssl_options["cert_reqs"] = ssl.CERT_NONE - self._parse_url() if self.unix_socket_path: @@ -211,7 +206,20 @@ def connect(self): """ if self.scheme == "wss": # default port is now 443; upgrade self.sender to send ssl - self.sock = ssl.wrap_socket(self.sock, **self.ssl_options) + protocol = getattr(ssl, 'PROTOCOL_TLS_CLIENT', ssl.PROTOCOL_TLSv1_2) # PROTOCOL_TLS_CLIENT is correct for newer Python but doesn't exist on older Pythons + context = ssl.SSLContext(protocol) + if self.ssl_options.get('certfile', None): + context.load_cert_chain(self.ssl_options.get('certfile'), self.ssl_options.get('keyfile')) + + if self.ssl_options.get('ca_certs'): + context.load_verify_locations(self.ssl_options['ca_certs']) + + # Prevent check_hostname requires server_hostname (ref #187) + if "cert_reqs" not in self.ssl_options: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + self.sock = context.wrap_socket(self.sock, server_hostname=self.host) self._is_secure = True self.sock.connect(self.bind_addr) @@ -253,7 +261,6 @@ def handshake_headers(self): handshake. """ headers = [ - ('Host', '%s:%s' % (self.host, self.port)), ('Connection', 'Upgrade'), ('Upgrade', 'websocket'), ('Sec-WebSocket-Key', self.key.decode('utf-8')), @@ -266,6 +273,12 @@ def handshake_headers(self): if self.extra_headers: headers.extend(self.extra_headers) + # keep old logic if no overriding Host in headers + if not any(x for x in headers if x[0].lower() == 'host') and \ + 'host' not in self.exclude_headers: + headers.append(('Host', '%s:%s' % (self.host, self.port))) + + if not any(x for x in headers if x[0].lower() == 'origin') and \ 'origin' not in self.exclude_headers: diff --git a/ws4py/compat.py b/ws4py/compat.py index e986e33..5b8870d 100644 --- a/ws4py/compat.py +++ b/ws4py/compat.py @@ -34,7 +34,7 @@ def ord(c): else: py3k = False from urlparse import urlsplit - range = xrange + range = xrange # noqa: F821 unicode = unicode basestring = basestring ord = ord diff --git a/ws4py/server/cherrypyserver.py b/ws4py/server/cherrypyserver.py index 5b93465..49ffebf 100644 --- a/ws4py/server/cherrypyserver.py +++ b/ws4py/server/cherrypyserver.py @@ -379,4 +379,4 @@ def index(self): cherrypy.log("Handler created: %s" % repr(cherrypy.request.ws_handler)) cherrypy.quickstart(Root(), '/', config={'/': {'tools.websocket.on': True, - 'tools.websocket.handler_cls': EchoWebSocketHandler}}) + 'tools.websocket.handler_cls': EchoWebSocketHandler}}) # noqa: F821 diff --git a/ws4py/server/tulipserver.py b/ws4py/server/tulipserver.py index 2786c16..fdf749c 100644 --- a/ws4py/server/tulipserver.py +++ b/ws4py/server/tulipserver.py @@ -9,6 +9,7 @@ from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError from ws4py.websocket import WebSocket +from ws4py import _asyncio_compat LF = b'\n' CRLF = b'\r\n' @@ -25,7 +26,7 @@ def __init__(self, handler_cls): def _pseudo_connected(self, reader, writer): pass - + def connection_made(self, transport): """ A peer is now connected and we receive an instance @@ -40,17 +41,17 @@ def connection_made(self, transport): #self.stream.set_transport(transport) asyncio.StreamReaderProtocol.connection_made(self, transport) # Let make it concurrent for others to tag along - f = asyncio.async(self.handle_initial_handshake()) + f = _asyncio_compat.ensure_future(self.handle_initial_handshake()) f.add_done_callback(self.terminated) @property def writer(self): return self._stream_writer - + @property def reader(self): return self._stream_reader - + def terminated(self, f): if f.done() and not f.cancelled(): ex = f.exception() @@ -70,12 +71,12 @@ def close(self): transport. """ self.ws.close() - + def timeout(self): self.ws.close_connection() if self.ws.started: self.ws.closed(1002, "Peer connection timed-out") - + def connection_lost(self, exc): """ The peer connection is now, the closing @@ -88,7 +89,7 @@ def connection_lost(self, exc): self.ws.close_connection() if self.ws.started: self.ws.closed(1002, "Peer connection was lost") - + @asyncio.coroutine def handle_initial_handshake(self): """ @@ -100,15 +101,15 @@ def handle_initial_handshake(self): """ request_line = yield from self.next_line() method, uri, req_protocol = request_line.strip().split(SPACE, 2) - + # GET required if method.upper() != b'GET': raise HandshakeError('HTTP method must be a GET') - + headers = yield from self.read_headers() if req_protocol == b'HTTP/1.1' and 'Host' not in headers: raise ValueError("Missing host header") - + for key, expected_value in [('Upgrade', 'websocket'), ('Connection', 'upgrade')]: actual_value = headers.get(key, '').lower() @@ -160,7 +161,7 @@ def handle_initial_handshake(self): self.ws.protocols = ws_protocols self.ws.extensions = ws_extensions self.ws.headers = headers - + response = [req_protocol + b' 101 Switching Protocols'] response.append(b'Upgrade: websocket') response.append(b'Content-Type: text/plain') @@ -184,7 +185,7 @@ def handle_websocket(self): exchange is completed and terminated. """ yield from self.ws.run() - + @asyncio.coroutine def read_headers(self): """ @@ -198,21 +199,21 @@ def read_headers(self): if line == CRLF: break return BytesHeaderParser().parsebytes(headers) - + @asyncio.coroutine def next_line(self): """ Reads data until \r\n is met and then return all read - bytes. + bytes. """ line = yield from self.reader.readline() if not line.endswith(CRLF): raise ValueError("Missing mandatory trailing CRLF") return line - + if __name__ == '__main__': from ws4py.async_websocket import EchoWebSocket - + loop = asyncio.get_event_loop() def start_server(): diff --git a/ws4py/websocket.py b/ws4py/websocket.py index 61f8c33..4dd6259 100644 --- a/ws4py/websocket.py +++ b/ws4py/websocket.py @@ -141,6 +141,17 @@ def __init__(self, sock, protocols=None, extensions=None, environ=None, heartbea "Internal buffer to get around SSL problems" self.buf = b'' + self.sock_timeout = None + """ + Used to set socket.settimeout(value): + From: https://docs.python.org/3.11/library/socket.html#socket.socket.settimeout + The value argument can be a nonnegative floating point number expressing seconds, or None. + If a non-zero value is given, subsequent socket operations will raise a timeout exception + if the timeout period value has elapsed before the operation has completed. + If zero is given, the socket is put in non-blocking mode. + If None is given, the socket is put in blocking mode. + """ + self._local_address = None self._peer_address = None @@ -223,11 +234,14 @@ def close_connection(self): if self.sock: try: self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + try: self.sock.close() except: pass - finally: - self.sock = None + self.sock = None + def ping(self, message): """ @@ -440,7 +454,7 @@ def terminate(self): self.stream = None self.environ = None - def process(self, bytes): + def process(self, data): """ Takes some bytes and process them through the internal stream's parser. If a message of any kind is found, performs one of these actions: @@ -456,13 +470,13 @@ def process(self, bytes): """ s = self.stream - if not bytes and self.reading_buffer_size > 0: + if not data and self.reading_buffer_size > 0: return False - self.reading_buffer_size = s.parser.send(bytes) or DEFAULT_READING_SIZE + self.reading_buffer_size = s.parser.send(data) or DEFAULT_READING_SIZE if s.closing is not None: - logger.debug("Closing message received (%d) '%s'" % (s.closing.code, s.closing.reason)) + logger.debug("Closing message received (%d): %s" % (s.closing.code, s.closing.reason.decode() if isinstance(s.closing.reason, bytes) else s.closing.reason)) if not self.server_terminated: self.close(s.closing.code, s.closing.reason) else: @@ -471,7 +485,7 @@ def process(self, bytes): if s.errors: for error in s.errors: - logger.debug("Error message received (%d) '%s'" % (error.code, error.reason)) + logger.debug("Error message received (%d): %s" % (error.code, error.reason.decode() if isinstance(error.reason, bytes) else error.reason)) self.close(error.code, error.reason) s.errors = [] return False @@ -515,10 +529,12 @@ def run(self): we initiate the closing of the connection with the appropiate error code. - This method is blocking and should likely be run - in a thread. + The self.sock_timeout determines whether this method + is blocking, or can timeout on reads. If a timeout + occurs, the unhandled_error function will be called + It should likely be run in a thread. """ - self.sock.setblocking(True) + self.sock.settimeout(self.sock_timeout) with Heartbeat(self, frequency=self.heartbeat_freq): s = self.stream