././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3663085 python_engineio-4.12.1/0000775000175000017500000000000015010141646014363 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/LICENSE0000664000175000017500000000207214707424343015403 0ustar00miguelmiguelThe MIT License (MIT) Copyright (c) 2015 Miguel Grinberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/MANIFEST.in0000664000175000017500000000011014707424343016123 0ustar00miguelmiguelinclude README.md LICENSE tox.ini tests/**/* docs/**/* exclude **/*.pyc ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3663085 python_engineio-4.12.1/PKG-INFO0000644000175000017500000000425515010141646015464 0ustar00miguelmiguelMetadata-Version: 2.4 Name: python-engineio Version: 4.12.1 Summary: Engine.IO server and client for Python Author-email: Miguel Grinberg License: MIT Project-URL: Homepage, https://github.com/miguelgrinberg/python-engineio Project-URL: Bug Tracker, https://github.com/miguelgrinberg/python-engineio/issues Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 3 Classifier: Operating System :: OS Independent Requires-Python: >=3.6 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: simple-websocket>=0.10.0 Provides-Extra: client Requires-Dist: requests>=2.21.0; extra == "client" Requires-Dist: websocket-client>=0.54.0; extra == "client" Provides-Extra: asyncio-client Requires-Dist: aiohttp>=3.4; extra == "asyncio-client" Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Dynamic: license-file python-engineio =============== [![Build status](https://github.com/miguelgrinberg/python-engineio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-engineio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-engineio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-engineio) Python implementation of the `Engine.IO` realtime client and server. Sponsors -------- The following organizations are funding this project: ![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| -|- Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? Resources --------- - [Documentation](https://python-engineio.readthedocs.io/) - [PyPI](https://pypi.python.org/pypi/python-engineio) - [Change Log](https://github.com/miguelgrinberg/python-engineio/blob/main/CHANGES.md) - Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/README.md0000664000175000017500000000237514707424343015663 0ustar00miguelmiguelpython-engineio =============== [![Build status](https://github.com/miguelgrinberg/python-engineio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-engineio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-engineio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-engineio) Python implementation of the `Engine.IO` realtime client and server. Sponsors -------- The following organizations are funding this project: ![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| -|- Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? Resources --------- - [Documentation](https://python-engineio.readthedocs.io/) - [PyPI](https://pypi.python.org/pypi/python-engineio) - [Change Log](https://github.com/miguelgrinberg/python-engineio/blob/main/CHANGES.md) - Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3623085 python_engineio-4.12.1/docs/0000775000175000017500000000000015010141646015313 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/docs/Makefile0000664000175000017500000000110414707424343016761 0ustar00miguelmiguel# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3623085 python_engineio-4.12.1/docs/_static/0000775000175000017500000000000015010141646016741 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/docs/_static/README.md0000664000175000017500000000006314707424343020231 0ustar00miguelmiguelPlace static files used by the documentation here. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734262534.0 python_engineio-4.12.1/docs/api.rst0000664000175000017500000000067114727537406016643 0ustar00miguelmiguelAPI Reference ============= .. toctree:: :maxdepth: 3 .. module:: engineio .. autoclass:: Client :members: :inherited-members: .. autoclass:: AsyncClient :members: :inherited-members: .. autoclass:: Server :members: :inherited-members: .. autoclass:: AsyncServer :members: :inherited-members: .. autoclass:: WSGIApp :members: .. autoclass:: ASGIApp :members: .. autoclass:: Middleware :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/docs/client.rst0000664000175000017500000001366314730320266017341 0ustar00miguelmiguelThe Engine.IO Client ==================== This package contains two Engine.IO clients: - The :func:`engineio.Client` class creates a client compatible with the standard Python library. - The :func:`engineio.AsyncClient` class creates a client compatible with the ``asyncio`` package. The methods in the two clients are the same, with the only difference that in the ``asyncio`` client most methods are implemented as coroutines. Installation ------------ To install the standard Python client along with its dependencies, use the following command:: pip install "python-engineio[client]" If instead you plan on using the ``asyncio`` client, then use this:: pip install "python-engineio[asyncio_client]" Creating a Client Instance -------------------------- To instantiate an Engine.IO client, simply create an instance of the appropriate client class:: import engineio # standard Python eio = engineio.Client() # asyncio eio = engineio.AsyncClient() Defining Event Handlers ----------------------- To responds to events triggered by the connection or the server, event Handler functions must be defined using the ``on`` decorator:: @eio.on('connect') def on_connect(): print('I'm connected!') @eio.on('message') def on_message(data): print('I received a message!') @eio.on('disconnect') def on_disconnect(reason): print('I'm disconnected! reason:', reason) For the ``asyncio`` server, event handlers can be regular functions as above, or can also be coroutines:: @eio.on('message') async def on_message(data): print('I received a message!') The argument given to the ``on`` decorator is the event name. The events that are supported are ``connect``, ``message`` and ``disconnect``. The ``data`` argument passed to the ``'message'`` event handler contains application-specific data provided by the server with the event. The ``disconnect`` handler is invoked for client initiated disconnects, server initiated disconnects, or accidental disconnects, for example due to networking failures. The argument passed to this handler provides the disconnect reason. Example:: @eio.on('disconnect') def on_disconnect(reason): if reason == eio.reason.CLIENT_DISCONNECT: print('client disconnection') elif reason == eio.reason.SERVER_DISCONNECT: print('the server kicked me out') else: print(f'disconnect reason: {reason}') Connecting to a Server ---------------------- The connection to a server is established by calling the ``connect()`` method:: eio.connect('http://localhost:5000') In the case of the ``asyncio`` client, the method is a coroutine:: await eio.connect('http://localhost:5000') Upon connection, the server assigns the client a unique session identifier. The applicaction can find this identifier in the ``sid`` attribute:: print('my sid is', eio.sid) Sending Messages ---------------- The client can send a message to the server using the ``send()`` method:: eio.send({'foo': 'bar'}) Or in the case of ``asyncio``, as a coroutine:: await eio.send({'foo': 'bar'}) The single argument provided to the method is the data that is passed on to the server. The data can be of type ``str``, ``bytes``, ``dict`` or ``list``. The data included inside dictionaries and lists is also constrained to these types. The ``send()`` method can be invoked inside an event handler as a response to a server event, or in any other part of the application, including in background tasks. Disconnecting from the Server ----------------------------- At any time the client can request to be disconnected from the server by invoking the ``disconnect()`` method:: eio.disconnect() For the ``asyncio`` client this is a coroutine:: await eio.disconnect() Managing Background Tasks ------------------------- When a client connection to the server is established, a few background tasks will be spawned to keep the connection alive and handle incoming events. The application running on the main thread is free to do any work, as this is not going to prevent the functioning of the Engine.IO client. If the application does not have anything to do in the main thread and just wants to wait until the connection ends, it can call the ``wait()`` method:: eio.wait() Or in the ``asyncio`` version:: await eio.wait() For the convenience of the application, a helper function is provided to start a custom background task:: def my_background_task(my_argument) # do some background work here! pass eio.start_background_task(my_background_task, 123) The arguments passed to this method are the background function and any positional or keyword arguments to invoke the function with. Here is the ``asyncio`` version:: async def my_background_task(my_argument) # do some background work here! pass eio.start_background_task(my_background_task, 123) Note that this function is not a coroutine, since it does not wait for the background function to end, but the background function is. The ``sleep()`` method is a second convenience function that is provided for the benefit of applications working with background tasks of their own:: eio.sleep(2) Or for ``asyncio``:: await eio.sleep(2) The single argument passed to the method is the number of seconds to sleep for. Debugging and Troubleshooting ----------------------------- To help you debug issues, the client can be configured to output logs to the terminal:: import engineio # standard Python eio = engineio.Client(logger=True) # asyncio eio = engineio.AsyncClient(logger=True) The ``logger`` argument can be set to ``True`` to output logs to ``stderr``, or to an object compatible with Python's ``logging`` package where the logs should be emitted to. A value of ``False`` disables logging. Logging can help identify the cause of connection problems, unexpected disconnections and other issues. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/docs/conf.py0000664000175000017500000001260714726642012016626 0ustar00miguelmiguel# # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'python-engineio' copyright = '2018, Miguel Grinberg' author = 'Miguel Grinberg' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] autodoc_member_order = 'bysource' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { 'github_user': 'miguelgrinberg', 'github_repo': 'python-engineio', 'github_banner': True, 'github_button': True, 'github_type': 'star', 'fixed_sidebar': True, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'python-engineiodoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'python-engineio.tex', 'python-engineio Documentation', 'Miguel Grinberg', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'python-engineio', 'python-engineio Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'python-engineio', 'python-engineio Documentation', author, 'python-engineio', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/docs/index.rst0000664000175000017500000000103414707424343017164 0ustar00miguelmiguel.. python-engineio documentation master file, created by sphinx-quickstart on Sat Nov 24 09:42:25 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. python-engineio =============== This project implements Python based Engine.IO client and server that can run standalone or integrated with a variety of Python web frameworks and applications. .. toctree:: :maxdepth: 3 intro client server api * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/docs/intro.rst0000664000175000017500000001173614707424343017222 0ustar00miguelmiguel.. engineio documentation master file, created by sphinx-quickstart on Sat Jun 13 23:41:23 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Getting Started =============== What is Engine.IO? ------------------ Engine.IO is a lightweight transport protocol that enables real-time bidirectional event-based communication between clients (typically, though not always, web browsers) and a server. The official implementations of the client and server components are written in JavaScript. This package provides Python implementations of both, each with standard and ``asyncio`` variants. The Engine.IO protocol is extremely simple. Once a connection between a client and a server is established, either side can send "messages" to the other side. Event handlers provided by the applications on both ends are invoked when a message is received, or when a connection is established or dropped. Client Examples --------------- The example that follows shows a simple Python client:: import engineio eio = engineio.Client() @eio.on('connect') def on_connect(): print('connection established') @eio.on('message') def on_message(data): print('message received with ', data) eio.send({'response': 'my response'}) @eio.on('disconnect') def on_disconnect(): print('disconnected from server') eio.connect('http://localhost:5000') eio.wait() And here is a similar client written using the official Engine.IO Javascript client:: Client Features --------------- - Can connect to other Engine.IO complaint servers besides the one in this package. - Compatible with Python 3.6+. - Two versions of the client, one for standard Python and another for ``asyncio``. - Uses an event-based architecture implemented with decorators that hides the details of the protocol. - Implements HTTP long-polling and WebSocket transports. Server Examples --------------- The following application is a basic example that uses the Eventlet asynchronous server:: import engineio import eventlet eio = engineio.Server() app = engineio.WSGIApp(eio, static_files={ '/': {'content_type': 'text/html', 'filename': 'index.html'} }) @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') def message(sid, data): print("message ", data) eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) if __name__ == '__main__': eventlet.wsgi.server(eventlet.listen(('', 5000)), app) Below is a similar application, coded for asyncio and the Uvicorn web server:: import engineio import uvicorn eio = engineio.AsyncServer() app = engineio.ASGIApp(eio, static_files={ '/': {'content_type': 'text/html', 'filename': 'index.html'} }) @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') async def message(sid, data): print("message ", data) await eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) if __name__ == '__main__': uvicorn.run('127.0.0.1', 5000) Server Features --------------- - Can accept clients running other complaint Engine.IO clients besides the one in this package. - Compatible with Python 3.6+. - Two versions of the server, one for standard Python and another for ``asyncio``. - Supports large number of clients even on modest hardware due to being asynchronous. - Can be hosted on any `WSGI `_ and `ASGI `_ web servers includind `Gunicorn `_, `Uvicorn `_, `eventlet `_ and `gevent `_. - Can be integrated with WSGI applications written in frameworks such as Flask, Django, etc. - Can be integrated with `aiohttp `_, `sanic `_ and `tornado `_ ``asyncio`` applications. - Uses an event-based architecture implemented with decorators that hides the details of the protocol. - Implements HTTP long-polling and WebSocket transports. - Supports XHR2 and XHR browsers as clients. - Supports text and binary messages. - Supports gzip and deflate HTTP compression. - Configurable CORS responses to avoid cross-origin problems with browsers. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/docs/make.bat0000664000175000017500000000142314707424343016732 0ustar00miguelmiguel@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/docs/server.rst0000664000175000017500000006004514730320266017365 0ustar00miguelmiguelThe Engine.IO Server ==================== This package contains two Engine.IO servers: - The :func:`engineio.Server` class creates a server compatible with the standard Python library. - The :func:`engineio.AsyncServer` class creates a server compatible with the ``asyncio`` package. The methods in the two servers are the same, with the only difference that in the ``asyncio`` server most methods are implemented as coroutines. Installation ------------ To install the Python Engine.IO server use the following command:: pip install "python-engineio" In addition to the server, you will need to select an asynchronous framework or server to use along with it. The list of supported packages is covered in the :ref:`deployment-strategies` section. Creating a Server Instance -------------------------- An Engine.IO server is an instance of class :class:`engineio.Server`. This instance can be transformed into a standard WSGI application by wrapping it with the :class:`engineio.WSGIApp` class:: import engineio # create a Engine.IO server eio = engineio.Server() # wrap with a WSGI application app = engineio.WSGIApp(eio) For asyncio based servers, the :class:`engineio.AsyncServer` class provides the same functionality, but in a coroutine friendly format. If desired, The :class:`engineio.ASGIApp` class can transform the server into a standard ASGI application:: # create a Engine.IO server eio = engineio.AsyncServer() # wrap with ASGI application app = engineio.ASGIApp(eio) These two wrappers can also act as middlewares, forwarding any traffic that is not intended to the Engine.IO server to another application. This allows Engine.IO servers to integrate easily into existing WSGI or ASGI applications:: from wsgi import app # a Flask, Django, etc. application app = engineio.WSGIApp(eio, app) Serving Static Files -------------------- The Engine.IO server can be configured to serve static files to clients. This is particularly useful to deliver HTML, CSS and JavaScript files to clients when this package is used without a companion web framework. Static files are configured with a Python dictionary in which each key/value pair is a static file mapping rule. In its simplest form, this dictionary has one or more static file URLs as keys, and the corresponding files in the server as values:: static_files = { '/': 'latency.html', '/static/engine.io.js': 'static/engine.io.js', '/static/style.css': 'static/style.css', } With this example configuration, when the server receives a request for ``/`` (the root URL) it will return the contents of the file ``latency.html`` in the current directory, and will assign a content type based on the file extension, in this case ``text/html``. Files with the ``.html``, ``.css``, ``.js``, ``.json``, ``.jpg``, ``.png``, ``.gif`` and ``.txt`` file extensions are automatically recognized and assigned the correct content type. For files with other file extensions or with no file extension, the ``application/octet-stream`` content type is used as a default. If desired, an explicit content type for a static file can be given as follows:: static_files = { '/': {'filename': 'latency.html', 'content_type': 'text/plain'}, } It is also possible to configure an entire directory in a single rule, so that all the files in it are served as static files:: static_files = { '/static': './public', } In this example any files with URLs starting with ``/static`` will be served directly from the ``public`` folder in the current directory, so for example, the URL ``/static/index.html`` will return local file ``./public/index.html`` and the URL ``/static/css/styles.css`` will return local file ``./public/css/styles.css``. If a URL that ends in a ``/`` is requested, then a default filename of ``index.html`` is appended to it. In the previous example, a request for the ``/static/`` URL would return local file ``./public/index.html``. The default filename to serve for slash-ending URLs can be set in the static files dictionary with an empty key:: static_files = { '/static': './public', '': 'image.gif', } With this configuration, a request for ``/static/`` would return local file ``./public/image.gif``. A non-standard content type can also be specified if needed:: static_files = { '/static': './public', '': {'filename': 'image.gif', 'content_type': 'text/plain'}, } The static file configuration dictionary is given as the ``static_files`` argument to the ``engineio.WSGIApp`` or ``engineio.ASGIApp`` classes:: # for standard WSGI applications eio = engineio.Server() app = engineio.WSGIApp(eio, static_files=static_files) # for asyncio-based ASGI applications eio = engineio.AsyncServer() app = engineio.ASGIApp(eio, static_files=static_files) The routing precedence in these two classes is as follows: - First, the path is checked against the Engine.IO path. - Next, the path is checked against the static file configuration, if present. - If the path did not match the Engine.IO path or any static file, control is passed to the secondary application if configured, else a 404 error is returned. Note: static file serving is intended for development use only, and as such it lacks important features such as caching. Do not use in a production environment. Defining Event Handlers ----------------------- To responds to events triggered by the connection or the client, event Handler functions must be defined using the ``on`` decorator:: @eio.on('connect') def on_connect(sid): print('A client connected!') @eio.on('message') def on_message(sid, data): print('I received a message!') @eio.on('disconnect') def on_disconnect(sid, reason): print('Client disconnected! reason:', reason) For the ``asyncio`` server, event handlers can be regular functions as above, or can also be coroutines:: @eio.on('message') async def on_message(sid, data): print('I received a message!') The argument given to the ``on`` decorator is the event name. The events that are supported are ``connect``, ``message`` and ``disconnect``. The ``sid`` argument passed into all the event handlers is a connection identifier for the client. All the events from a client will use the same ``sid`` value. The ``connect`` handler is the place where the server can perform authentication. The value returned by this handler is used to determine if the connection is accepted or rejected. When the handler does not return any value (which is the same as returning ``None``) or when it returns ``True`` the connection is accepted. If the handler returns ``False`` or any JSON compatible data type (string, integer, list or dictionary) the connection is rejected. A rejected connection triggers a response with a 401 status code. The ``data`` argument passed to the ``'message'`` event handler contains application-specific data provided by the client with the event. The ``disconnect`` handler is invoked for client initiated disconnects, server initiated disconnects, or accidental disconnects, for example due to networking failures. The second argument passed to this handler provides the disconnect reason. Example:: @eio.on('disconnect') def on_disconnect(sid, reason): if reason == eio.reason.CLIENT_DISCONNECT: print('the client went away') elif reason == eio.reason.SERVER_DISCONNECT: print('the client was kicked out') else: print(f'disconnect reason: {reason}') Sending Messages ---------------- The server can send a message to any client using the ``send()`` method:: eio.send(sid, {'foo': 'bar'}) Or in the case of ``asyncio``, as a coroutine:: await eio.send(sid, {'foo': 'bar'}) The first argument provided to the method is the connection identifier for the recipient client. The second argument is the data that is passed on to the server. The data can be of type ``str``, ``bytes``, ``dict`` or ``list``. The data included inside dictionaries and lists is also constrained to these types. The ``send()`` method can be invoked inside an event handler as a response to a client event, or in any other part of the application, including in background tasks. User Sessions ------------- The server can maintain application-specific information in a user session dedicated to each connected client. Applications can use the user session to write any details about the user that need to be preserved throughout the life of the connection, such as usernames or user ids. The ``save_session()`` and ``get_session()`` methods are used to store and retrieve information in the user session:: @eio.on('connect') def on_connect(sid, environ): username = authenticate_user(environ) eio.save_session(sid, {'username': username}) @eio.on('message') def on_message(sid, data): session = eio.get_session(sid) print('message from ', session['username']) For the ``asyncio`` server, these methods are coroutines:: @eio.on('connect') async def on_connect(sid, environ): username = authenticate_user(environ) await eio.save_session(sid, {'username': username}) @eio.on('message') async def on_message(sid, data): session = await eio.get_session(sid) print('message from ', session['username']) The session can also be manipulated with the `session()` context manager:: @eio.on('connect') def on_connect(sid, environ): username = authenticate_user(environ) with eio.session(sid) as session: session['username'] = username @eio.on('message') def on_message(sid, data): with eio.session(sid) as session: print('message from ', session['username']) For the ``asyncio`` server, an asynchronous context manager is used:: @eio.on('connect') def on_connect(sid, environ): username = authenticate_user(environ) async with eio.session(sid) as session: session['username'] = username @eio.on('message') def on_message(sid, data): async with eio.session(sid) as session: print('message from ', session['username']) Note: the contents of the user session are destroyed when the client disconnects. Disconnecting a Client ---------------------- At any time the server can disconnect a client from the server by invoking the ``disconnect()`` method and passing the ``sid`` value assigned to the client:: eio.disconnect(sid) For the ``asyncio`` client this is a coroutine:: await eio.disconnect(sid) Managing Background Tasks ------------------------- For the convenience of the application, a helper function is provided to start a custom background task:: def my_background_task(my_argument) # do some background work here! pass eio.start_background_task(my_background_task, 123) The arguments passed to this method are the background function and any positional or keyword arguments to invoke the function with. Here is the ``asyncio`` version:: async def my_background_task(my_argument) # do some background work here! pass eio.start_background_task(my_background_task, 123) Note that this function is not a coroutine, since it does not wait for the background function to end, but the background function is. The ``sleep()`` method is a second convenience function that is provided for the benefit of applications working with background tasks of their own:: eio.sleep(2) Or for ``asyncio``:: await eio.sleep(2) The single argument passed to the method is the number of seconds to sleep for. Debugging and Troubleshooting ----------------------------- To help you debug issues, the server can be configured to output logs to the terminal:: import engineio # standard Python eio = engineio.Server(logger=True) # asyncio eio = engineio.AsyncServer(logger=True) The ``logger`` argument can be set to ``True`` to output logs to ``stderr``, or to an object compatible with Python's ``logging`` package where the logs should be emitted to. A value of ``False`` disables logging. Logging can help identify the cause of connection problems, 400 responses, bad performance and other issues. .. _deployment-strategies: Deployment Strategies --------------------- The following sections describe a variety of deployment strategies for Engine.IO servers. Uvicorn, Daphne, and other ASGI servers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``engineio.ASGIApp`` class is an ASGI compatible application that can forward Engine.IO traffic to an ``engineio.AsyncServer`` instance:: eio = engineio.AsyncServer(async_mode='asgi') app = engineio.ASGIApp(eio) If desired, the ``engineio.ASGIApp`` class can forward any traffic that is not Engine.IO to another ASGI application, making it possible to deploy a standard ASGI web application and the Engine.IO server as a bundle:: eio = engineio.AsyncServer(async_mode='asgi') app = engineio.ASGIApp(eio, other_app) The ``ASGIApp`` instance is a fully complaint ASGI instance that can be deployed with an ASGI compatible web server. Aiohttp ~~~~~~~ `aiohttp `_ provides a framework with support for HTTP and WebSocket, based on asyncio. Instances of class ``engineio.AsyncServer`` will automatically use aiohttp for asynchronous operations if the library is installed. To request its use explicitly, the ``async_mode`` option can be given in the constructor:: eio = engineio.AsyncServer(async_mode='aiohttp') A server configured for aiohttp must be attached to an existing application:: app = web.Application() eio.attach(app) The aiohttp application can define regular routes that will coexist with the Engine.IO server. A typical pattern is to add routes that serve a client application and any associated static files. The aiohttp application is then executed in the usual manner:: if __name__ == '__main__': web.run_app(app) Tornado ~~~~~~~ `Tornado `_ is a web framework with support for HTTP and WebSocket. Only Tornado version 5 and newer are supported, thanks to its tight integration with asyncio. Instances of class ``engineio.AsyncServer`` will automatically use tornado for asynchronous operations if the library is installed. To request its use explicitly, the ``async_mode`` option can be given in the constructor:: eio = engineio.AsyncServer(async_mode='tornado') A server configured for tornado must include a request handler for Engine.IO:: app = tornado.web.Application( [ (r"/engine.io/", engineio.get_tornado_handler(eio)), ], # ... other application options ) The tornado application can define other routes that will coexist with the Engine.IO server. A typical pattern is to add routes that serve a client application and any associated static files. The tornado application is then executed in the usual manner:: app.listen(port) tornado.ioloop.IOLoop.current().start() Sanic ~~~~~ Note: Due to some backward incompatible changes introduced in recent versions of Sanic, it is currently recommended that a Sanic application is deployed with the ASGI integration instead. `Sanic `_ is a very efficient asynchronous web server for Python. Instances of class ``engineio.AsyncServer`` will automatically use Sanic for asynchronous operations if the framework is installed. To request its use explicitly, the ``async_mode`` option can be given in the constructor:: eio = engineio.AsyncServer(async_mode='sanic') A server configured for Sanic must be attached to an existing application:: app = Sanic() eio.attach(app) The Sanic application can define regular routes that will coexist with the Engine.IO server. A typical pattern is to add routes that serve a client application and any associated static files to this application. The Sanic application is then executed in the usual manner:: if __name__ == '__main__': app.run() It has been reported that the CORS support provided by the Sanic extension `sanic-cors `_ is incompatible with this package's own support for this protocol. To disable CORS support in this package and let Sanic take full control, initialize the server as follows:: eio = engineio.AsyncServer(async_mode='sanic', cors_allowed_origins=[]) On the Sanic side you will need to enable the `CORS_SUPPORTS_CREDENTIALS` setting in addition to any other configuration that you use:: app.config['CORS_SUPPORTS_CREDENTIALS'] = True Eventlet ~~~~~~~~ `Eventlet `_ is a high performance concurrent networking library for Python 2 and 3 that uses coroutines, enabling code to be written in the same style used with the blocking standard library functions. An Engine.IO server deployed with eventlet has access to the long-polling and WebSocket transports. Instances of class ``engineio.Server`` will automatically use eventlet for asynchronous operations if the library is installed. To request its use explicitly, the ``async_mode`` option can be given in the constructor:: eio = engineio.Server(async_mode='eventlet') A server configured for eventlet is deployed as a regular WSGI application using the provided ``engineio.WSGIApp``:: app = engineio.WSGIApp(eio) import eventlet eventlet.wsgi.server(eventlet.listen(('', 8000)), app) Eventlet with Gunicorn ~~~~~~~~~~~~~~~~~~~~~~ An alternative to running the eventlet WSGI server as above is to use `gunicorn `_, a fully featured pure Python web server. The command to launch the application under gunicorn is shown below:: $ gunicorn -k eventlet -w 1 module:app Due to limitations in its load balancing algorithm, gunicorn can only be used with one worker process, so the ``-w 1`` option is required. Note that a single eventlet worker can handle a large number of concurrent clients. Another limitation when using gunicorn is that the WebSocket transport is not available, because this transport it requires extensions to the WSGI standard. Note: Eventlet provides a ``monkey_patch()`` function that replaces all the blocking functions in the standard library with equivalent asynchronous versions. While python-engineio does not require monkey patching, other libraries such as database drivers are likely to require it. Gevent ~~~~~~ `Gevent `_ is another asynchronous framework based on coroutines, very similar to eventlet. An Engine.IO server deployed with gevent has access to the long-polling and websocket transports. Instances of class ``engineio.Server`` will automatically use gevent for asynchronous operations if the library is installed and eventlet is not installed. To request gevent to be selected explicitly, the ``async_mode`` option can be given in the constructor:: eio = engineio.Server(async_mode='gevent') A server configured for gevent is deployed as a regular WSGI application using the provided ``engineio.WSGIApp``:: from gevent import pywsgi app = engineio.WSGIApp(eio) pywsgi.WSGIServer(('', 8000), app).serve_forever() Gevent with Gunicorn ~~~~~~~~~~~~~~~~~~~~ An alternative to running the gevent WSGI server as above is to use `gunicorn `_, a fully featured pure Python web server. The command to launch the application under gunicorn is shown below:: $ gunicorn -k gevent -w 1 module:app Same as with eventlet, due to limitations in its load balancing algorithm, gunicorn can only be used with one worker process, so the ``-w 1`` option is required. Note that a single gevent worker can handle a large number of concurrent clients. Note: Gevent provides a ``monkey_patch()`` function that replaces all the blocking functions in the standard library with equivalent asynchronous versions. While python-engineio does not require monkey patching, other libraries such as database drivers are likely to require it. uWSGI ~~~~~ When using the uWSGI server in combination with gevent, the Engine.IO server can take advantage of uWSGI's native WebSocket support. Instances of class ``engineio.Server`` will automatically use this option for asynchronous operations if both gevent and uWSGI are installed and eventlet is not installed. To request this asynchoronous mode explicitly, the ``async_mode`` option can be given in the constructor:: # gevent with uWSGI eio = engineio.Server(async_mode='gevent_uwsgi') A complete explanation of the configuration and usage of the uWSGI server is beyond the scope of this documentation. The uWSGI server is a fairly complex package that provides a large and comprehensive set of options. It must be compiled with WebSocket and SSL support for the WebSocket transport to be available. As way of an introduction, the following command starts a uWSGI server for the ``latency.py`` example on port 5000:: $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app Standard Threads ~~~~~~~~~~~~~~~~ While not comparable to eventlet and gevent in terms of performance, the Engine.IO server can also be configured to work with multi-threaded web servers that use standard Python threads. This is an ideal setup to use with development servers such as `Werkzeug `_. Instances of class ``engineio.Server`` will automatically use the threading mode if neither eventlet nor gevent are not installed. To request the threading mode explicitly, the ``async_mode`` option can be given in the constructor:: eio = engineio.Server(async_mode='threading') A server configured for threading is deployed as a regular web application, using any WSGI complaint multi-threaded server. The example below deploys an Engine.IO application combined with a Flask web application, using Flask's development web server based on Werkzeug:: eio = engineio.Server(async_mode='threading') app = Flask(__name__) app.wsgi_app = engineio.WSGIApp(eio, app.wsgi_app) # ... Engine.IO and Flask handler functions ... if __name__ == '__main__': app.run() The example that follows shows how to start an Engine.IO application using Gunicorn's threaded worker class:: $ gunicorn -w 1 --threads 100 module:app With the above configuration the server will be able to handle up to 100 concurrent clients. When using standard threads, WebSocket is supported through the `simple-websocket `_ package, which must be installed separately. This package provides a multi-threaded WebSocket server that is compatible with Werkzeug and Gunicorn's threaded worker. Other multi-threaded web servers are not supported and will not enable the WebSocket transport. Scalability Notes ~~~~~~~~~~~~~~~~~ Engine.IO is a stateful protocol, which makes horizontal scaling more difficult. To deploy a cluster of Engine.IO processes hosted on one or multiple servers the following conditions must be met: - Each Engine.IO server process must be able to handle multiple requests concurrently. This is required because long-polling clients send two requests in parallel. Worker processes that can only handle one request at a time are not supported. - The load balancer must be configured to always forward requests from a client to the same process. Load balancers call this *sticky sessions*, or *session affinity*. Cross-Origin Controls --------------------- For security reasons, this server enforces a same-origin policy by default. In practical terms, this means the following: - If an incoming HTTP or WebSocket request includes the ``Origin`` header, this header must match the scheme and host of the connection URL. In case of a mismatch, a 400 status code response is returned and the connection is rejected. - No restrictions are imposed on incoming requests that do not include the ``Origin`` header. If necessary, the ``cors_allowed_origins`` option can be used to allow other origins. This argument can be set to a string to set a single allowed origin, or to a list to allow multiple origins. A special value of ``'*'`` can be used to instruct the server to allow all origins, but this should be done with care, as this could make the server vulnerable to Cross-Site Request Forgery (CSRF) attacks. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977700.0 python_engineio-4.12.1/pyproject.toml0000664000175000017500000000224315010141644017276 0ustar00miguelmiguel[project] name = "python-engineio" version = "4.12.1" license = {text = "MIT"} authors = [{name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com"}] description = "Engine.IO server and client for Python" classifiers = [ "Environment :: Web Environment", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] requires-python = ">=3.6" dependencies = ["simple-websocket >= 0.10.0"] [project.readme] file = "README.md" content-type = "text/markdown" [project.urls] Homepage = "https://github.com/miguelgrinberg/python-engineio" "Bug Tracker" = "https://github.com/miguelgrinberg/python-engineio/issues" [project.optional-dependencies] client = [ "requests >= 2.21.0", "websocket-client >= 0.54.0", ] asyncio_client = ["aiohttp >= 3.4"] docs = ["sphinx"] [tool.setuptools] zip-safe = false include-package-data = true package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] namespaces = false [build-system] requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3663085 python_engineio-4.12.1/setup.cfg0000664000175000017500000000004615010141646016204 0ustar00miguelmiguel[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3613086 python_engineio-4.12.1/src/0000775000175000017500000000000015010141646015152 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3633084 python_engineio-4.12.1/src/engineio/0000775000175000017500000000000015010141646016747 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/src/engineio/__init__.py0000664000175000017500000000074114707424343021074 0ustar00miguelmiguelfrom .client import Client from .middleware import WSGIApp, Middleware from .server import Server from .async_server import AsyncServer from .async_client import AsyncClient from .async_drivers.asgi import ASGIApp try: from .async_drivers.tornado import get_tornado_handler except ImportError: # pragma: no cover get_tornado_handler = None __all__ = ['Server', 'WSGIApp', 'Middleware', 'Client', 'AsyncServer', 'ASGIApp', 'get_tornado_handler', 'AsyncClient'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734479853.0 python_engineio-4.12.1/src/engineio/async_client.py0000664000175000017500000007140614730407755022022 0ustar00miguelmiguelimport asyncio import signal import ssl import threading try: import aiohttp except ImportError: # pragma: no cover aiohttp = None from . import base_client from . import exceptions from . import packet from . import payload async_signal_handler_set = False # this set is used to keep references to background tasks to prevent them from # being garbage collected mid-execution. Solution taken from # https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task task_reference_holder = set() def async_signal_handler(): """SIGINT handler. Disconnect all active async clients. """ async def _handler(): # pragma: no cover for c in base_client.connected_clients[:]: if c.is_asyncio_based(): await c.disconnect() # cancel all running tasks tasks = [task for task in asyncio.all_tasks() if task is not asyncio.current_task()] for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) asyncio.get_running_loop().stop() asyncio.ensure_future(_handler()) class AsyncClient(base_client.BaseClient): """An Engine.IO client for asyncio. This class implements a fully compliant Engine.IO web client with support for websocket and long-polling transports, compatible with the asyncio framework on Python 3.5 or newer. :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. The default is ``False``. Note that fatal errors are logged even when ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param request_timeout: A timeout in seconds for requests. The default is 5 seconds. :param http_session: an initialized ``aiohttp.ClientSession`` object to be used when sending requests to the server. Use it if you need to add special client options such as proxy servers, SSL certificates, custom CA bundle, etc. :param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to skip SSL certificate verification, allowing connections to servers with self signed certificates. The default is ``True``. :param handle_sigint: Set to ``True`` to automatically handle disconnection when the process is interrupted, or to ``False`` to leave interrupt handling to the calling application. Interrupt handling can only be enabled when the client instance is created in the main thread. :param websocket_extra_options: Dictionary containing additional keyword arguments passed to ``aiohttp.ws_connect()``. :param timestamp_requests: If ``True`` a timestamp is added to the query string of Socket.IO requests as a cache-busting measure. Set to ``False`` to disable. """ def is_asyncio_based(self): return True async def connect(self, url, headers=None, transports=None, engineio_path='engine.io'): """Connect to an Engine.IO server. :param url: The URL of the Engine.IO server. It can include custom query string parameters if required by the server. :param headers: A dictionary with custom headers to send with the connection request. :param transports: The list of allowed transports. Valid transports are ``'polling'`` and ``'websocket'``. If not given, the polling transport is connected first, then an upgrade to websocket is attempted. :param engineio_path: The endpoint where the Engine.IO server is installed. The default value is appropriate for most cases. Note: this method is a coroutine. Example usage:: eio = engineio.Client() await eio.connect('http://localhost:5000') """ global async_signal_handler_set if self.handle_sigint and not async_signal_handler_set and \ threading.current_thread() == threading.main_thread(): try: asyncio.get_running_loop().add_signal_handler( signal.SIGINT, async_signal_handler) except NotImplementedError: # pragma: no cover self.logger.warning('Signal handler is unsupported') async_signal_handler_set = True if self.state != 'disconnected': raise ValueError('Client is not in a disconnected state') valid_transports = ['polling', 'websocket'] if transports is not None: if isinstance(transports, str): transports = [transports] transports = [transport for transport in transports if transport in valid_transports] if not transports: raise ValueError('No valid transports provided') self.transports = transports or valid_transports self.queue = self.create_queue() return await getattr(self, '_connect_' + self.transports[0])( url, headers or {}, engineio_path) async def wait(self): """Wait until the connection with the server ends. Client applications can use this function to block the main thread during the life of the connection. Note: this method is a coroutine. """ if self.read_loop_task: await self.read_loop_task async def send(self, data): """Send a message to the server. :param data: The data to send to the server. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. Note: this method is a coroutine. """ await self._send_packet(packet.Packet(packet.MESSAGE, data=data)) async def disconnect(self, abort=False, reason=None): """Disconnect from the server. :param abort: If set to ``True``, do not wait for background tasks associated with the connection to end. Note: this method is a coroutine. """ if self.state == 'connected': await self._send_packet(packet.Packet(packet.CLOSE)) await self.queue.put(None) self.state = 'disconnecting' await self._trigger_event('disconnect', reason or self.reason.CLIENT_DISCONNECT, run_async=False) if self.current_transport == 'websocket': await self.ws.close() if not abort: await self.read_loop_task self.state = 'disconnected' try: base_client.connected_clients.remove(self) except ValueError: # pragma: no cover pass await self._reset() def start_background_task(self, target, *args, **kwargs): """Start a background task. This is a utility function that applications can use to start a background task. :param target: the target function to execute. :param args: arguments to pass to the function. :param kwargs: keyword arguments to pass to the function. The return value is a ``asyncio.Task`` object. """ return asyncio.ensure_future(target(*args, **kwargs)) async def sleep(self, seconds=0): """Sleep for the requested amount of time. Note: this method is a coroutine. """ return await asyncio.sleep(seconds) def create_queue(self): """Create a queue object.""" q = asyncio.Queue() q.Empty = asyncio.QueueEmpty return q def create_event(self): """Create an event object.""" return asyncio.Event() async def _reset(self): super()._reset() if not self.external_http: # pragma: no cover if self.http and not self.http.closed: await self.http.close() def __del__(self): # pragma: no cover # try to close the aiohttp session if it is still open if self.http and not self.http.closed: try: loop = asyncio.get_event_loop() if loop.is_running(): loop.ensure_future(self.http.close()) else: loop.run_until_complete(self.http.close()) except: pass async def _connect_polling(self, url, headers, engineio_path): """Establish a long-polling connection to the Engine.IO server.""" if aiohttp is None: # pragma: no cover self.logger.error('aiohttp not installed -- cannot make HTTP ' 'requests!') return self.base_url = self._get_engineio_url(url, engineio_path, 'polling') self.logger.info('Attempting polling connection to ' + self.base_url) r = await self._send_request( 'GET', self.base_url + self._get_url_timestamp(), headers=headers, timeout=self.request_timeout) if r is None or isinstance(r, str): await self._reset() raise exceptions.ConnectionError( r or 'Connection refused by the server') if r.status < 200 or r.status >= 300: await self._reset() try: arg = await r.json() except aiohttp.ClientError: arg = None raise exceptions.ConnectionError( 'Unexpected status code {} in server response'.format( r.status), arg) try: p = payload.Payload(encoded_payload=(await r.read()).decode( 'utf-8')) except ValueError: raise exceptions.ConnectionError( 'Unexpected response from server') from None open_packet = p.packets[0] if open_packet.packet_type != packet.OPEN: raise exceptions.ConnectionError( 'OPEN packet not returned by server') self.logger.info( 'Polling connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'polling' self.base_url += '&sid=' + self.sid self.state = 'connected' base_client.connected_clients.append(self) await self._trigger_event('connect', run_async=False) for pkt in p.packets[1:]: await self._receive_packet(pkt) if 'websocket' in self.upgrades and 'websocket' in self.transports: # attempt to upgrade to websocket if await self._connect_websocket(url, headers, engineio_path): # upgrade to websocket succeeded, we're done here return self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_polling) async def _connect_websocket(self, url, headers, engineio_path): """Establish or upgrade to a WebSocket connection with the server.""" if aiohttp is None: # pragma: no cover self.logger.error('aiohttp package not installed') return False websocket_url = self._get_engineio_url(url, engineio_path, 'websocket') if self.sid: self.logger.info( 'Attempting WebSocket upgrade to ' + websocket_url) upgrade = True websocket_url += '&sid=' + self.sid else: upgrade = False self.base_url = websocket_url self.logger.info( 'Attempting WebSocket connection to ' + websocket_url) if self.http is None or self.http.closed: # pragma: no cover self.http = aiohttp.ClientSession() # extract any new cookies passed in a header so that they can also be # sent the the WebSocket route cookies = {} for header, value in headers.items(): if header.lower() == 'cookie': cookies = dict( [cookie.split('=', 1) for cookie in value.split('; ')]) del headers[header] break self.http.cookie_jar.update_cookies(cookies) extra_options = {'timeout': self.request_timeout} if not self.ssl_verify: ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE extra_options['ssl'] = ssl_context # combine internally generated options with the ones supplied by the # caller. The caller's options take precedence. headers.update(self.websocket_extra_options.pop('headers', {})) extra_options['headers'] = headers extra_options.update(self.websocket_extra_options) try: ws = await self.http.ws_connect( websocket_url + self._get_url_timestamp(), **extra_options) except (aiohttp.client_exceptions.WSServerHandshakeError, aiohttp.client_exceptions.ServerConnectionError, aiohttp.client_exceptions.ClientConnectionError): if upgrade: self.logger.warning( 'WebSocket upgrade failed: connection error') return False else: raise exceptions.ConnectionError('Connection error') if upgrade: p = packet.Packet(packet.PING, data='probe').encode() try: await ws.send_str(p) except Exception as e: # pragma: no cover self.logger.warning( 'WebSocket upgrade failed: unexpected send exception: %s', str(e)) return False try: p = (await ws.receive()).data except Exception as e: # pragma: no cover self.logger.warning( 'WebSocket upgrade failed: unexpected recv exception: %s', str(e)) return False pkt = packet.Packet(encoded_packet=p) if pkt.packet_type != packet.PONG or pkt.data != 'probe': self.logger.warning( 'WebSocket upgrade failed: no PONG packet') return False p = packet.Packet(packet.UPGRADE).encode() try: await ws.send_str(p) except Exception as e: # pragma: no cover self.logger.warning( 'WebSocket upgrade failed: unexpected send exception: %s', str(e)) return False self.current_transport = 'websocket' self.logger.info('WebSocket upgrade was successful') else: try: p = (await ws.receive()).data except Exception as e: # pragma: no cover raise exceptions.ConnectionError( 'Unexpected recv exception: ' + str(e)) open_packet = packet.Packet(encoded_packet=p) if open_packet.packet_type != packet.OPEN: raise exceptions.ConnectionError('no OPEN packet') self.logger.info( 'WebSocket connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'websocket' self.state = 'connected' base_client.connected_clients.append(self) await self._trigger_event('connect', run_async=False) self.ws = ws self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_websocket) return True async def _receive_packet(self, pkt): """Handle incoming packets from the server.""" packet_name = packet.packet_names[pkt.packet_type] \ if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN' self.logger.info( 'Received packet %s data %s', packet_name, pkt.data if not isinstance(pkt.data, bytes) else '') if pkt.packet_type == packet.MESSAGE: await self._trigger_event('message', pkt.data, run_async=True) elif pkt.packet_type == packet.PING: await self._send_packet(packet.Packet(packet.PONG, pkt.data)) elif pkt.packet_type == packet.CLOSE: await self.disconnect(abort=True, reason=self.reason.SERVER_DISCONNECT) elif pkt.packet_type == packet.NOOP: pass else: self.logger.error('Received unexpected packet of type %s', pkt.packet_type) async def _send_packet(self, pkt): """Queue a packet to be sent to the server.""" if self.state != 'connected': return await self.queue.put(pkt) self.logger.info( 'Sending packet %s data %s', packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') async def _send_request( self, method, url, headers=None, body=None, timeout=None): # pragma: no cover if self.http is None or self.http.closed: self.http = aiohttp.ClientSession() http_method = getattr(self.http, method.lower()) try: if not self.ssl_verify: return await http_method( url, headers=headers, data=body, timeout=aiohttp.ClientTimeout(total=timeout), ssl=False) else: return await http_method( url, headers=headers, data=body, timeout=aiohttp.ClientTimeout(total=timeout)) except (aiohttp.ClientError, asyncio.TimeoutError) as exc: self.logger.info('HTTP %s request to %s failed with error %s.', method, url, exc) return str(exc) async def _trigger_event(self, event, *args, **kwargs): """Invoke an event handler.""" run_async = kwargs.pop('run_async', False) ret = None if event in self.handlers: if asyncio.iscoroutinefunction(self.handlers[event]) is True: if run_async: task = self.start_background_task(self.handlers[event], *args) task_reference_holder.add(task) task.add_done_callback(task_reference_holder.discard) return task else: try: try: ret = await self.handlers[event](*args) except TypeError: if event == 'disconnect' and \ len(args) == 1: # pragma: no branch # legacy disconnect events do not have a reason # argument return await self.handlers[event]() else: # pragma: no cover raise except asyncio.CancelledError: # pragma: no cover pass except: self.logger.exception(event + ' async handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False else: if run_async: async def async_handler(): return self.handlers[event](*args) task = self.start_background_task(async_handler) task_reference_holder.add(task) task.add_done_callback(task_reference_holder.discard) return task else: try: try: ret = self.handlers[event](*args) except TypeError: if event == 'disconnect' and \ len(args) == 1: # pragma: no branch # legacy disconnect events do not have a reason # argument ret = self.handlers[event]() else: # pragma: no cover raise except: self.logger.exception(event + ' handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False return ret async def _read_loop_polling(self): """Read packets by polling the Engine.IO server.""" while self.state == 'connected' and self.write_loop_task: self.logger.info( 'Sending polling GET request to ' + self.base_url) r = await self._send_request( 'GET', self.base_url + self._get_url_timestamp(), timeout=max(self.ping_interval, self.ping_timeout) + 5) if r is None or isinstance(r, str): self.logger.warning( r or 'Connection refused by the server, aborting') await self.queue.put(None) break if r.status < 200 or r.status >= 300: self.logger.warning('Unexpected status code %s in server ' 'response, aborting', r.status) await self.queue.put(None) break try: p = payload.Payload(encoded_payload=(await r.read()).decode( 'utf-8')) except ValueError: self.logger.warning( 'Unexpected packet from server, aborting') await self.queue.put(None) break for pkt in p.packets: await self._receive_packet(pkt) if self.write_loop_task: # pragma: no branch self.logger.info('Waiting for write loop task to end') await self.write_loop_task if self.state == 'connected': await self._trigger_event( 'disconnect', self.reason.TRANSPORT_ERROR, run_async=False) try: base_client.connected_clients.remove(self) except ValueError: # pragma: no cover pass await self._reset() self.logger.info('Exiting read loop task') async def _read_loop_websocket(self): """Read packets from the Engine.IO WebSocket connection.""" while self.state == 'connected': p = None try: p = await asyncio.wait_for( self.ws.receive(), timeout=self.ping_interval + self.ping_timeout) if not isinstance(p.data, (str, bytes)): # pragma: no cover self.logger.warning( 'Server sent %s packet data %s, aborting', 'close' if p.type in [aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING] else str(p.type), str(p.data)) await self.queue.put(None) break # the connection is broken p = p.data except asyncio.TimeoutError: self.logger.warning( 'Server has stopped communicating, aborting') await self.queue.put(None) break except aiohttp.client_exceptions.ServerDisconnectedError: self.logger.info( 'Read loop: WebSocket connection was closed, aborting') await self.queue.put(None) break except Exception as e: self.logger.info( 'Unexpected error receiving packet: "%s", aborting', str(e)) await self.queue.put(None) break try: pkt = packet.Packet(encoded_packet=p) except Exception as e: # pragma: no cover self.logger.info( 'Unexpected error decoding packet: "%s", aborting', str(e)) await self.queue.put(None) break await self._receive_packet(pkt) if self.write_loop_task: # pragma: no branch self.logger.info('Waiting for write loop task to end') await self.write_loop_task if self.state == 'connected': await self._trigger_event( 'disconnect', self.reason.TRANSPORT_ERROR, run_async=False) try: base_client.connected_clients.remove(self) except ValueError: # pragma: no cover pass await self._reset() self.logger.info('Exiting read loop task') async def _write_loop(self): """This background task sends packages to the server as they are pushed to the send queue. """ while self.state == 'connected': # to simplify the timeout handling, use the maximum of the # ping interval and ping timeout as timeout, with an extra 5 # seconds grace period timeout = max(self.ping_interval, self.ping_timeout) + 5 packets = None try: packets = [await asyncio.wait_for(self.queue.get(), timeout)] except (self.queue.Empty, asyncio.TimeoutError): self.logger.error('packet queue is empty, aborting') break except asyncio.CancelledError: # pragma: no cover break if packets == [None]: self.queue.task_done() packets = [] else: while True: try: packets.append(self.queue.get_nowait()) except self.queue.Empty: break if packets[-1] is None: packets = packets[:-1] self.queue.task_done() break if not packets: # empty packet list returned -> connection closed break if self.current_transport == 'polling': p = payload.Payload(packets=packets) r = await self._send_request( 'POST', self.base_url, body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=self.request_timeout) for pkt in packets: self.queue.task_done() if r is None or isinstance(r, str): self.logger.warning( r or 'Connection refused by the server, aborting') break if r.status < 200 or r.status >= 300: self.logger.warning('Unexpected status code %s in server ' 'response, aborting', r.status) self.write_loop_task = None break else: # websocket try: for pkt in packets: if pkt.binary: await self.ws.send_bytes(pkt.encode()) else: await self.ws.send_str(pkt.encode()) self.queue.task_done() except (aiohttp.client_exceptions.ServerDisconnectedError, BrokenPipeError, OSError): self.logger.info( 'Write loop: WebSocket connection was closed, ' 'aborting') break self.logger.info('Exiting write loop task') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3633084 python_engineio-4.12.1/src/engineio/async_drivers/0000775000175000017500000000000015010141646021622 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/src/engineio/async_drivers/__init__.py0000664000175000017500000000000014707424343023733 0ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/async_drivers/_websocket_wsgi.py0000664000175000017500000000166514726642012025370 0ustar00miguelmiguelimport simple_websocket class SimpleWebSocketWSGI: # pragma: no cover """ This wrapper class provides a threading WebSocket interface that is compatible with eventlet's implementation. """ def __init__(self, handler, server, **kwargs): self.app = handler self.server_args = kwargs def __call__(self, environ, start_response): self.ws = simple_websocket.Server(environ, **self.server_args) ret = self.app(self) if self.ws.mode == 'gunicorn': raise StopIteration() return ret def close(self): if self.ws.connected: self.ws.close() def send(self, message): try: return self.ws.send(message) except simple_websocket.ConnectionClosed: raise OSError() def wait(self): try: return self.ws.receive() except simple_websocket.ConnectionClosed: return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/async_drivers/aiohttp.py0000664000175000017500000000725214726642012023660 0ustar00miguelmiguelimport asyncio import sys from urllib.parse import urlsplit from aiohttp.web import Response, WebSocketResponse def create_route(app, engineio_server, engineio_endpoint): """This function sets up the engine.io endpoint as a route for the application. Note that both GET and POST requests must be hooked up on the engine.io endpoint. """ app.router.add_get(engineio_endpoint, engineio_server.handle_request) app.router.add_post(engineio_endpoint, engineio_server.handle_request) app.router.add_route('OPTIONS', engineio_endpoint, engineio_server.handle_request) def translate_request(request): """This function takes the arguments passed to the request handler and uses them to generate a WSGI compatible environ dictionary. """ message = request._message payload = request._payload uri_parts = urlsplit(message.path) environ = { 'wsgi.input': payload, 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'SERVER_SOFTWARE': 'aiohttp', 'REQUEST_METHOD': message.method, 'QUERY_STRING': uri_parts.query or '', 'RAW_URI': message.path, 'SERVER_PROTOCOL': 'HTTP/%s.%s' % message.version, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '0', 'SERVER_NAME': 'aiohttp', 'SERVER_PORT': '0', 'aiohttp.request': request } for hdr_name, hdr_value in message.headers.items(): hdr_name = hdr_name.upper() if hdr_name == 'CONTENT-TYPE': environ['CONTENT_TYPE'] = hdr_value continue elif hdr_name == 'CONTENT-LENGTH': environ['CONTENT_LENGTH'] = hdr_value continue key = 'HTTP_%s' % hdr_name.replace('-', '_') if key in environ: hdr_value = f'{environ[key]},{hdr_value}' environ[key] = hdr_value environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') path_info = uri_parts.path environ['PATH_INFO'] = path_info environ['SCRIPT_NAME'] = '' return environ def make_response(status, headers, payload, environ): """This function generates an appropriate response object for this async mode. """ return Response(body=payload, status=int(status.split()[0]), headers=headers) class WebSocket: # pragma: no cover """ This wrapper class provides a aiohttp WebSocket interface that is somewhat compatible with eventlet's implementation. """ def __init__(self, handler, server): self.handler = handler self._sock = None async def __call__(self, environ): request = environ['aiohttp.request'] self._sock = WebSocketResponse(max_msg_size=0) await self._sock.prepare(request) self.environ = environ await self.handler(self) return self._sock async def close(self): await self._sock.close() async def send(self, message): if isinstance(message, bytes): f = self._sock.send_bytes else: f = self._sock.send_str if asyncio.iscoroutinefunction(f): await f(message) else: f(message) async def wait(self): msg = await self._sock.receive() if not isinstance(msg.data, bytes) and \ not isinstance(msg.data, str): raise OSError() return msg.data _async = { 'asyncio': True, 'create_route': create_route, 'translate_request': translate_request, 'make_response': make_response, 'websocket': WebSocket, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977416.0 python_engineio-4.12.1/src/engineio/async_drivers/asgi.py0000664000175000017500000002611715010141210023111 0ustar00miguelmiguelimport os import sys import asyncio from engineio.static_files import get_static_file class ASGIApp: """ASGI application middleware for Engine.IO. This middleware dispatches traffic to an Engine.IO application. It can also serve a list of static files to the client, or forward unrelated HTTP traffic to another ASGI application. :param engineio_server: The Engine.IO server. Must be an instance of the ``engineio.AsyncServer`` class. :param static_files: A dictionary with static file mapping rules. See the documentation for details on this argument. :param other_asgi_app: A separate ASGI app that receives all other traffic. :param engineio_path: The endpoint where the Engine.IO application should be installed. The default value is appropriate for most cases. With a value of ``None``, all incoming traffic is directed to the Engine.IO server, with the assumption that routing, if necessary, is handled by a different layer. When this option is set to ``None``, ``static_files`` and ``other_asgi_app`` are ignored. :param on_startup: function to be called on application startup; can be coroutine :param on_shutdown: function to be called on application shutdown; can be coroutine Example usage:: import engineio import uvicorn eio = engineio.AsyncServer() app = engineio.ASGIApp(eio, static_files={ '/': {'content_type': 'text/html', 'filename': 'index.html'}, '/index.html': {'content_type': 'text/html', 'filename': 'index.html'}, }) uvicorn.run(app, '127.0.0.1', 5000) """ def __init__(self, engineio_server, other_asgi_app=None, static_files=None, engineio_path='engine.io', on_startup=None, on_shutdown=None): self.engineio_server = engineio_server self.other_asgi_app = other_asgi_app self.engineio_path = engineio_path if self.engineio_path is not None: if not self.engineio_path.startswith('/'): self.engineio_path = '/' + self.engineio_path if not self.engineio_path.endswith('/'): self.engineio_path += '/' self.static_files = static_files or {} self.on_startup = on_startup self.on_shutdown = on_shutdown async def __call__(self, scope, receive, send): if scope['type'] == 'lifespan': await self.lifespan(scope, receive, send) elif scope['type'] in ['http', 'websocket'] and ( self.engineio_path is None or self._ensure_trailing_slash(scope['path']).startswith( self.engineio_path)): await self.engineio_server.handle_request(scope, receive, send) else: static_file = get_static_file(scope['path'], self.static_files) \ if scope['type'] == 'http' and self.static_files else None if static_file and os.path.exists(static_file['filename']): await self.serve_static_file(static_file, receive, send) elif self.other_asgi_app is not None: await self.other_asgi_app(scope, receive, send) else: await self.not_found(receive, send) async def serve_static_file(self, static_file, receive, send): # pragma: no cover event = await receive() if event['type'] == 'http.request': with open(static_file['filename'], 'rb') as f: payload = f.read() await send({'type': 'http.response.start', 'status': 200, 'headers': [(b'Content-Type', static_file[ 'content_type'].encode('utf-8'))]}) await send({'type': 'http.response.body', 'body': payload}) async def lifespan(self, scope, receive, send): if self.other_asgi_app is not None and self.on_startup is None and \ self.on_shutdown is None: # let the other ASGI app handle lifespan events await self.other_asgi_app(scope, receive, send) return while True: event = await receive() if event['type'] == 'lifespan.startup': if self.on_startup: try: await self.on_startup() \ if asyncio.iscoroutinefunction(self.on_startup) \ else self.on_startup() except: await send({'type': 'lifespan.startup.failed'}) return await send({'type': 'lifespan.startup.complete'}) elif event['type'] == 'lifespan.shutdown': if self.on_shutdown: try: await self.on_shutdown() \ if asyncio.iscoroutinefunction(self.on_shutdown) \ else self.on_shutdown() except: await send({'type': 'lifespan.shutdown.failed'}) return await send({'type': 'lifespan.shutdown.complete'}) return async def not_found(self, receive, send): """Return a 404 Not Found error to the client.""" await send({'type': 'http.response.start', 'status': 404, 'headers': [(b'Content-Type', b'text/plain')]}) await send({'type': 'http.response.body', 'body': b'Not Found'}) def _ensure_trailing_slash(self, path): if not path.endswith('/'): path += '/' return path async def translate_request(scope, receive, send): class AwaitablePayload: # pragma: no cover def __init__(self, payload): self.payload = payload or b'' async def read(self, length=None): if length is None: r = self.payload self.payload = b'' else: r = self.payload[:length] self.payload = self.payload[length:] return r event = await receive() payload = b'' if event['type'] == 'http.request': payload += event.get('body') or b'' while event.get('more_body'): event = await receive() if event['type'] == 'http.request': payload += event.get('body') or b'' elif event['type'] == 'websocket.connect': pass else: return {} raw_uri = scope['path'] query_string = '' if 'query_string' in scope and scope['query_string']: try: query_string = scope['query_string'].decode('utf-8') except UnicodeDecodeError: pass else: raw_uri += '?' + query_string environ = { 'wsgi.input': AwaitablePayload(payload), 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'SERVER_SOFTWARE': 'asgi', 'REQUEST_METHOD': scope.get('method', 'GET'), 'PATH_INFO': scope['path'], 'QUERY_STRING': query_string, 'RAW_URI': raw_uri, 'SCRIPT_NAME': '', 'SERVER_PROTOCOL': 'HTTP/1.1', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '0', 'SERVER_NAME': 'asgi', 'SERVER_PORT': '0', 'asgi.receive': receive, 'asgi.send': send, 'asgi.scope': scope, } for hdr_name, hdr_value in scope['headers']: try: hdr_name = hdr_name.upper().decode('utf-8') hdr_value = hdr_value.decode('utf-8') except UnicodeDecodeError: # skip header if it cannot be decoded continue if hdr_name == 'CONTENT-TYPE': environ['CONTENT_TYPE'] = hdr_value continue elif hdr_name == 'CONTENT-LENGTH': environ['CONTENT_LENGTH'] = hdr_value continue key = 'HTTP_%s' % hdr_name.replace('-', '_') if key in environ: hdr_value = f'{environ[key]},{hdr_value}' environ[key] = hdr_value environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') return environ async def make_response(status, headers, payload, environ): headers = [(h[0].encode('utf-8'), h[1].encode('utf-8')) for h in headers] if environ['asgi.scope']['type'] == 'websocket': if status.startswith('200 '): await environ['asgi.send']({'type': 'websocket.accept', 'headers': headers}) else: if payload: reason = payload.decode('utf-8') \ if isinstance(payload, bytes) else str(payload) await environ['asgi.send']({'type': 'websocket.close', 'reason': reason}) else: await environ['asgi.send']({'type': 'websocket.close'}) return await environ['asgi.send']({'type': 'http.response.start', 'status': int(status.split(' ')[0]), 'headers': headers}) await environ['asgi.send']({'type': 'http.response.body', 'body': payload}) class WebSocket: # pragma: no cover """ This wrapper class provides an asgi WebSocket interface that is somewhat compatible with eventlet's implementation. """ def __init__(self, handler, server): self.handler = handler self.asgi_receive = None self.asgi_send = None async def __call__(self, environ): self.asgi_receive = environ['asgi.receive'] self.asgi_send = environ['asgi.send'] await self.asgi_send({'type': 'websocket.accept'}) await self.handler(self) return '' # send nothing as response async def close(self): try: await self.asgi_send({'type': 'websocket.close'}) except Exception: # if the socket is already close we don't care pass async def send(self, message): msg_bytes = None msg_text = None if isinstance(message, bytes): msg_bytes = message else: msg_text = message await self.asgi_send({'type': 'websocket.send', 'bytes': msg_bytes, 'text': msg_text}) async def wait(self): event = await self.asgi_receive() if event['type'] != 'websocket.receive': raise OSError() if 'bytes' in event: return event['bytes'] elif 'text' in event: return event['text'] else: # pragma: no cover raise OSError() _async = { 'asyncio': True, 'translate_request': translate_request, 'make_response': make_response, 'websocket': WebSocket, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/async_drivers/eventlet.py0000664000175000017500000000330714726642012024033 0ustar00miguelmiguelfrom eventlet.green.threading import Event from eventlet import queue, sleep, spawn from eventlet.websocket import WebSocketWSGI as _WebSocketWSGI class EventletThread: # pragma: no cover """Thread class that uses eventlet green threads. Eventlet's own Thread class has a strange bug that causes _DummyThread objects to be created and leaked, since they are never garbage collected. """ def __init__(self, target, args=None, kwargs=None): self.target = target self.args = args or () self.kwargs = kwargs or {} self.g = None def start(self): self.g = spawn(self.target, *self.args, **self.kwargs) def join(self): if self.g: return self.g.wait() class WebSocketWSGI(_WebSocketWSGI): # pragma: no cover def __init__(self, handler, server): try: super().__init__( handler, max_frame_length=int(server.max_http_buffer_size)) except TypeError: # pragma: no cover # older versions of eventlet do not support a max frame size super().__init__(handler) self._sock = None def __call__(self, environ, start_response): if 'eventlet.input' not in environ: raise RuntimeError('You need to use the eventlet server. ' 'See the Deployment section of the ' 'documentation for more information.') self._sock = environ['eventlet.input'].get_socket() return super().__call__(environ, start_response) _async = { 'thread': EventletThread, 'queue': queue.Queue, 'queue_empty': queue.Empty, 'event': Event, 'websocket': WebSocketWSGI, 'sleep': sleep, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/src/engineio/async_drivers/gevent.py0000664000175000017500000000562214707424343023503 0ustar00miguelmiguelimport gevent from gevent import queue from gevent.event import Event try: # use gevent-websocket if installed import geventwebsocket # noqa SimpleWebSocketWSGI = None except ImportError: # pragma: no cover # fallback to simple_websocket when gevent-websocket is not installed from engineio.async_drivers._websocket_wsgi import SimpleWebSocketWSGI class Thread(gevent.Greenlet): # pragma: no cover """ This wrapper class provides gevent Greenlet interface that is compatible with the standard library's Thread class. """ def __init__(self, target, args=[], kwargs={}): super().__init__(target, *args, **kwargs) def _run(self): return self.run() if SimpleWebSocketWSGI is not None: class WebSocketWSGI(SimpleWebSocketWSGI): # pragma: no cover """ This wrapper class provides a gevent WebSocket interface that is compatible with eventlet's implementation, using the simple-websocket package. """ def __init__(self, handler, server): # to avoid the requirement that the standard library is # monkey-patched, here we pass the gevent versions of the # concurrency and networking classes required by simple-websocket import gevent.event import gevent.selectors super().__init__(handler, server, thread_class=Thread, event_class=gevent.event.Event, selector_class=gevent.selectors.DefaultSelector) else: class WebSocketWSGI: # pragma: no cover """ This wrapper class provides a gevent WebSocket interface that is compatible with eventlet's implementation, using the gevent-websocket package. """ def __init__(self, handler, server): self.app = handler def __call__(self, environ, start_response): if 'wsgi.websocket' not in environ: raise RuntimeError('The gevent-websocket server is not ' 'configured appropriately. ' 'See the Deployment section of the ' 'documentation for more information.') self._sock = environ['wsgi.websocket'] self.environ = environ self.version = self._sock.version self.path = self._sock.path self.origin = self._sock.origin self.protocol = self._sock.protocol return self.app(self) def close(self): return self._sock.close() def send(self, message): return self._sock.send(message) def wait(self): return self._sock.receive() _async = { 'thread': Thread, 'queue': queue.JoinableQueue, 'queue_empty': queue.Empty, 'event': Event, 'websocket': WebSocketWSGI, 'sleep': gevent.sleep, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/async_drivers/gevent_uwsgi.py0000664000175000017500000001350214726642012024711 0ustar00miguelmiguelimport gevent from gevent import queue from gevent.event import Event from gevent import selectors import uwsgi _websocket_available = hasattr(uwsgi, 'websocket_handshake') class Thread(gevent.Greenlet): # pragma: no cover """ This wrapper class provides gevent Greenlet interface that is compatible with the standard library's Thread class. """ def __init__(self, target, args=[], kwargs={}): super().__init__(target, *args, **kwargs) def _run(self): return self.run() class uWSGIWebSocket: # pragma: no cover """ This wrapper class provides a uWSGI WebSocket interface that is compatible with eventlet's implementation. """ def __init__(self, handler, server): self.app = handler self._sock = None self.received_messages = [] def __call__(self, environ, start_response): self._sock = uwsgi.connection_fd() self.environ = environ uwsgi.websocket_handshake() self._req_ctx = None if hasattr(uwsgi, 'request_context'): # uWSGI >= 2.1.x with support for api access across-greenlets self._req_ctx = uwsgi.request_context() else: # use event and queue for sending messages self._event = Event() self._send_queue = queue.Queue() # spawn a select greenlet def select_greenlet_runner(fd, event): """Sets event when data becomes available to read on fd.""" sel = selectors.DefaultSelector() sel.register(fd, selectors.EVENT_READ) try: while True: sel.select() event.set() except gevent.GreenletExit: sel.unregister(fd) self._select_greenlet = gevent.spawn( select_greenlet_runner, self._sock, self._event) self.app(self) uwsgi.disconnect() return '' # send nothing as response def close(self): """Disconnects uWSGI from the client.""" if self._req_ctx is None: # better kill it here in case wait() is not called again self._select_greenlet.kill() self._event.set() def _send(self, msg): """Transmits message either in binary or UTF-8 text mode, depending on its type.""" if isinstance(msg, bytes): method = uwsgi.websocket_send_binary else: method = uwsgi.websocket_send if self._req_ctx is not None: method(msg, request_context=self._req_ctx) else: method(msg) def _decode_received(self, msg): """Returns either bytes or str, depending on message type.""" if not isinstance(msg, bytes): # already decoded - do nothing return msg # only decode from utf-8 if message is not binary data type = ord(msg[0:1]) if type >= 48: # no binary return msg.decode('utf-8') # binary message, don't try to decode return msg def send(self, msg): """Queues a message for sending. Real transmission is done in wait method. Sends directly if uWSGI version is new enough.""" if self._req_ctx is not None: self._send(msg) else: self._send_queue.put(msg) self._event.set() def wait(self): """Waits and returns received messages. If running in compatibility mode for older uWSGI versions, it also sends messages that have been queued by send(). A return value of None means that connection was closed. This must be called repeatedly. For uWSGI < 2.1.x it must be called from the main greenlet.""" while True: if self._req_ctx is not None: try: msg = uwsgi.websocket_recv(request_context=self._req_ctx) except OSError: # connection closed self.close() return None return self._decode_received(msg) else: if self.received_messages: return self.received_messages.pop(0) # we wake up at least every 3 seconds to let uWSGI # do its ping/ponging event_set = self._event.wait(timeout=3) if event_set: self._event.clear() # maybe there is something to send msgs = [] while True: try: msgs.append(self._send_queue.get(block=False)) except gevent.queue.Empty: break for msg in msgs: try: self._send(msg) except OSError: self.close() return None # maybe there is something to receive, if not, at least # ensure uWSGI does its ping/ponging while True: try: msg = uwsgi.websocket_recv_nb() except OSError: # connection closed self.close() return None if msg: # message available self.received_messages.append( self._decode_received(msg)) else: break if self.received_messages: return self.received_messages.pop(0) _async = { 'thread': Thread, 'queue': queue.JoinableQueue, 'queue_empty': queue.Empty, 'event': Event, 'websocket': uWSGIWebSocket if _websocket_available else None, 'sleep': gevent.sleep, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734479853.0 python_engineio-4.12.1/src/engineio/async_drivers/sanic.py0000664000175000017500000001061714730407755023314 0ustar00miguelmiguelimport sys from urllib.parse import urlsplit try: # pragma: no cover from sanic.response import HTTPResponse try: from sanic.server.protocols.websocket_protocol import WebSocketProtocol except ImportError: from sanic.websocket import WebSocketProtocol except ImportError: HTTPResponse = None WebSocketProtocol = None def create_route(app, engineio_server, engineio_endpoint): # pragma: no cover """This function sets up the engine.io endpoint as a route for the application. Note that both GET and POST requests must be hooked up on the engine.io endpoint. """ app.add_route(engineio_server.handle_request, engineio_endpoint, methods=['GET', 'POST', 'OPTIONS']) try: app.enable_websocket() except AttributeError: # ignore, this version does not support websocket pass def translate_request(request): # pragma: no cover """This function takes the arguments passed to the request handler and uses them to generate a WSGI compatible environ dictionary. """ class AwaitablePayload: def __init__(self, payload): self.payload = payload or b'' async def read(self, length=None): if length is None: r = self.payload self.payload = b'' else: r = self.payload[:length] self.payload = self.payload[length:] return r uri_parts = urlsplit(request.url) environ = { 'wsgi.input': AwaitablePayload(request.body), 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'SERVER_SOFTWARE': 'sanic', 'REQUEST_METHOD': request.method, 'QUERY_STRING': uri_parts.query or '', 'RAW_URI': request.url, 'SERVER_PROTOCOL': 'HTTP/' + request.version, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '0', 'SERVER_NAME': 'sanic', 'SERVER_PORT': '0', 'sanic.request': request } for hdr_name, hdr_value in request.headers.items(): hdr_name = hdr_name.upper() if hdr_name == 'CONTENT-TYPE': environ['CONTENT_TYPE'] = hdr_value continue elif hdr_name == 'CONTENT-LENGTH': environ['CONTENT_LENGTH'] = hdr_value continue key = 'HTTP_%s' % hdr_name.replace('-', '_') if key in environ: hdr_value = f'{environ[key]},{hdr_value}' environ[key] = hdr_value environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') path_info = uri_parts.path environ['PATH_INFO'] = path_info environ['SCRIPT_NAME'] = '' return environ def make_response(status, headers, payload, environ): # pragma: no cover """This function generates an appropriate response object for this async mode. """ headers_dict = {} content_type = None for h in headers: if h[0].lower() == 'content-type': content_type = h[1] else: headers_dict[h[0]] = h[1] return HTTPResponse(body=payload, content_type=content_type, status=int(status.split()[0]), headers=headers_dict) class WebSocket: # pragma: no cover """ This wrapper class provides a sanic WebSocket interface that is somewhat compatible with eventlet's implementation. """ def __init__(self, handler, server): self.handler = handler self.server = server self._sock = None async def __call__(self, environ): request = environ['sanic.request'] protocol = request.transport.get_protocol() self._sock = await protocol.websocket_handshake(request) self.environ = environ await self.handler(self) return self.server._ok() async def close(self): await self._sock.close() async def send(self, message): await self._sock.send(message) async def wait(self): data = await self._sock.recv() if not isinstance(data, bytes) and \ not isinstance(data, str): raise OSError() return data _async = { 'asyncio': True, 'create_route': create_route, 'translate_request': translate_request, 'make_response': make_response, 'websocket': WebSocket if WebSocketProtocol else None, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/src/engineio/async_drivers/threading.py0000664000175000017500000000071714707424343024160 0ustar00miguelmiguelimport queue import threading import time from engineio.async_drivers._websocket_wsgi import SimpleWebSocketWSGI class DaemonThread(threading.Thread): # pragma: no cover def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, daemon=True) _async = { 'thread': DaemonThread, 'queue': queue.Queue, 'queue_empty': queue.Empty, 'event': threading.Event, 'websocket': SimpleWebSocketWSGI, 'sleep': time.sleep, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/async_drivers/tornado.py0000664000175000017500000001340514726642012023653 0ustar00miguelmiguelimport asyncio import sys from urllib.parse import urlsplit from .. import exceptions import tornado.web import tornado.websocket def get_tornado_handler(engineio_server): class Handler(tornado.websocket.WebSocketHandler): # pragma: no cover def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if isinstance(engineio_server.cors_allowed_origins, str): if engineio_server.cors_allowed_origins == '*': self.allowed_origins = None else: self.allowed_origins = [ engineio_server.cors_allowed_origins] else: self.allowed_origins = engineio_server.cors_allowed_origins self.receive_queue = asyncio.Queue() async def get(self, *args, **kwargs): if self.request.headers.get('Upgrade', '').lower() == 'websocket': ret = super().get(*args, **kwargs) if asyncio.iscoroutine(ret): await ret else: await engineio_server.handle_request(self) async def open(self, *args, **kwargs): # this is the handler for the websocket request asyncio.ensure_future(engineio_server.handle_request(self)) async def post(self, *args, **kwargs): await engineio_server.handle_request(self) async def options(self, *args, **kwargs): await engineio_server.handle_request(self) async def on_message(self, message): await self.receive_queue.put(message) async def get_next_message(self): return await self.receive_queue.get() def on_close(self): self.receive_queue.put_nowait(None) def check_origin(self, origin): if self.allowed_origins is None or origin in self.allowed_origins: return True return super().check_origin(origin) def get_compression_options(self): # enable compression return {} return Handler def translate_request(handler): """This function takes the arguments passed to the request handler and uses them to generate a WSGI compatible environ dictionary. """ class AwaitablePayload: def __init__(self, payload): self.payload = payload or b'' async def read(self, length=None): if length is None: r = self.payload self.payload = b'' else: r = self.payload[:length] self.payload = self.payload[length:] return r payload = handler.request.body uri_parts = urlsplit(handler.request.path) full_uri = handler.request.path if handler.request.query: # pragma: no cover full_uri += '?' + handler.request.query environ = { 'wsgi.input': AwaitablePayload(payload), 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'SERVER_SOFTWARE': 'aiohttp', 'REQUEST_METHOD': handler.request.method, 'QUERY_STRING': handler.request.query or '', 'RAW_URI': full_uri, 'SERVER_PROTOCOL': 'HTTP/%s' % handler.request.version, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '0', 'SERVER_NAME': 'aiohttp', 'SERVER_PORT': '0', 'tornado.handler': handler } for hdr_name, hdr_value in handler.request.headers.items(): hdr_name = hdr_name.upper() if hdr_name == 'CONTENT-TYPE': environ['CONTENT_TYPE'] = hdr_value continue elif hdr_name == 'CONTENT-LENGTH': environ['CONTENT_LENGTH'] = hdr_value continue key = 'HTTP_%s' % hdr_name.replace('-', '_') environ[key] = hdr_value environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') path_info = uri_parts.path environ['PATH_INFO'] = path_info environ['SCRIPT_NAME'] = '' return environ def make_response(status, headers, payload, environ): """This function generates an appropriate response object for this async mode. """ tornado_handler = environ['tornado.handler'] try: tornado_handler.set_status(int(status.split()[0])) except RuntimeError: # pragma: no cover # for websocket connections Tornado does not accept a response, since # it already emitted the 101 status code return for header, value in headers: tornado_handler.set_header(header, value) tornado_handler.write(payload) tornado_handler.finish() class WebSocket: # pragma: no cover """ This wrapper class provides a tornado WebSocket interface that is somewhat compatible with eventlet's implementation. """ def __init__(self, handler, server): self.handler = handler self.tornado_handler = None async def __call__(self, environ): self.tornado_handler = environ['tornado.handler'] self.environ = environ await self.handler(self) async def close(self): self.tornado_handler.close() async def send(self, message): try: self.tornado_handler.write_message( message, binary=isinstance(message, bytes)) except tornado.websocket.WebSocketClosedError: raise exceptions.EngineIOError() async def wait(self): msg = await self.tornado_handler.get_next_message() if not isinstance(msg, bytes) and \ not isinstance(msg, str): raise OSError() return msg _async = { 'asyncio': True, 'translate_request': translate_request, 'make_response': make_response, 'websocket': WebSocket, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/src/engineio/async_server.py0000664000175000017500000006542414730320266022044 0ustar00miguelmiguelimport asyncio import urllib from . import base_server from . import exceptions from . import packet from . import async_socket # this set is used to keep references to background tasks to prevent them from # being garbage collected mid-execution. Solution taken from # https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task task_reference_holder = set() class AsyncServer(base_server.BaseServer): """An Engine.IO server for asyncio. This class implements a fully compliant Engine.IO web server with support for websocket and long-polling transports, compatible with the asyncio framework on Python 3.5 or newer. :param async_mode: The asynchronous model to use. See the Deployment section in the documentation for a description of the available options. Valid async modes are "aiohttp", "sanic", "tornado" and "asgi". If this argument is not given, "aiohttp" is tried first, followed by "sanic", "tornado", and finally "asgi". The first async mode that has all its dependencies installed is the one that is chosen. :param ping_interval: The interval in seconds at which the server pings the client. The default is 25 seconds. For advanced control, a two element tuple can be given, where the first number is the ping interval and the second is a grace period added by the server. :param ping_timeout: The time in seconds that the client waits for the server to respond before disconnecting. The default is 20 seconds. :param max_http_buffer_size: The maximum size that is accepted for incoming messages. The default is 1,000,000 bytes. In spite of its name, the value set in this argument is enforced for HTTP long-polling and WebSocket connections. :param allow_upgrades: Whether to allow transport upgrades or not. :param http_compression: Whether to compress packages when using the polling transport. :param compression_threshold: Only compress messages when their byte size is greater than this value. :param cookie: If set to a string, it is the name of the HTTP cookie the server sends back tot he client containing the client session id. If set to a dictionary, the ``'name'`` key contains the cookie name and other keys define cookie attributes, where the value of each attribute can be a string, a callable with no arguments, or a boolean. If set to ``None`` (the default), a cookie is not sent to the client. :param cors_allowed_origins: Origin or list of origins that are allowed to connect to this server. Only the same origin is allowed by default. Set this argument to ``'*'`` to allow all origins, or to ``[]`` to disable CORS handling. :param cors_credentials: Whether credentials (cookies, authentication) are allowed in requests to this server. :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. Note that fatal errors are logged even when ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param async_handlers: If set to ``True``, run message event handlers in non-blocking threads. To run handlers synchronously, set to ``False``. The default is ``True``. :param monitor_clients: If set to ``True``, a background task will ensure inactive clients are closed. Set to ``False`` to disable the monitoring task (not recommended). The default is ``True``. :param transports: The list of allowed transports. Valid transports are ``'polling'`` and ``'websocket'``. Defaults to ``['polling', 'websocket']``. :param kwargs: Reserved for future extensions, any additional parameters given as keyword arguments will be silently ignored. """ def is_asyncio_based(self): return True def async_modes(self): return ['aiohttp', 'sanic', 'tornado', 'asgi'] def attach(self, app, engineio_path='engine.io'): """Attach the Engine.IO server to an application.""" engineio_path = engineio_path.strip('/') self._async['create_route'](app, self, f'/{engineio_path}/') async def send(self, sid, data): """Send a message to a client. :param sid: The session id of the recipient client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. Note: this method is a coroutine. """ await self.send_packet(sid, packet.Packet(packet.MESSAGE, data=data)) async def send_packet(self, sid, pkt): """Send a raw packet to a client. :param sid: The session id of the recipient client. :param pkt: The packet to send to the client. Note: this method is a coroutine. """ try: socket = self._get_socket(sid) except KeyError: # the socket is not available self.logger.warning('Cannot send to sid %s', sid) return await socket.send(pkt) async def get_session(self, sid): """Return the user session for a client. :param sid: The session id of the client. The return value is a dictionary. Modifications made to this dictionary are not guaranteed to be preserved. If you want to modify the user session, use the ``session`` context manager instead. """ socket = self._get_socket(sid) return socket.session async def save_session(self, sid, session): """Store the user session for a client. :param sid: The session id of the client. :param session: The session dictionary. """ socket = self._get_socket(sid) socket.session = session def session(self, sid): """Return the user session for a client with context manager syntax. :param sid: The session id of the client. This is a context manager that returns the user session dictionary for the client. Any changes that are made to this dictionary inside the context manager block are saved back to the session. Example usage:: @eio.on('connect') def on_connect(sid, environ): username = authenticate_user(environ) if not username: return False with eio.session(sid) as session: session['username'] = username @eio.on('message') def on_message(sid, msg): async with eio.session(sid) as session: print('received message from ', session['username']) """ class _session_context_manager: def __init__(self, server, sid): self.server = server self.sid = sid self.session = None async def __aenter__(self): self.session = await self.server.get_session(sid) return self.session async def __aexit__(self, *args): await self.server.save_session(sid, self.session) return _session_context_manager(self, sid) async def disconnect(self, sid=None): """Disconnect a client. :param sid: The session id of the client to close. If this parameter is not given, then all clients are closed. Note: this method is a coroutine. """ if sid is not None: try: socket = self._get_socket(sid) except KeyError: # pragma: no cover # the socket was already closed or gone pass else: await socket.close(reason=self.reason.SERVER_DISCONNECT) if sid in self.sockets: # pragma: no cover del self.sockets[sid] else: await asyncio.wait([ asyncio.create_task(client.close( reason=self.reason.SERVER_DISCONNECT)) for client in self.sockets.values() ]) self.sockets = {} async def handle_request(self, *args, **kwargs): """Handle an HTTP request from the client. This is the entry point of the Engine.IO application. This function returns the HTTP response to deliver to the client. Note: this method is a coroutine. """ translate_request = self._async['translate_request'] if asyncio.iscoroutinefunction(translate_request): environ = await translate_request(*args, **kwargs) else: environ = translate_request(*args, **kwargs) if self.cors_allowed_origins != []: # Validate the origin header if present # This is important for WebSocket more than for HTTP, since # browsers only apply CORS controls to HTTP. origin = environ.get('HTTP_ORIGIN') if origin: allowed_origins = self._cors_allowed_origins(environ) if allowed_origins is not None and origin not in \ allowed_origins: self._log_error_once( origin + ' is not an accepted origin.', 'bad-origin') return await self._make_response( self._bad_request( origin + ' is not an accepted origin.'), environ) method = environ['REQUEST_METHOD'] query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) sid = query['sid'][0] if 'sid' in query else None jsonp = False jsonp_index = None # make sure the client uses an allowed transport transport = query.get('transport', ['polling'])[0] if transport not in self.transports: self._log_error_once('Invalid transport', 'bad-transport') return await self._make_response( self._bad_request('Invalid transport'), environ) # make sure the client speaks a compatible Engine.IO version sid = query['sid'][0] if 'sid' in query else None if sid is None and query.get('EIO') != ['4']: self._log_error_once( 'The client is using an unsupported version of the Socket.IO ' 'or Engine.IO protocols', 'bad-version' ) return await self._make_response(self._bad_request( 'The client is using an unsupported version of the Socket.IO ' 'or Engine.IO protocols' ), environ) if 'j' in query: jsonp = True try: jsonp_index = int(query['j'][0]) except (ValueError, KeyError, IndexError): # Invalid JSONP index number pass if jsonp and jsonp_index is None: self._log_error_once('Invalid JSONP index number', 'bad-jsonp-index') r = self._bad_request('Invalid JSONP index number') elif method == 'GET': upgrade_header = environ.get('HTTP_UPGRADE').lower() \ if 'HTTP_UPGRADE' in environ else None if sid is None: # transport must be one of 'polling' or 'websocket'. # if 'websocket', the HTTP_UPGRADE header must match. if transport == 'polling' \ or transport == upgrade_header == 'websocket': r = await self._handle_connect(environ, transport, jsonp_index) else: self._log_error_once('Invalid websocket upgrade', 'bad-upgrade') r = self._bad_request('Invalid websocket upgrade') else: if sid not in self.sockets: self._log_error_once(f'Invalid session {sid}', 'bad-sid') r = self._bad_request(f'Invalid session {sid}') else: try: socket = self._get_socket(sid) except KeyError as e: # pragma: no cover self._log_error_once(f'{e} {sid}', 'bad-sid') r = self._bad_request(f'{e} {sid}') else: if self.transport(sid) != transport and \ transport != upgrade_header: self._log_error_once( f'Invalid transport for session {sid}', 'bad-transport') r = self._bad_request('Invalid transport') else: try: packets = await socket.handle_get_request( environ) if isinstance(packets, list): r = self._ok(packets, jsonp_index=jsonp_index) else: r = packets except exceptions.EngineIOError: if sid in self.sockets: # pragma: no cover await self.disconnect(sid) r = self._bad_request() if sid in self.sockets and \ self.sockets[sid].closed: del self.sockets[sid] elif method == 'POST': if sid is None or sid not in self.sockets: self._log_error_once(f'Invalid session {sid}', 'bad-sid') r = self._bad_request(f'Invalid session {sid}') else: socket = self._get_socket(sid) try: await socket.handle_post_request(environ) r = self._ok(jsonp_index=jsonp_index) except exceptions.EngineIOError: if sid in self.sockets: # pragma: no cover await self.disconnect(sid) r = self._bad_request() except: # pragma: no cover # for any other unexpected errors, we log the error # and keep going self.logger.exception('post request handler error') r = self._ok(jsonp_index=jsonp_index) elif method == 'OPTIONS': r = self._ok() else: self.logger.warning('Method %s not supported', method) r = self._method_not_found() if not isinstance(r, dict): return r if self.http_compression and \ len(r['response']) >= self.compression_threshold: encodings = [e.split(';')[0].strip() for e in environ.get('HTTP_ACCEPT_ENCODING', '').split(',')] for encoding in encodings: if encoding in self.compression_methods: r['response'] = \ getattr(self, '_' + encoding)(r['response']) r['headers'] += [('Content-Encoding', encoding)] break return await self._make_response(r, environ) async def shutdown(self): """Stop Socket.IO background tasks. This method stops background activity initiated by the Socket.IO server. It must be called before shutting down the web server. """ self.logger.info('Socket.IO is shutting down') if self.service_task_event: # pragma: no cover self.service_task_event.set() await self.service_task_handle self.service_task_handle = None def start_background_task(self, target, *args, **kwargs): """Start a background task using the appropriate async model. This is a utility function that applications can use to start a background task using the method that is compatible with the selected async mode. :param target: the target function to execute. :param args: arguments to pass to the function. :param kwargs: keyword arguments to pass to the function. The return value is a ``asyncio.Task`` object. """ return asyncio.ensure_future(target(*args, **kwargs)) async def sleep(self, seconds=0): """Sleep for the requested amount of time using the appropriate async model. This is a utility function that applications can use to put a task to sleep without having to worry about using the correct call for the selected async mode. Note: this method is a coroutine. """ return await asyncio.sleep(seconds) def create_queue(self, *args, **kwargs): """Create a queue object using the appropriate async model. This is a utility function that applications can use to create a queue without having to worry about using the correct call for the selected async mode. For asyncio based async modes, this returns an instance of ``asyncio.Queue``. """ return asyncio.Queue(*args, **kwargs) def get_queue_empty_exception(self): """Return the queue empty exception for the appropriate async model. This is a utility function that applications can use to work with a queue without having to worry about using the correct call for the selected async mode. For asyncio based async modes, this returns an instance of ``asyncio.QueueEmpty``. """ return asyncio.QueueEmpty def create_event(self, *args, **kwargs): """Create an event object using the appropriate async model. This is a utility function that applications can use to create an event without having to worry about using the correct call for the selected async mode. For asyncio based async modes, this returns an instance of ``asyncio.Event``. """ return asyncio.Event(*args, **kwargs) async def _make_response(self, response_dict, environ): cors_headers = self._cors_headers(environ) make_response = self._async['make_response'] if asyncio.iscoroutinefunction(make_response): response = await make_response( response_dict['status'], response_dict['headers'] + cors_headers, response_dict['response'], environ) else: response = make_response( response_dict['status'], response_dict['headers'] + cors_headers, response_dict['response'], environ) return response async def _handle_connect(self, environ, transport, jsonp_index=None): """Handle a client connection request.""" if self.start_service_task: # start the service task to monitor connected clients self.start_service_task = False self.service_task_handle = self.start_background_task( self._service_task) sid = self.generate_id() s = async_socket.AsyncSocket(self, sid) self.sockets[sid] = s pkt = packet.Packet(packet.OPEN, { 'sid': sid, 'upgrades': self._upgrades(sid, transport), 'pingTimeout': int(self.ping_timeout * 1000), 'pingInterval': int( self.ping_interval + self.ping_interval_grace_period) * 1000, 'maxPayload': self.max_http_buffer_size, }) await s.send(pkt) s.schedule_ping() ret = await self._trigger_event('connect', sid, environ, run_async=False) if ret is not None and ret is not True: del self.sockets[sid] self.logger.warning('Application rejected connection') return self._unauthorized(ret or None) if transport == 'websocket': ret = await s.handle_get_request(environ) if s.closed and sid in self.sockets: # websocket connection ended, so we are done del self.sockets[sid] return ret else: s.connected = True headers = None if self.cookie: if isinstance(self.cookie, dict): headers = [( 'Set-Cookie', self._generate_sid_cookie(sid, self.cookie) )] else: headers = [( 'Set-Cookie', self._generate_sid_cookie(sid, { 'name': self.cookie, 'path': '/', 'SameSite': 'Lax' }) )] try: return self._ok(await s.poll(), headers=headers, jsonp_index=jsonp_index) except exceptions.QueueEmpty: return self._bad_request() async def _trigger_event(self, event, *args, **kwargs): """Invoke an event handler.""" run_async = kwargs.pop('run_async', False) ret = None if event in self.handlers: if asyncio.iscoroutinefunction(self.handlers[event]): async def run_async_handler(): try: try: return await self.handlers[event](*args) except TypeError: if event == 'disconnect' and \ len(args) == 2: # pragma: no branch # legacy disconnect events do not have a reason # argument return await self.handlers[event](args[0]) else: # pragma: no cover raise except asyncio.CancelledError: # pragma: no cover pass except: self.logger.exception(event + ' async handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False if run_async: ret = self.start_background_task(run_async_handler) task_reference_holder.add(ret) ret.add_done_callback(task_reference_holder.discard) else: ret = await run_async_handler() else: async def run_sync_handler(): try: try: return self.handlers[event](*args) except TypeError: if event == 'disconnect' and \ len(args) == 2: # pragma: no branch # legacy disconnect events do not have a reason # argument return self.handlers[event](args[0]) else: # pragma: no cover raise except: self.logger.exception(event + ' handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False if run_async: ret = self.start_background_task(run_sync_handler) task_reference_holder.add(ret) ret.add_done_callback(task_reference_holder.discard) else: ret = await run_sync_handler() return ret async def _service_task(self): # pragma: no cover """Monitor connected clients and clean up those that time out.""" loop = asyncio.get_running_loop() self.service_task_event = self.create_event() while not self.service_task_event.is_set(): if len(self.sockets) == 0: # nothing to do try: await asyncio.wait_for(self.service_task_event.wait(), timeout=self.ping_timeout) break except asyncio.TimeoutError: continue # go through the entire client list in a ping interval cycle sleep_interval = self.ping_timeout / len(self.sockets) try: # iterate over the current clients for s in self.sockets.copy().values(): if s.closed: try: del self.sockets[s.sid] except KeyError: # the socket could have also been removed by # the _get_socket() method from another thread pass elif not s.closing: await s.check_ping_timeout() try: await asyncio.wait_for(self.service_task_event.wait(), timeout=sleep_interval) raise KeyboardInterrupt() except asyncio.TimeoutError: continue except ( SystemExit, KeyboardInterrupt, asyncio.CancelledError, GeneratorExit, ): self.logger.info('service task canceled') break except: if loop.is_closed(): self.logger.info('event loop is closed, exiting service ' 'task') break # an unexpected exception has occurred, log it and continue self.logger.exception('service task exception') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977320.0 python_engineio-4.12.1/src/engineio/async_socket.py0000664000175000017500000002473315010141050022004 0ustar00miguelmiguelimport asyncio import sys import time from . import base_socket from . import exceptions from . import packet from . import payload class AsyncSocket(base_socket.BaseSocket): async def poll(self): """Wait for packets to send to the client.""" try: packets = [await asyncio.wait_for( self.queue.get(), self.server.ping_interval + self.server.ping_timeout)] self.queue.task_done() except (asyncio.TimeoutError, asyncio.CancelledError): raise exceptions.QueueEmpty() if packets == [None]: return [] while True: try: pkt = self.queue.get_nowait() self.queue.task_done() if pkt is None: self.queue.put_nowait(None) break packets.append(pkt) except asyncio.QueueEmpty: break return packets async def receive(self, pkt): """Receive packet from the client.""" self.server.logger.info('%s: Received packet %s data %s', self.sid, packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') if pkt.packet_type == packet.PONG: self.schedule_ping() elif pkt.packet_type == packet.MESSAGE: await self.server._trigger_event( 'message', self.sid, pkt.data, run_async=self.server.async_handlers) elif pkt.packet_type == packet.UPGRADE: await self.send(packet.Packet(packet.NOOP)) elif pkt.packet_type == packet.CLOSE: await self.close(wait=False, abort=True, reason=self.server.reason.CLIENT_DISCONNECT) else: raise exceptions.UnknownPacketError() async def check_ping_timeout(self): """Make sure the client is still sending pings.""" if self.closed: raise exceptions.SocketIsClosedError() if self.last_ping and \ time.time() - self.last_ping > self.server.ping_timeout: self.server.logger.info('%s: Client is gone, closing socket', self.sid) # Passing abort=False here will cause close() to write a # CLOSE packet. This has the effect of updating half-open sockets # to their correct state of disconnected await self.close(wait=False, abort=False, reason=self.server.reason.PING_TIMEOUT) return False return True async def send(self, pkt): """Send a packet to the client.""" if not await self.check_ping_timeout(): return else: await self.queue.put(pkt) self.server.logger.info('%s: Sending packet %s data %s', self.sid, packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') async def handle_get_request(self, environ): """Handle a long-polling GET request from the client.""" connections = [ s.strip() for s in environ.get('HTTP_CONNECTION', '').lower().split(',')] transport = environ.get('HTTP_UPGRADE', '').lower() if 'upgrade' in connections and transport in self.upgrade_protocols: self.server.logger.info('%s: Received request to upgrade to %s', self.sid, transport) return await getattr(self, '_upgrade_' + transport)(environ) if self.upgrading or self.upgraded: # we are upgrading to WebSocket, do not return any more packets # through the polling endpoint return [packet.Packet(packet.NOOP)] try: packets = await self.poll() except exceptions.QueueEmpty: exc = sys.exc_info() await self.close(wait=False, reason=self.server.reason.TRANSPORT_ERROR) raise exc[1].with_traceback(exc[2]) return packets async def handle_post_request(self, environ): """Handle a long-polling POST request from the client.""" length = int(environ.get('CONTENT_LENGTH', '0')) if length > self.server.max_http_buffer_size: raise exceptions.ContentTooLongError() else: body = (await environ['wsgi.input'].read(length)).decode('utf-8') p = payload.Payload(encoded_payload=body) for pkt in p.packets: await self.receive(pkt) async def close(self, wait=True, abort=False, reason=None): """Close the socket connection.""" if not self.closed and not self.closing: self.closing = True await self.server._trigger_event( 'disconnect', self.sid, reason or self.server.reason.SERVER_DISCONNECT, run_async=False) if not abort: await self.send(packet.Packet(packet.CLOSE)) self.closed = True if wait: await self.queue.join() def schedule_ping(self): self.server.start_background_task(self._send_ping) async def _send_ping(self): self.last_ping = None await asyncio.sleep(self.server.ping_interval) if not self.closing and not self.closed: self.last_ping = time.time() await self.send(packet.Packet(packet.PING)) async def _upgrade_websocket(self, environ): """Upgrade the connection from polling to websocket.""" if self.upgraded: raise OSError('Socket has been upgraded already') if self.server._async['websocket'] is None: # the selected async mode does not support websocket return self.server._bad_request() ws = self.server._async['websocket']( self._websocket_handler, self.server) return await ws(environ) async def _websocket_handler(self, ws): """Engine.IO handler for websocket transport.""" async def websocket_wait(): data = await ws.wait() if data and len(data) > self.server.max_http_buffer_size: raise ValueError('packet is too large') return data if self.connected: # the socket was already connected, so this is an upgrade self.upgrading = True # hold packet sends during the upgrade try: pkt = await websocket_wait() except OSError: # pragma: no cover return decoded_pkt = packet.Packet(encoded_packet=pkt) if decoded_pkt.packet_type != packet.PING or \ decoded_pkt.data != 'probe': self.server.logger.info( '%s: Failed websocket upgrade, no PING packet', self.sid) self.upgrading = False return await ws.send(packet.Packet(packet.PONG, data='probe').encode()) await self.queue.put(packet.Packet(packet.NOOP)) # end poll try: pkt = await websocket_wait() except OSError: # pragma: no cover self.upgrading = False return decoded_pkt = packet.Packet(encoded_packet=pkt) if decoded_pkt.packet_type != packet.UPGRADE: self.upgraded = False self.server.logger.info( ('%s: Failed websocket upgrade, expected UPGRADE packet, ' 'received %s instead.'), self.sid, pkt) self.upgrading = False return self.upgraded = True self.upgrading = False else: self.connected = True self.upgraded = True # start separate writer thread async def writer(): while True: packets = None try: packets = await self.poll() except exceptions.QueueEmpty: break if not packets: # empty packet list returned -> connection closed break try: for pkt in packets: await ws.send(pkt.encode()) except: break await ws.close() writer_task = asyncio.ensure_future(writer()) self.server.logger.info( '%s: Upgrade to websocket successful', self.sid) while True: p = None wait_task = asyncio.ensure_future(websocket_wait()) try: p = await asyncio.wait_for( wait_task, self.server.ping_interval + self.server.ping_timeout) except asyncio.CancelledError: # pragma: no cover # there is a bug (https://bugs.python.org/issue30508) in # asyncio that causes a "Task exception never retrieved" error # to appear when wait_task raises an exception before it gets # cancelled. Calling wait_task.exception() prevents the error # from being issued in Python 3.6, but causes other errors in # other versions, so we run it with all errors suppressed and # hope for the best. try: wait_task.exception() except: pass break except: break if p is None: # connection closed by client break pkt = packet.Packet(encoded_packet=p) try: await self.receive(pkt) except exceptions.UnknownPacketError: # pragma: no cover pass except exceptions.SocketIsClosedError: # pragma: no cover self.server.logger.info('Receive error -- socket is closed') break except: # pragma: no cover # if we get an unexpected exception we log the error and exit # the connection properly self.server.logger.exception('Unknown receive error') await self.queue.put(None) # unlock the writer task so it can exit await asyncio.wait_for(writer_task, timeout=None) await self.close(wait=False, abort=True, reason=self.server.reason.TRANSPORT_CLOSE) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/src/engineio/base_client.py0000664000175000017500000001231214730320266021575 0ustar00miguelmiguelimport logging import signal import threading import time import urllib from . import packet default_logger = logging.getLogger('engineio.client') connected_clients = [] def signal_handler(sig, frame): """SIGINT handler. Disconnect all active clients and then invoke the original signal handler. """ for client in connected_clients[:]: if not client.is_asyncio_based(): client.disconnect() if callable(original_signal_handler): return original_signal_handler(sig, frame) else: # pragma: no cover # Handle case where no original SIGINT handler was present. return signal.default_int_handler(sig, frame) original_signal_handler = None class BaseClient: event_names = ['connect', 'disconnect', 'message'] class reason: """Disconnection reasons.""" #: Client-initiated disconnection. CLIENT_DISCONNECT = 'client disconnect' #: Server-initiated disconnection. SERVER_DISCONNECT = 'server disconnect' #: Transport error. TRANSPORT_ERROR = 'transport error' def __init__(self, logger=False, json=None, request_timeout=5, http_session=None, ssl_verify=True, handle_sigint=True, websocket_extra_options=None, timestamp_requests=True): global original_signal_handler if handle_sigint and original_signal_handler is None and \ threading.current_thread() == threading.main_thread(): original_signal_handler = signal.signal(signal.SIGINT, signal_handler) self.handlers = {} self.base_url = None self.transports = None self.current_transport = None self.sid = None self.upgrades = None self.ping_interval = None self.ping_timeout = None self.http = http_session self.external_http = http_session is not None self.handle_sigint = handle_sigint self.ws = None self.read_loop_task = None self.write_loop_task = None self.queue = None self.state = 'disconnected' self.ssl_verify = ssl_verify self.websocket_extra_options = websocket_extra_options or {} self.timestamp_requests = timestamp_requests if json is not None: packet.Packet.json = json if not isinstance(logger, bool): self.logger = logger else: self.logger = default_logger if self.logger.level == logging.NOTSET: if logger: self.logger.setLevel(logging.INFO) else: self.logger.setLevel(logging.ERROR) self.logger.addHandler(logging.StreamHandler()) self.request_timeout = request_timeout def is_asyncio_based(self): return False def on(self, event, handler=None): """Register an event handler. :param event: The event name. Can be ``'connect'``, ``'message'`` or ``'disconnect'``. :param handler: The function that should be invoked to handle the event. When this parameter is not given, the method acts as a decorator for the handler function. Example usage:: # as a decorator: @eio.on('connect') def connect_handler(): print('Connection request') # as a method: def message_handler(msg): print('Received message: ', msg) eio.send('response') eio.on('message', message_handler) """ if event not in self.event_names: raise ValueError('Invalid event') def set_handler(handler): self.handlers[event] = handler return handler if handler is None: return set_handler set_handler(handler) def transport(self): """Return the name of the transport currently in use. The possible values returned by this function are ``'polling'`` and ``'websocket'``. """ return self.current_transport def _reset(self): self.state = 'disconnected' self.sid = None def _get_engineio_url(self, url, engineio_path, transport): """Generate the Engine.IO connection URL.""" engineio_path = engineio_path.strip('/') parsed_url = urllib.parse.urlparse(url) if transport == 'polling': scheme = 'http' elif transport == 'websocket': scheme = 'ws' else: # pragma: no cover raise ValueError('invalid transport') if parsed_url.scheme in ['https', 'wss']: scheme += 's' return ('{scheme}://{netloc}/{path}/?{query}' '{sep}transport={transport}&EIO=4').format( scheme=scheme, netloc=parsed_url.netloc, path=engineio_path, query=parsed_url.query, sep='&' if parsed_url.query else '', transport=transport) def _get_url_timestamp(self): """Generate the Engine.IO query string timestamp.""" if not self.timestamp_requests: return '' return '&t=' + str(time.time()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744471505.0 python_engineio-4.12.1/src/engineio/base_server.py0000664000175000017500000003447514776502721021653 0ustar00miguelmiguelimport base64 import gzip import importlib import io import logging import secrets import zlib from . import packet from . import payload default_logger = logging.getLogger('engineio.server') class BaseServer: compression_methods = ['gzip', 'deflate'] event_names = ['connect', 'disconnect', 'message'] valid_transports = ['polling', 'websocket'] _default_monitor_clients = True sequence_number = 0 class reason: """Disconnection reasons.""" #: Server-initiated disconnection. SERVER_DISCONNECT = 'server disconnect' #: Client-initiated disconnection. CLIENT_DISCONNECT = 'client disconnect' #: Ping timeout. PING_TIMEOUT = 'ping timeout' #: Transport close. TRANSPORT_CLOSE = 'transport close' #: Transport error. TRANSPORT_ERROR = 'transport error' def __init__(self, async_mode=None, ping_interval=25, ping_timeout=20, max_http_buffer_size=1000000, allow_upgrades=True, http_compression=True, compression_threshold=1024, cookie=None, cors_allowed_origins=None, cors_credentials=True, logger=False, json=None, async_handlers=True, monitor_clients=None, transports=None, **kwargs): self.ping_timeout = ping_timeout if isinstance(ping_interval, tuple): self.ping_interval = ping_interval[0] self.ping_interval_grace_period = ping_interval[1] else: self.ping_interval = ping_interval self.ping_interval_grace_period = 0 self.max_http_buffer_size = max_http_buffer_size self.allow_upgrades = allow_upgrades self.http_compression = http_compression self.compression_threshold = compression_threshold self.cookie = cookie self.cors_allowed_origins = cors_allowed_origins self.cors_credentials = cors_credentials self.async_handlers = async_handlers self.sockets = {} self.handlers = {} self.log_message_keys = set() self.start_service_task = monitor_clients \ if monitor_clients is not None else self._default_monitor_clients self.service_task_handle = None self.service_task_event = None if json is not None: packet.Packet.json = json if not isinstance(logger, bool): self.logger = logger else: self.logger = default_logger if self.logger.level == logging.NOTSET: if logger: self.logger.setLevel(logging.INFO) else: self.logger.setLevel(logging.ERROR) self.logger.addHandler(logging.StreamHandler()) modes = self.async_modes() if async_mode is not None: modes = [async_mode] if async_mode in modes else [] self._async = None self.async_mode = None for mode in modes: try: self._async = importlib.import_module( 'engineio.async_drivers.' + mode)._async asyncio_based = self._async['asyncio'] \ if 'asyncio' in self._async else False if asyncio_based != self.is_asyncio_based(): continue # pragma: no cover self.async_mode = mode break except ImportError: pass if self.async_mode is None: raise ValueError('Invalid async_mode specified') if self.is_asyncio_based() and \ ('asyncio' not in self._async or not self._async['asyncio']): # pragma: no cover raise ValueError('The selected async_mode is not asyncio ' 'compatible') if not self.is_asyncio_based() and 'asyncio' in self._async and \ self._async['asyncio']: # pragma: no cover raise ValueError('The selected async_mode requires asyncio and ' 'must use the AsyncServer class') if transports is not None: if isinstance(transports, str): transports = [transports] transports = [transport for transport in transports if transport in self.valid_transports] if not transports: raise ValueError('No valid transports provided') self.transports = transports or self.valid_transports self.logger.info('Server initialized for %s.', self.async_mode) def is_asyncio_based(self): return False def async_modes(self): return ['eventlet', 'gevent_uwsgi', 'gevent', 'threading'] def on(self, event, handler=None): """Register an event handler. :param event: The event name. Can be ``'connect'``, ``'message'`` or ``'disconnect'``. :param handler: The function that should be invoked to handle the event. When this parameter is not given, the method acts as a decorator for the handler function. Example usage:: # as a decorator: @eio.on('connect') def connect_handler(sid, environ): print('Connection request') if environ['REMOTE_ADDR'] in blacklisted: return False # reject # as a method: def message_handler(sid, msg): print('Received message: ', msg) eio.send(sid, 'response') eio.on('message', message_handler) The handler function receives the ``sid`` (session ID) for the client as first argument. The ``'connect'`` event handler receives the WSGI environment as a second argument, and can return ``False`` to reject the connection. The ``'message'`` handler receives the message payload as a second argument. The ``'disconnect'`` handler does not take a second argument. """ if event not in self.event_names: raise ValueError('Invalid event') def set_handler(handler): self.handlers[event] = handler return handler if handler is None: return set_handler set_handler(handler) def transport(self, sid): """Return the name of the transport used by the client. The two possible values returned by this function are ``'polling'`` and ``'websocket'``. :param sid: The session of the client. """ return 'websocket' if self._get_socket(sid).upgraded else 'polling' def create_queue(self, *args, **kwargs): """Create a queue object using the appropriate async model. This is a utility function that applications can use to create a queue without having to worry about using the correct call for the selected async mode. """ return self._async['queue'](*args, **kwargs) def get_queue_empty_exception(self): """Return the queue empty exception for the appropriate async model. This is a utility function that applications can use to work with a queue without having to worry about using the correct call for the selected async mode. """ return self._async['queue_empty'] def create_event(self, *args, **kwargs): """Create an event object using the appropriate async model. This is a utility function that applications can use to create an event without having to worry about using the correct call for the selected async mode. """ return self._async['event'](*args, **kwargs) def generate_id(self): """Generate a unique session id.""" id = base64.b64encode( secrets.token_bytes(12) + self.sequence_number.to_bytes(3, 'big')) self.sequence_number = (self.sequence_number + 1) & 0xffffff return id.decode('utf-8').replace('/', '_').replace('+', '-') def _generate_sid_cookie(self, sid, attributes): """Generate the sid cookie.""" cookie = attributes.get('name', 'io') + '=' + sid for attribute, value in attributes.items(): if attribute == 'name': continue if callable(value): value = value() if value is True: cookie += '; ' + attribute else: cookie += '; ' + attribute + '=' + value return cookie def _upgrades(self, sid, transport): """Return the list of possible upgrades for a client connection.""" if not self.allow_upgrades or self._get_socket(sid).upgraded or \ transport == 'websocket': return [] if self._async['websocket'] is None: # pragma: no cover self._log_error_once( 'The WebSocket transport is not available, you must install a ' 'WebSocket server that is compatible with your async mode to ' 'enable it. See the documentation for details.', 'no-websocket') return [] return ['websocket'] def _get_socket(self, sid): """Return the socket object for a given session.""" try: s = self.sockets[sid] except KeyError: raise KeyError('Session not found') if s.closed: del self.sockets[sid] raise KeyError('Session is disconnected') return s def _ok(self, packets=None, headers=None, jsonp_index=None): """Generate a successful HTTP response.""" if packets is not None: if headers is None: headers = [] headers += [('Content-Type', 'text/plain; charset=UTF-8')] return {'status': '200 OK', 'headers': headers, 'response': payload.Payload(packets=packets).encode( jsonp_index=jsonp_index).encode('utf-8')} else: return {'status': '200 OK', 'headers': [('Content-Type', 'text/plain')], 'response': b'OK'} def _bad_request(self, message=None): """Generate a bad request HTTP error response.""" if message is None: message = 'Bad Request' message = packet.Packet.json.dumps(message) return {'status': '400 BAD REQUEST', 'headers': [('Content-Type', 'text/plain')], 'response': message.encode('utf-8')} def _method_not_found(self): """Generate a method not found HTTP error response.""" return {'status': '405 METHOD NOT FOUND', 'headers': [('Content-Type', 'text/plain')], 'response': b'Method Not Found'} def _unauthorized(self, message=None): """Generate a unauthorized HTTP error response.""" if message is None: message = 'Unauthorized' message = packet.Packet.json.dumps(message) return {'status': '401 UNAUTHORIZED', 'headers': [('Content-Type', 'application/json')], 'response': message.encode('utf-8')} def _cors_allowed_origins(self, environ): if self.cors_allowed_origins is None: allowed_origins = [] if 'wsgi.url_scheme' in environ and 'HTTP_HOST' in environ: allowed_origins.append('{scheme}://{host}'.format( scheme=environ['wsgi.url_scheme'], host=environ['HTTP_HOST'])) if 'HTTP_X_FORWARDED_PROTO' in environ or \ 'HTTP_X_FORWARDED_HOST' in environ: scheme = environ.get( 'HTTP_X_FORWARDED_PROTO', environ['wsgi.url_scheme']).split(',')[0].strip() allowed_origins.append('{scheme}://{host}'.format( scheme=scheme, host=environ.get( 'HTTP_X_FORWARDED_HOST', environ['HTTP_HOST']).split( ',')[0].strip())) elif self.cors_allowed_origins == '*': allowed_origins = None elif isinstance(self.cors_allowed_origins, str): allowed_origins = [self.cors_allowed_origins] elif callable(self.cors_allowed_origins): origin = environ.get('HTTP_ORIGIN') try: is_allowed = self.cors_allowed_origins(origin, environ) except TypeError: is_allowed = self.cors_allowed_origins(origin) allowed_origins = [origin] if is_allowed else [] else: allowed_origins = self.cors_allowed_origins return allowed_origins def _cors_headers(self, environ): """Return the cross-origin-resource-sharing headers.""" if self.cors_allowed_origins == []: # special case, CORS handling is completely disabled return [] headers = [] allowed_origins = self._cors_allowed_origins(environ) if 'HTTP_ORIGIN' in environ and \ (allowed_origins is None or environ['HTTP_ORIGIN'] in allowed_origins): headers = [('Access-Control-Allow-Origin', environ['HTTP_ORIGIN'])] if environ['REQUEST_METHOD'] == 'OPTIONS': headers += [('Access-Control-Allow-Methods', 'OPTIONS, GET, POST')] if 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' in environ: headers += [('Access-Control-Allow-Headers', environ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])] if self.cors_credentials: headers += [('Access-Control-Allow-Credentials', 'true')] return headers def _gzip(self, response): """Apply gzip compression to a response.""" bytesio = io.BytesIO() with gzip.GzipFile(fileobj=bytesio, mode='w') as gz: gz.write(response) return bytesio.getvalue() def _deflate(self, response): """Apply deflate compression to a response.""" return zlib.compress(response) def _log_error_once(self, message, message_key): """Log message with logging.ERROR level the first time, then log with given level.""" if message_key not in self.log_message_keys: self.logger.error(message + ' (further occurrences of this error ' 'will be logged with level INFO)') self.log_message_keys.add(message_key) else: self.logger.info(message) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/base_socket.py0000664000175000017500000000061714726642012021615 0ustar00miguelmiguelclass BaseSocket: upgrade_protocols = ['websocket'] def __init__(self, server, sid): self.server = server self.sid = sid self.queue = self.server.create_queue() self.last_ping = None self.connected = False self.upgrading = False self.upgraded = False self.closing = False self.closed = False self.session = {} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/src/engineio/client.py0000664000175000017500000006513314730320266020614 0ustar00miguelmiguelfrom base64 import b64encode from engineio.json import JSONDecodeError import logging import queue import ssl import threading import time import urllib try: import requests except ImportError: # pragma: no cover requests = None try: import websocket except ImportError: # pragma: no cover websocket = None from . import base_client from . import exceptions from . import packet from . import payload default_logger = logging.getLogger('engineio.client') class Client(base_client.BaseClient): """An Engine.IO client. This class implements a fully compliant Engine.IO web client with support for websocket and long-polling transports. :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. The default is ``False``. Note that fatal errors are logged even when ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param request_timeout: A timeout in seconds for requests. The default is 5 seconds. :param http_session: an initialized ``requests.Session`` object to be used when sending requests to the server. Use it if you need to add special client options such as proxy servers, SSL certificates, custom CA bundle, etc. :param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to skip SSL certificate verification, allowing connections to servers with self signed certificates. The default is ``True``. :param handle_sigint: Set to ``True`` to automatically handle disconnection when the process is interrupted, or to ``False`` to leave interrupt handling to the calling application. Interrupt handling can only be enabled when the client instance is created in the main thread. :param websocket_extra_options: Dictionary containing additional keyword arguments passed to ``websocket.create_connection()``. :param timestamp_requests: If ``True`` a timestamp is added to the query string of Socket.IO requests as a cache-busting measure. Set to ``False`` to disable. """ def connect(self, url, headers=None, transports=None, engineio_path='engine.io'): """Connect to an Engine.IO server. :param url: The URL of the Engine.IO server. It can include custom query string parameters if required by the server. :param headers: A dictionary with custom headers to send with the connection request. :param transports: The list of allowed transports. Valid transports are ``'polling'`` and ``'websocket'``. If not given, the polling transport is connected first, then an upgrade to websocket is attempted. :param engineio_path: The endpoint where the Engine.IO server is installed. The default value is appropriate for most cases. Example usage:: eio = engineio.Client() eio.connect('http://localhost:5000') """ if self.state != 'disconnected': raise ValueError('Client is not in a disconnected state') valid_transports = ['polling', 'websocket'] if transports is not None: if isinstance(transports, str): transports = [transports] transports = [transport for transport in transports if transport in valid_transports] if not transports: raise ValueError('No valid transports provided') self.transports = transports or valid_transports self.queue = self.create_queue() return getattr(self, '_connect_' + self.transports[0])( url, headers or {}, engineio_path) def wait(self): """Wait until the connection with the server ends. Client applications can use this function to block the main thread during the life of the connection. """ if self.read_loop_task: self.read_loop_task.join() def send(self, data): """Send a message to the server. :param data: The data to send to the server. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. """ self._send_packet(packet.Packet(packet.MESSAGE, data=data)) def disconnect(self, abort=False, reason=None): """Disconnect from the server. :param abort: If set to ``True``, do not wait for background tasks associated with the connection to end. """ if self.state == 'connected': self._send_packet(packet.Packet(packet.CLOSE)) self.queue.put(None) self.state = 'disconnecting' self._trigger_event('disconnect', reason or self.reason.CLIENT_DISCONNECT, run_async=False) if self.current_transport == 'websocket': self.ws.close() if not abort: self.read_loop_task.join() self.state = 'disconnected' try: base_client.connected_clients.remove(self) except ValueError: # pragma: no cover pass self._reset() def start_background_task(self, target, *args, **kwargs): """Start a background task. This is a utility function that applications can use to start a background task. :param target: the target function to execute. :param args: arguments to pass to the function. :param kwargs: keyword arguments to pass to the function. This function returns an object that represents the background task, on which the ``join()`` method can be invoked to wait for the task to complete. """ th = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=True) th.start() return th def sleep(self, seconds=0): """Sleep for the requested amount of time.""" return time.sleep(seconds) def create_queue(self, *args, **kwargs): """Create a queue object.""" q = queue.Queue(*args, **kwargs) q.Empty = queue.Empty return q def create_event(self, *args, **kwargs): """Create an event object.""" return threading.Event(*args, **kwargs) def _connect_polling(self, url, headers, engineio_path): """Establish a long-polling connection to the Engine.IO server.""" if requests is None: # pragma: no cover # not installed self.logger.error('requests package is not installed -- cannot ' 'send HTTP requests!') return self.base_url = self._get_engineio_url(url, engineio_path, 'polling') self.logger.info('Attempting polling connection to ' + self.base_url) r = self._send_request( 'GET', self.base_url + self._get_url_timestamp(), headers=headers, timeout=self.request_timeout) if r is None or isinstance(r, str): self._reset() raise exceptions.ConnectionError( r or 'Connection refused by the server') if r.status_code < 200 or r.status_code >= 300: self._reset() try: arg = r.json() except JSONDecodeError: arg = None raise exceptions.ConnectionError( 'Unexpected status code {} in server response'.format( r.status_code), arg) try: p = payload.Payload(encoded_payload=r.content.decode('utf-8')) except ValueError: raise exceptions.ConnectionError( 'Unexpected response from server') from None open_packet = p.packets[0] if open_packet.packet_type != packet.OPEN: raise exceptions.ConnectionError( 'OPEN packet not returned by server') self.logger.info( 'Polling connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'polling' self.base_url += '&sid=' + self.sid self.state = 'connected' base_client.connected_clients.append(self) self._trigger_event('connect', run_async=False) for pkt in p.packets[1:]: self._receive_packet(pkt) if 'websocket' in self.upgrades and 'websocket' in self.transports: # attempt to upgrade to websocket if self._connect_websocket(url, headers, engineio_path): # upgrade to websocket succeeded, we're done here return # start background tasks associated with this client self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_polling) def _connect_websocket(self, url, headers, engineio_path): """Establish or upgrade to a WebSocket connection with the server.""" if websocket is None: # pragma: no cover # not installed self.logger.error('websocket-client package not installed, only ' 'polling transport is available') return False websocket_url = self._get_engineio_url(url, engineio_path, 'websocket') if self.sid: self.logger.info( 'Attempting WebSocket upgrade to ' + websocket_url) upgrade = True websocket_url += '&sid=' + self.sid else: upgrade = False self.base_url = websocket_url self.logger.info( 'Attempting WebSocket connection to ' + websocket_url) # get cookies and other settings from the long-polling connection # so that they are preserved when connecting to the WebSocket route cookies = None extra_options = {} if self.http: # cookies cookies = '; '.join([f"{cookie.name}={cookie.value}" for cookie in self.http.cookies]) for header, value in headers.items(): if header.lower() == 'cookie': if cookies: cookies += '; ' cookies += value del headers[header] break # auth if 'Authorization' not in headers and self.http.auth is not None: if not isinstance(self.http.auth, tuple): # pragma: no cover raise ValueError('Only basic authentication is supported') basic_auth = '{}:{}'.format( self.http.auth[0], self.http.auth[1]).encode('utf-8') basic_auth = b64encode(basic_auth).decode('utf-8') headers['Authorization'] = 'Basic ' + basic_auth # cert # this can be given as ('certfile', 'keyfile') or just 'certfile' if isinstance(self.http.cert, tuple): extra_options['sslopt'] = { 'certfile': self.http.cert[0], 'keyfile': self.http.cert[1]} elif self.http.cert: extra_options['sslopt'] = {'certfile': self.http.cert} # proxies if self.http.proxies: proxy_url = None if websocket_url.startswith('ws://'): proxy_url = self.http.proxies.get( 'ws', self.http.proxies.get('http')) else: # wss:// proxy_url = self.http.proxies.get( 'wss', self.http.proxies.get('https')) if proxy_url: parsed_url = urllib.parse.urlparse( proxy_url if '://' in proxy_url else 'scheme://' + proxy_url) extra_options['http_proxy_host'] = parsed_url.hostname extra_options['http_proxy_port'] = parsed_url.port extra_options['http_proxy_auth'] = ( (parsed_url.username, parsed_url.password) if parsed_url.username or parsed_url.password else None) # verify if isinstance(self.http.verify, str): if 'sslopt' in extra_options: extra_options['sslopt']['ca_certs'] = self.http.verify else: extra_options['sslopt'] = {'ca_certs': self.http.verify} elif not self.http.verify: self.ssl_verify = False if not self.ssl_verify: if 'sslopt' in extra_options: extra_options['sslopt'].update({"cert_reqs": ssl.CERT_NONE}) else: extra_options['sslopt'] = {"cert_reqs": ssl.CERT_NONE} # combine internally generated options with the ones supplied by the # caller. The caller's options take precedence. headers.update(self.websocket_extra_options.pop('header', {})) extra_options['header'] = headers extra_options['cookie'] = cookies extra_options['enable_multithread'] = True extra_options['timeout'] = self.request_timeout extra_options.update(self.websocket_extra_options) try: ws = websocket.create_connection( websocket_url + self._get_url_timestamp(), **extra_options) except (ConnectionError, OSError, websocket.WebSocketException): if upgrade: self.logger.warning( 'WebSocket upgrade failed: connection error') return False else: raise exceptions.ConnectionError('Connection error') if upgrade: p = packet.Packet(packet.PING, data='probe').encode() try: ws.send(p) except Exception as e: # pragma: no cover self.logger.warning( 'WebSocket upgrade failed: unexpected send exception: %s', str(e)) return False try: p = ws.recv() except Exception as e: # pragma: no cover self.logger.warning( 'WebSocket upgrade failed: unexpected recv exception: %s', str(e)) return False pkt = packet.Packet(encoded_packet=p) if pkt.packet_type != packet.PONG or pkt.data != 'probe': self.logger.warning( 'WebSocket upgrade failed: no PONG packet') return False p = packet.Packet(packet.UPGRADE).encode() try: ws.send(p) except Exception as e: # pragma: no cover self.logger.warning( 'WebSocket upgrade failed: unexpected send exception: %s', str(e)) return False self.current_transport = 'websocket' self.logger.info('WebSocket upgrade was successful') else: try: p = ws.recv() except Exception as e: # pragma: no cover raise exceptions.ConnectionError( 'Unexpected recv exception: ' + str(e)) open_packet = packet.Packet(encoded_packet=p) if open_packet.packet_type != packet.OPEN: raise exceptions.ConnectionError('no OPEN packet') self.logger.info( 'WebSocket connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'websocket' self.state = 'connected' base_client.connected_clients.append(self) self._trigger_event('connect', run_async=False) self.ws = ws self.ws.settimeout(self.ping_interval + self.ping_timeout) # start background tasks associated with this client self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_websocket) return True def _receive_packet(self, pkt): """Handle incoming packets from the server.""" packet_name = packet.packet_names[pkt.packet_type] \ if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN' self.logger.info( 'Received packet %s data %s', packet_name, pkt.data if not isinstance(pkt.data, bytes) else '') if pkt.packet_type == packet.MESSAGE: self._trigger_event('message', pkt.data, run_async=True) elif pkt.packet_type == packet.PING: self._send_packet(packet.Packet(packet.PONG, pkt.data)) elif pkt.packet_type == packet.CLOSE: self.disconnect(abort=True, reason=self.reason.SERVER_DISCONNECT) elif pkt.packet_type == packet.NOOP: pass else: self.logger.error('Received unexpected packet of type %s', pkt.packet_type) def _send_packet(self, pkt): """Queue a packet to be sent to the server.""" if self.state != 'connected': return self.queue.put(pkt) self.logger.info( 'Sending packet %s data %s', packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') def _send_request( self, method, url, headers=None, body=None, timeout=None): # pragma: no cover if self.http is None: self.http = requests.Session() if not self.ssl_verify: self.http.verify = False try: return self.http.request(method, url, headers=headers, data=body, timeout=timeout) except requests.exceptions.RequestException as exc: self.logger.info('HTTP %s request to %s failed with error %s.', method, url, exc) return str(exc) def _trigger_event(self, event, *args, **kwargs): """Invoke an event handler.""" run_async = kwargs.pop('run_async', False) if event in self.handlers: if run_async: return self.start_background_task(self.handlers[event], *args) else: try: try: return self.handlers[event](*args) except TypeError: if event == 'disconnect' and \ len(args) == 1: # pragma: no branch # legacy disconnect events do not have a reason # argument return self.handlers[event]() else: # pragma: no cover raise except: self.logger.exception(event + ' handler error') def _read_loop_polling(self): """Read packets by polling the Engine.IO server.""" while self.state == 'connected' and self.write_loop_task: self.logger.info( 'Sending polling GET request to ' + self.base_url) r = self._send_request( 'GET', self.base_url + self._get_url_timestamp(), timeout=max(self.ping_interval, self.ping_timeout) + 5) if r is None or isinstance(r, str): self.logger.warning( r or 'Connection refused by the server, aborting') self.queue.put(None) break if r.status_code < 200 or r.status_code >= 300: self.logger.warning('Unexpected status code %s in server ' 'response, aborting', r.status_code) self.queue.put(None) break try: p = payload.Payload(encoded_payload=r.content.decode('utf-8')) except ValueError: self.logger.warning( 'Unexpected packet from server, aborting') self.queue.put(None) break for pkt in p.packets: self._receive_packet(pkt) if self.write_loop_task: # pragma: no branch self.logger.info('Waiting for write loop task to end') self.write_loop_task.join() if self.state == 'connected': self._trigger_event('disconnect', self.reason.TRANSPORT_ERROR, run_async=False) try: base_client.connected_clients.remove(self) except ValueError: # pragma: no cover pass self._reset() self.logger.info('Exiting read loop task') def _read_loop_websocket(self): """Read packets from the Engine.IO WebSocket connection.""" while self.state == 'connected': p = None try: p = self.ws.recv() if len(p) == 0 and not self.ws.connected: # pragma: no cover # websocket client can return an empty string after close raise websocket.WebSocketConnectionClosedException() except websocket.WebSocketTimeoutException: self.logger.warning( 'Server has stopped communicating, aborting') self.queue.put(None) break except websocket.WebSocketConnectionClosedException: self.logger.warning( 'WebSocket connection was closed, aborting') self.queue.put(None) break except Exception as e: # pragma: no cover if type(e) is OSError and e.errno == 9: self.logger.info( 'WebSocket connection is closing, aborting') else: self.logger.info( 'Unexpected error receiving packet: "%s", aborting', str(e)) self.queue.put(None) break try: pkt = packet.Packet(encoded_packet=p) except Exception as e: # pragma: no cover self.logger.info( 'Unexpected error decoding packet: "%s", aborting', str(e)) self.queue.put(None) break self._receive_packet(pkt) if self.write_loop_task: # pragma: no branch self.logger.info('Waiting for write loop task to end') self.write_loop_task.join() if self.state == 'connected': self._trigger_event('disconnect', self.reason.TRANSPORT_ERROR, run_async=False) try: base_client.connected_clients.remove(self) except ValueError: # pragma: no cover pass self._reset() self.logger.info('Exiting read loop task') def _write_loop(self): """This background task sends packages to the server as they are pushed to the send queue. """ while self.state == 'connected': # to simplify the timeout handling, use the maximum of the # ping interval and ping timeout as timeout, with an extra 5 # seconds grace period timeout = max(self.ping_interval, self.ping_timeout) + 5 packets = None try: packets = [self.queue.get(timeout=timeout)] except self.queue.Empty: self.logger.error('packet queue is empty, aborting') break if packets == [None]: self.queue.task_done() packets = [] else: while True: try: packets.append(self.queue.get(block=False)) except self.queue.Empty: break if packets[-1] is None: packets = packets[:-1] self.queue.task_done() break if not packets: # empty packet list returned -> connection closed break if self.current_transport == 'polling': p = payload.Payload(packets=packets) r = self._send_request( 'POST', self.base_url, body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=self.request_timeout) for pkt in packets: self.queue.task_done() if r is None or isinstance(r, str): self.logger.warning( r or 'Connection refused by the server, aborting') break if r.status_code < 200 or r.status_code >= 300: self.logger.warning('Unexpected status code %s in server ' 'response, aborting', r.status_code) self.write_loop_task = None break else: # websocket try: for pkt in packets: encoded_packet = pkt.encode() if pkt.binary: self.ws.send_binary(encoded_packet) else: self.ws.send(encoded_packet) self.queue.task_done() except (websocket.WebSocketConnectionClosedException, BrokenPipeError, OSError): self.logger.warning( 'WebSocket connection was closed, aborting') break self.logger.info('Exiting write loop task') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/src/engineio/exceptions.py0000664000175000017500000000044414707424343021516 0ustar00miguelmiguelclass EngineIOError(Exception): pass class ContentTooLongError(EngineIOError): pass class UnknownPacketError(EngineIOError): pass class QueueEmpty(EngineIOError): pass class SocketIsClosedError(EngineIOError): pass class ConnectionError(EngineIOError): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/src/engineio/json.py0000664000175000017500000000062514707424343020307 0ustar00miguelmiguel"""JSON-compatible module with sane defaults.""" from json import * # noqa: F401, F403 from json import loads as original_loads def _safe_int(s): if len(s) > 100: raise ValueError('Integer is too large') return int(s) def loads(*args, **kwargs): if 'parse_int' not in kwargs: # pragma: no cover kwargs['parse_int'] = _safe_int return original_loads(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/middleware.py0000664000175000017500000000724614726642012021455 0ustar00miguelmiguelimport os from engineio.static_files import get_static_file class WSGIApp: """WSGI application middleware for Engine.IO. This middleware dispatches traffic to an Engine.IO application. It can also serve a list of static files to the client, or forward unrelated HTTP traffic to another WSGI application. :param engineio_app: The Engine.IO server. Must be an instance of the ``engineio.Server`` class. :param wsgi_app: The WSGI app that receives all other traffic. :param static_files: A dictionary with static file mapping rules. See the documentation for details on this argument. :param engineio_path: The endpoint where the Engine.IO application should be installed. The default value is appropriate for most cases. Example usage:: import engineio import eventlet eio = engineio.Server() app = engineio.WSGIApp(eio, static_files={ '/': {'content_type': 'text/html', 'filename': 'index.html'}, '/index.html': {'content_type': 'text/html', 'filename': 'index.html'}, }) eventlet.wsgi.server(eventlet.listen(('', 8000)), app) """ def __init__(self, engineio_app, wsgi_app=None, static_files=None, engineio_path='engine.io'): self.engineio_app = engineio_app self.wsgi_app = wsgi_app self.engineio_path = engineio_path if not self.engineio_path.startswith('/'): self.engineio_path = '/' + self.engineio_path if not self.engineio_path.endswith('/'): self.engineio_path += '/' self.static_files = static_files or {} def __call__(self, environ, start_response): if 'gunicorn.socket' in environ: # gunicorn saves the socket under environ['gunicorn.socket'], while # eventlet saves it under environ['eventlet.input']. Eventlet also # stores the socket inside a wrapper class, while gunicon writes it # directly into the environment. To give eventlet's WebSocket # module access to this socket when running under gunicorn, here we # copy the socket to the eventlet format. class Input: def __init__(self, socket): self.socket = socket def get_socket(self): return self.socket environ['eventlet.input'] = Input(environ['gunicorn.socket']) path = environ['PATH_INFO'] if path is not None and path.startswith(self.engineio_path): return self.engineio_app.handle_request(environ, start_response) else: static_file = get_static_file(path, self.static_files) \ if self.static_files else None if static_file and os.path.exists(static_file['filename']): start_response( '200 OK', [('Content-Type', static_file['content_type'])]) with open(static_file['filename'], 'rb') as f: return [f.read()] elif self.wsgi_app is not None: return self.wsgi_app(environ, start_response) return self.not_found(start_response) def not_found(self, start_response): start_response("404 Not Found", [('Content-Type', 'text/plain')]) return [b'Not Found'] class Middleware(WSGIApp): """This class has been renamed to ``WSGIApp`` and is now deprecated.""" def __init__(self, engineio_app, wsgi_app=None, engineio_path='engine.io'): super().__init__(engineio_app, wsgi_app, engineio_path=engineio_path) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744471503.0 python_engineio-4.12.1/src/engineio/packet.py0000664000175000017500000000617614776502717020624 0ustar00miguelmiguelimport base64 from engineio import json as _json (OPEN, CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP) = (0, 1, 2, 3, 4, 5, 6) packet_names = ['OPEN', 'CLOSE', 'PING', 'PONG', 'MESSAGE', 'UPGRADE', 'NOOP'] binary_types = (bytes, bytearray) class Packet: """Engine.IO packet.""" json = _json def __init__(self, packet_type=NOOP, data=None, encoded_packet=None): self.packet_type = packet_type self.data = data self.encode_cache = None if isinstance(data, str): self.binary = False elif isinstance(data, binary_types): self.binary = True else: self.binary = False if self.binary and self.packet_type != MESSAGE: raise ValueError('Binary packets can only be of type MESSAGE') if encoded_packet is not None: self.decode(encoded_packet) def encode(self, b64=False): """Encode the packet for transmission. Note: as a performance optimization, subsequent calls to this method will return a cached encoded packet, even if the data has changed. """ if self.encode_cache: return self.encode_cache if self.binary: if b64: encoded_packet = 'b' + base64.b64encode(self.data).decode( 'utf-8') else: encoded_packet = self.data else: encoded_packet = str(self.packet_type) if isinstance(self.data, str): encoded_packet += self.data elif isinstance(self.data, dict) or isinstance(self.data, list): encoded_packet += self.json.dumps(self.data, separators=(',', ':')) elif self.data is not None: encoded_packet += str(self.data) self.encode_cache = encoded_packet return encoded_packet def decode(self, encoded_packet): """Decode a transmitted package.""" self.binary = isinstance(encoded_packet, binary_types) if not self.binary and len(encoded_packet) == 0: raise ValueError('Invalid empty packet received') b64 = not self.binary and encoded_packet[0] == 'b' if b64: self.binary = True self.packet_type = MESSAGE self.data = base64.b64decode(encoded_packet[1:]) else: if self.binary and not isinstance(encoded_packet, bytes): encoded_packet = bytes(encoded_packet) if self.binary: self.packet_type = MESSAGE self.data = encoded_packet else: self.packet_type = int(encoded_packet[0]) try: if encoded_packet[1].isnumeric(): # do not allow integer payloads, see # github.com/miguelgrinberg/python-engineio/issues/75 # for background on this decision raise ValueError self.data = self.json.loads(encoded_packet[1:]) except (ValueError, IndexError): self.data = encoded_packet[1:] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734034442.0 python_engineio-4.12.1/src/engineio/payload.py0000664000175000017500000000300314726642012020754 0ustar00miguelmiguelimport urllib from . import packet class Payload: """Engine.IO payload.""" max_decode_packets = 16 def __init__(self, packets=None, encoded_payload=None): self.packets = packets or [] if encoded_payload is not None: self.decode(encoded_payload) def encode(self, jsonp_index=None): """Encode the payload for transmission.""" encoded_payload = '' for pkt in self.packets: if encoded_payload: encoded_payload += '\x1e' encoded_payload += pkt.encode(b64=True) if jsonp_index is not None: encoded_payload = '___eio[' + \ str(jsonp_index) + \ ']("' + \ encoded_payload.replace('"', '\\"') + \ '");' return encoded_payload def decode(self, encoded_payload): """Decode a transmitted payload.""" self.packets = [] if len(encoded_payload) == 0: return # JSONP POST payload starts with 'd=' if encoded_payload.startswith('d='): encoded_payload = urllib.parse.parse_qs( encoded_payload)['d'][0] encoded_packets = encoded_payload.split('\x1e') if len(encoded_packets) > self.max_decode_packets: raise ValueError('Too many packets in payload') self.packets = [packet.Packet(encoded_packet=encoded_packet) for encoded_packet in encoded_packets] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/src/engineio/server.py0000664000175000017500000005465514730320266020653 0ustar00miguelmiguelimport logging import urllib from . import base_server from . import exceptions from . import packet from . import socket default_logger = logging.getLogger('engineio.server') class Server(base_server.BaseServer): """An Engine.IO server. This class implements a fully compliant Engine.IO web server with support for websocket and long-polling transports. :param async_mode: The asynchronous model to use. See the Deployment section in the documentation for a description of the available options. Valid async modes are "threading", "eventlet", "gevent" and "gevent_uwsgi". If this argument is not given, "eventlet" is tried first, then "gevent_uwsgi", then "gevent", and finally "threading". The first async mode that has all its dependencies installed is the one that is chosen. :param ping_interval: The interval in seconds at which the server pings the client. The default is 25 seconds. For advanced control, a two element tuple can be given, where the first number is the ping interval and the second is a grace period added by the server. :param ping_timeout: The time in seconds that the client waits for the server to respond before disconnecting. The default is 20 seconds. :param max_http_buffer_size: The maximum size that is accepted for incoming messages. The default is 1,000,000 bytes. In spite of its name, the value set in this argument is enforced for HTTP long-polling and WebSocket connections. :param allow_upgrades: Whether to allow transport upgrades or not. The default is ``True``. :param http_compression: Whether to compress packages when using the polling transport. The default is ``True``. :param compression_threshold: Only compress messages when their byte size is greater than this value. The default is 1024 bytes. :param cookie: If set to a string, it is the name of the HTTP cookie the server sends back tot he client containing the client session id. If set to a dictionary, the ``'name'`` key contains the cookie name and other keys define cookie attributes, where the value of each attribute can be a string, a callable with no arguments, or a boolean. If set to ``None`` (the default), a cookie is not sent to the client. :param cors_allowed_origins: Origin or list of origins that are allowed to connect to this server. Only the same origin is allowed by default. Set this argument to ``'*'`` to allow all origins, or to ``[]`` to disable CORS handling. :param cors_credentials: Whether credentials (cookies, authentication) are allowed in requests to this server. The default is ``True``. :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. The default is ``False``. Note that fatal errors are logged even when ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param async_handlers: If set to ``True``, run message event handlers in non-blocking threads. To run handlers synchronously, set to ``False``. The default is ``True``. :param monitor_clients: If set to ``True``, a background task will ensure inactive clients are closed. Set to ``False`` to disable the monitoring task (not recommended). The default is ``True``. :param transports: The list of allowed transports. Valid transports are ``'polling'`` and ``'websocket'``. Defaults to ``['polling', 'websocket']``. :param kwargs: Reserved for future extensions, any additional parameters given as keyword arguments will be silently ignored. """ def send(self, sid, data): """Send a message to a client. :param sid: The session id of the recipient client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. """ self.send_packet(sid, packet.Packet(packet.MESSAGE, data=data)) def send_packet(self, sid, pkt): """Send a raw packet to a client. :param sid: The session id of the recipient client. :param pkt: The packet to send to the client. """ try: socket = self._get_socket(sid) except KeyError: # the socket is not available self.logger.warning('Cannot send to sid %s', sid) return socket.send(pkt) def get_session(self, sid): """Return the user session for a client. :param sid: The session id of the client. The return value is a dictionary. Modifications made to this dictionary are not guaranteed to be preserved unless ``save_session()`` is called, or when the ``session`` context manager is used. """ socket = self._get_socket(sid) return socket.session def save_session(self, sid, session): """Store the user session for a client. :param sid: The session id of the client. :param session: The session dictionary. """ socket = self._get_socket(sid) socket.session = session def session(self, sid): """Return the user session for a client with context manager syntax. :param sid: The session id of the client. This is a context manager that returns the user session dictionary for the client. Any changes that are made to this dictionary inside the context manager block are saved back to the session. Example usage:: @eio.on('connect') def on_connect(sid, environ): username = authenticate_user(environ) if not username: return False with eio.session(sid) as session: session['username'] = username @eio.on('message') def on_message(sid, msg): with eio.session(sid) as session: print('received message from ', session['username']) """ class _session_context_manager: def __init__(self, server, sid): self.server = server self.sid = sid self.session = None def __enter__(self): self.session = self.server.get_session(sid) return self.session def __exit__(self, *args): self.server.save_session(sid, self.session) return _session_context_manager(self, sid) def disconnect(self, sid=None): """Disconnect a client. :param sid: The session id of the client to close. If this parameter is not given, then all clients are closed. """ if sid is not None: try: socket = self._get_socket(sid) except KeyError: # pragma: no cover # the socket was already closed or gone pass else: socket.close(reason=self.reason.SERVER_DISCONNECT) if sid in self.sockets: # pragma: no cover del self.sockets[sid] else: for client in self.sockets.copy().values(): client.close(reason=self.reason.SERVER_DISCONNECT) self.sockets = {} def handle_request(self, environ, start_response): """Handle an HTTP request from the client. This is the entry point of the Engine.IO application, using the same interface as a WSGI application. For the typical usage, this function is invoked by the :class:`Middleware` instance, but it can be invoked directly when the middleware is not used. :param environ: The WSGI environment. :param start_response: The WSGI ``start_response`` function. This function returns the HTTP response body to deliver to the client as a byte sequence. """ if self.cors_allowed_origins != []: # Validate the origin header if present # This is important for WebSocket more than for HTTP, since # browsers only apply CORS controls to HTTP. origin = environ.get('HTTP_ORIGIN') if origin: allowed_origins = self._cors_allowed_origins(environ) if allowed_origins is not None and origin not in \ allowed_origins: self._log_error_once( origin + ' is not an accepted origin.', 'bad-origin') r = self._bad_request('Not an accepted origin.') start_response(r['status'], r['headers']) return [r['response']] method = environ['REQUEST_METHOD'] query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) jsonp = False jsonp_index = None # make sure the client uses an allowed transport transport = query.get('transport', ['polling'])[0] if transport not in self.transports: self._log_error_once('Invalid transport', 'bad-transport') r = self._bad_request('Invalid transport') start_response(r['status'], r['headers']) return [r['response']] # make sure the client speaks a compatible Engine.IO version sid = query['sid'][0] if 'sid' in query else None if sid is None and query.get('EIO') != ['4']: self._log_error_once( 'The client is using an unsupported version of the Socket.IO ' 'or Engine.IO protocols', 'bad-version') r = self._bad_request( 'The client is using an unsupported version of the Socket.IO ' 'or Engine.IO protocols') start_response(r['status'], r['headers']) return [r['response']] if 'j' in query: jsonp = True try: jsonp_index = int(query['j'][0]) except (ValueError, KeyError, IndexError): # Invalid JSONP index number pass if jsonp and jsonp_index is None: self._log_error_once('Invalid JSONP index number', 'bad-jsonp-index') r = self._bad_request('Invalid JSONP index number') elif method == 'GET': upgrade_header = environ.get('HTTP_UPGRADE').lower() \ if 'HTTP_UPGRADE' in environ else None if sid is None: # transport must be one of 'polling' or 'websocket'. # if 'websocket', the HTTP_UPGRADE header must match. if transport == 'polling' \ or transport == upgrade_header == 'websocket': r = self._handle_connect(environ, start_response, transport, jsonp_index) else: self._log_error_once('Invalid websocket upgrade', 'bad-upgrade') r = self._bad_request('Invalid websocket upgrade') else: if sid not in self.sockets: self._log_error_once(f'Invalid session {sid}', 'bad-sid') r = self._bad_request(f'Invalid session {sid}') else: try: socket = self._get_socket(sid) except KeyError as e: # pragma: no cover self._log_error_once(f'{e} {sid}', 'bad-sid') r = self._bad_request(f'{e} {sid}') else: if self.transport(sid) != transport and \ transport != upgrade_header: self._log_error_once( f'Invalid transport for session {sid}', 'bad-transport') r = self._bad_request('Invalid transport') else: try: packets = socket.handle_get_request( environ, start_response) if isinstance(packets, list): r = self._ok(packets, jsonp_index=jsonp_index) else: r = packets except exceptions.EngineIOError: if sid in self.sockets: # pragma: no cover self.disconnect(sid) r = self._bad_request() if sid in self.sockets and \ self.sockets[sid].closed: del self.sockets[sid] elif method == 'POST': if sid is None or sid not in self.sockets: self._log_error_once(f'Invalid session {sid}', 'bad-sid') r = self._bad_request(f'Invalid session {sid}') else: socket = self._get_socket(sid) try: socket.handle_post_request(environ) r = self._ok(jsonp_index=jsonp_index) except exceptions.EngineIOError: if sid in self.sockets: # pragma: no cover self.disconnect(sid) r = self._bad_request() except: # pragma: no cover # for any other unexpected errors, we log the error # and keep going self.logger.exception('post request handler error') r = self._ok(jsonp_index=jsonp_index) elif method == 'OPTIONS': r = self._ok() else: self.logger.warning('Method %s not supported', method) r = self._method_not_found() if not isinstance(r, dict): return r if self.http_compression and \ len(r['response']) >= self.compression_threshold: encodings = [e.split(';')[0].strip() for e in environ.get('HTTP_ACCEPT_ENCODING', '').split(',')] for encoding in encodings: if encoding in self.compression_methods: r['response'] = \ getattr(self, '_' + encoding)(r['response']) r['headers'] += [('Content-Encoding', encoding)] break cors_headers = self._cors_headers(environ) start_response(r['status'], r['headers'] + cors_headers) return [r['response']] def shutdown(self): """Stop Socket.IO background tasks. This method stops background activity initiated by the Socket.IO server. It must be called before shutting down the web server. """ self.logger.info('Socket.IO is shutting down') if self.service_task_event: # pragma: no cover self.service_task_event.set() self.service_task_handle.join() self.service_task_handle = None def start_background_task(self, target, *args, **kwargs): """Start a background task using the appropriate async model. This is a utility function that applications can use to start a background task using the method that is compatible with the selected async mode. :param target: the target function to execute. :param args: arguments to pass to the function. :param kwargs: keyword arguments to pass to the function. This function returns an object that represents the background task, on which the ``join()`` methond can be invoked to wait for the task to complete. """ th = self._async['thread'](target=target, args=args, kwargs=kwargs) th.start() return th # pragma: no cover def sleep(self, seconds=0): """Sleep for the requested amount of time using the appropriate async model. This is a utility function that applications can use to put a task to sleep without having to worry about using the correct call for the selected async mode. """ return self._async['sleep'](seconds) def _handle_connect(self, environ, start_response, transport, jsonp_index=None): """Handle a client connection request.""" if self.start_service_task: # start the service task to monitor connected clients self.start_service_task = False self.service_task_handle = self.start_background_task( self._service_task) sid = self.generate_id() s = socket.Socket(self, sid) self.sockets[sid] = s pkt = packet.Packet(packet.OPEN, { 'sid': sid, 'upgrades': self._upgrades(sid, transport), 'pingTimeout': int(self.ping_timeout * 1000), 'pingInterval': int( self.ping_interval + self.ping_interval_grace_period) * 1000, 'maxPayload': self.max_http_buffer_size, }) s.send(pkt) s.schedule_ping() # NOTE: some sections below are marked as "no cover" to workaround # what seems to be a bug in the coverage package. All the lines below # are covered by tests, but some are not reported as such for some # reason ret = self._trigger_event('connect', sid, environ, run_async=False) if ret is not None and ret is not True: # pragma: no cover del self.sockets[sid] self.logger.warning('Application rejected connection') return self._unauthorized(ret or None) if transport == 'websocket': # pragma: no cover ret = s.handle_get_request(environ, start_response) if s.closed and sid in self.sockets: # websocket connection ended, so we are done del self.sockets[sid] return ret else: # pragma: no cover s.connected = True headers = None if self.cookie: if isinstance(self.cookie, dict): headers = [( 'Set-Cookie', self._generate_sid_cookie(sid, self.cookie) )] else: headers = [( 'Set-Cookie', self._generate_sid_cookie(sid, { 'name': self.cookie, 'path': '/', 'SameSite': 'Lax' }) )] try: return self._ok(s.poll(), headers=headers, jsonp_index=jsonp_index) except exceptions.QueueEmpty: return self._bad_request() def _trigger_event(self, event, *args, **kwargs): """Invoke an event handler.""" run_async = kwargs.pop('run_async', False) if event in self.handlers: def run_handler(): try: try: return self.handlers[event](*args) except TypeError: if event == 'disconnect' and \ len(args) == 2: # pragma: no branch # legacy disconnect events do not have a reason # argument return self.handlers[event](args[0]) else: # pragma: no cover raise except: self.logger.exception(event + ' handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False if run_async: return self.start_background_task(run_handler) else: return run_handler() def _service_task(self): # pragma: no cover """Monitor connected clients and clean up those that time out.""" self.service_task_event = self.create_event() while not self.service_task_event.is_set(): if len(self.sockets) == 0: # nothing to do if self.service_task_event.wait(timeout=self.ping_timeout): break continue # go through the entire client list in a ping interval cycle sleep_interval = float(self.ping_timeout) / len(self.sockets) try: # iterate over the current clients for s in self.sockets.copy().values(): if s.closed: try: del self.sockets[s.sid] except KeyError: # the socket could have also been removed by # the _get_socket() method from another thread pass elif not s.closing: s.check_ping_timeout() if self.service_task_event.wait(timeout=sleep_interval): raise KeyboardInterrupt() except (SystemExit, KeyboardInterrupt): self.logger.info('service task canceled') break except: # an unexpected exception has occurred, log it and continue self.logger.exception('service task exception') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735498532.0 python_engineio-4.12.1/src/engineio/socket.py0000664000175000017500000002414614734315444020633 0ustar00miguelmiguelimport sys import time from . import base_socket from . import exceptions from . import packet from . import payload class Socket(base_socket.BaseSocket): """An Engine.IO socket.""" def poll(self): """Wait for packets to send to the client.""" queue_empty = self.server.get_queue_empty_exception() try: packets = [self.queue.get( timeout=self.server.ping_interval + self.server.ping_timeout)] self.queue.task_done() except queue_empty: raise exceptions.QueueEmpty() if packets == [None]: return [] while True: try: pkt = self.queue.get(block=False) self.queue.task_done() if pkt is None: self.queue.put(None) break packets.append(pkt) except queue_empty: break return packets def receive(self, pkt): """Receive packet from the client.""" packet_name = packet.packet_names[pkt.packet_type] \ if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN' self.server.logger.info('%s: Received packet %s data %s', self.sid, packet_name, pkt.data if not isinstance(pkt.data, bytes) else '') if pkt.packet_type == packet.PONG: self.schedule_ping() elif pkt.packet_type == packet.MESSAGE: self.server._trigger_event('message', self.sid, pkt.data, run_async=self.server.async_handlers) elif pkt.packet_type == packet.UPGRADE: self.send(packet.Packet(packet.NOOP)) elif pkt.packet_type == packet.CLOSE: self.close(wait=False, abort=True, reason=self.server.reason.CLIENT_DISCONNECT) else: raise exceptions.UnknownPacketError() def check_ping_timeout(self): """Make sure the client is still responding to pings.""" if self.closed: raise exceptions.SocketIsClosedError() if self.last_ping and \ time.time() - self.last_ping > self.server.ping_timeout: self.server.logger.info('%s: Client is gone, closing socket', self.sid) # Passing abort=False here will cause close() to write a # CLOSE packet. This has the effect of updating half-open sockets # to their correct state of disconnected self.close(wait=False, abort=False, reason=self.server.reason.PING_TIMEOUT) return False return True def send(self, pkt): """Send a packet to the client.""" if not self.check_ping_timeout(): return else: self.queue.put(pkt) self.server.logger.info('%s: Sending packet %s data %s', self.sid, packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') def handle_get_request(self, environ, start_response): """Handle a long-polling GET request from the client.""" connections = [ s.strip() for s in environ.get('HTTP_CONNECTION', '').lower().split(',')] transport = environ.get('HTTP_UPGRADE', '').lower() if 'upgrade' in connections and transport in self.upgrade_protocols: self.server.logger.info('%s: Received request to upgrade to %s', self.sid, transport) return getattr(self, '_upgrade_' + transport)(environ, start_response) if self.upgrading or self.upgraded: # we are upgrading to WebSocket, do not return any more packets # through the polling endpoint return [packet.Packet(packet.NOOP)] try: packets = self.poll() except exceptions.QueueEmpty: exc = sys.exc_info() self.close(wait=False, reason=self.server.reason.TRANSPORT_ERROR) raise exc[1].with_traceback(exc[2]) return packets def handle_post_request(self, environ): """Handle a long-polling POST request from the client.""" length = int(environ.get('CONTENT_LENGTH', '0')) if length > self.server.max_http_buffer_size: raise exceptions.ContentTooLongError() else: body = environ['wsgi.input'].read(length).decode('utf-8') p = payload.Payload(encoded_payload=body) for pkt in p.packets: self.receive(pkt) def close(self, wait=True, abort=False, reason=None): """Close the socket connection.""" if not self.closed and not self.closing: self.closing = True self.server._trigger_event( 'disconnect', self.sid, reason or self.server.reason.SERVER_DISCONNECT, run_async=False) if not abort: self.send(packet.Packet(packet.CLOSE)) self.closed = True self.queue.put(None) if wait: self.queue.join() def schedule_ping(self): self.server.start_background_task(self._send_ping) def _send_ping(self): self.last_ping = None self.server.sleep(self.server.ping_interval) if not self.closing and not self.closed: self.last_ping = time.time() self.send(packet.Packet(packet.PING)) def _upgrade_websocket(self, environ, start_response): """Upgrade the connection from polling to websocket.""" if self.upgraded: raise OSError('Socket has been upgraded already') if self.server._async['websocket'] is None: # the selected async mode does not support websocket return self.server._bad_request() ws = self.server._async['websocket']( self._websocket_handler, self.server) return ws(environ, start_response) def _websocket_handler(self, ws): """Engine.IO handler for websocket transport.""" def websocket_wait(): data = ws.wait() if data and len(data) > self.server.max_http_buffer_size: raise ValueError('packet is too large') return data # try to set a socket timeout matching the configured ping interval # and timeout for attr in ['_sock', 'socket']: # pragma: no cover if hasattr(ws, attr) and hasattr(getattr(ws, attr), 'settimeout'): getattr(ws, attr).settimeout( self.server.ping_interval + self.server.ping_timeout) if self.connected: # the socket was already connected, so this is an upgrade self.upgrading = True # hold packet sends during the upgrade pkt = websocket_wait() decoded_pkt = packet.Packet(encoded_packet=pkt) if decoded_pkt.packet_type != packet.PING or \ decoded_pkt.data != 'probe': self.server.logger.info( '%s: Failed websocket upgrade, no PING packet', self.sid) self.upgrading = False return [] ws.send(packet.Packet(packet.PONG, data='probe').encode()) self.queue.put(packet.Packet(packet.NOOP)) # end poll pkt = websocket_wait() decoded_pkt = packet.Packet(encoded_packet=pkt) if decoded_pkt.packet_type != packet.UPGRADE: self.upgraded = False self.server.logger.info( ('%s: Failed websocket upgrade, expected UPGRADE packet, ' 'received %s instead.'), self.sid, pkt) self.upgrading = False return [] self.upgraded = True self.upgrading = False else: self.connected = True self.upgraded = True # start separate writer thread def writer(): while True: packets = None try: packets = self.poll() except exceptions.QueueEmpty: break if not packets: # empty packet list returned -> connection closed break try: for pkt in packets: ws.send(pkt.encode()) except: break ws.close() writer_task = self.server.start_background_task(writer) self.server.logger.info( '%s: Upgrade to websocket successful', self.sid) while True: p = None try: p = websocket_wait() except Exception as e: # if the socket is already closed, we can assume this is a # downstream error of that if not self.closed: # pragma: no cover self.server.logger.info( '%s: Unexpected error "%s", closing connection', self.sid, str(e)) break if p is None: # connection closed by client break pkt = packet.Packet(encoded_packet=p) try: self.receive(pkt) except exceptions.UnknownPacketError: # pragma: no cover pass except exceptions.SocketIsClosedError: # pragma: no cover self.server.logger.info('Receive error -- socket is closed') break except: # pragma: no cover # if we get an unexpected exception we log the error and exit # the connection properly self.server.logger.exception('Unknown receive error') break self.queue.put(None) # unlock the writer task so that it can exit writer_task.join() self.close(wait=False, abort=True, reason=self.server.reason.TRANSPORT_CLOSE) return [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/src/engineio/static_files.py0000664000175000017500000000402014707424343022000 0ustar00miguelmiguelcontent_types = { 'css': 'text/css', 'gif': 'image/gif', 'html': 'text/html', 'jpg': 'image/jpeg', 'js': 'application/javascript', 'json': 'application/json', 'png': 'image/png', 'txt': 'text/plain', } def get_static_file(path, static_files): """Return the local filename and content type for the requested static file URL. :param path: the path portion of the requested URL. :param static_files: a static file configuration dictionary. This function returns a dictionary with two keys, "filename" and "content_type". If the requested URL does not match any static file, the return value is None. """ extra_path = '' if path in static_files: f = static_files[path] else: f = None while path != '': path, last = path.rsplit('/', 1) extra_path = '/' + last + extra_path if path in static_files: f = static_files[path] break elif path + '/' in static_files: f = static_files[path + '/'] break if f: if isinstance(f, str): f = {'filename': f} else: f = f.copy() # in case it is mutated below if f['filename'].endswith('/') and extra_path.startswith('/'): extra_path = extra_path[1:] f['filename'] += extra_path if f['filename'].endswith('/'): if '' in static_files: if isinstance(static_files[''], str): f['filename'] += static_files[''] else: f['filename'] += static_files['']['filename'] if 'content_type' in static_files['']: f['content_type'] = static_files['']['content_type'] else: f['filename'] += 'index.html' if 'content_type' not in f: ext = f['filename'].rsplit('.')[-1] f['content_type'] = content_types.get( ext, 'application/octet-stream') return f ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3653085 python_engineio-4.12.1/src/python_engineio.egg-info/0000775000175000017500000000000015010141646022042 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977702.0 python_engineio-4.12.1/src/python_engineio.egg-info/PKG-INFO0000644000175000017500000000425515010141646023143 0ustar00miguelmiguelMetadata-Version: 2.4 Name: python-engineio Version: 4.12.1 Summary: Engine.IO server and client for Python Author-email: Miguel Grinberg License: MIT Project-URL: Homepage, https://github.com/miguelgrinberg/python-engineio Project-URL: Bug Tracker, https://github.com/miguelgrinberg/python-engineio/issues Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 3 Classifier: Operating System :: OS Independent Requires-Python: >=3.6 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: simple-websocket>=0.10.0 Provides-Extra: client Requires-Dist: requests>=2.21.0; extra == "client" Requires-Dist: websocket-client>=0.54.0; extra == "client" Provides-Extra: asyncio-client Requires-Dist: aiohttp>=3.4; extra == "asyncio-client" Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Dynamic: license-file python-engineio =============== [![Build status](https://github.com/miguelgrinberg/python-engineio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-engineio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-engineio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-engineio) Python implementation of the `Engine.IO` realtime client and server. Sponsors -------- The following organizations are funding this project: ![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| -|- Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? Resources --------- - [Documentation](https://python-engineio.readthedocs.io/) - [PyPI](https://pypi.python.org/pypi/python-engineio) - [Change Log](https://github.com/miguelgrinberg/python-engineio/blob/main/CHANGES.md) - Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977702.0 python_engineio-4.12.1/src/python_engineio.egg-info/SOURCES.txt0000775000175000017500000000370315010141646023734 0ustar00miguelmiguelLICENSE MANIFEST.in README.md pyproject.toml tox.ini docs/Makefile docs/api.rst docs/client.rst docs/conf.py docs/index.rst docs/intro.rst docs/make.bat docs/server.rst docs/_static/README.md src/engineio/__init__.py src/engineio/async_client.py src/engineio/async_server.py src/engineio/async_socket.py src/engineio/base_client.py src/engineio/base_server.py src/engineio/base_socket.py src/engineio/client.py src/engineio/exceptions.py src/engineio/json.py src/engineio/middleware.py src/engineio/packet.py src/engineio/payload.py src/engineio/server.py src/engineio/socket.py src/engineio/static_files.py src/engineio/async_drivers/__init__.py src/engineio/async_drivers/_websocket_wsgi.py src/engineio/async_drivers/aiohttp.py src/engineio/async_drivers/asgi.py src/engineio/async_drivers/eventlet.py src/engineio/async_drivers/gevent.py src/engineio/async_drivers/gevent_uwsgi.py src/engineio/async_drivers/sanic.py src/engineio/async_drivers/threading.py src/engineio/async_drivers/tornado.py src/python_engineio.egg-info/PKG-INFO src/python_engineio.egg-info/SOURCES.txt src/python_engineio.egg-info/dependency_links.txt src/python_engineio.egg-info/not-zip-safe src/python_engineio.egg-info/requires.txt src/python_engineio.egg-info/top_level.txt tests/__init__.py tests/async/__init__.py tests/async/index.html tests/async/test_aiohttp.py tests/async/test_asgi.py tests/async/test_client.py tests/async/test_sanic.py tests/async/test_server.py tests/async/test_socket.py tests/async/test_tornado.py tests/common/__init__.py tests/common/index.html tests/common/test_client.py tests/common/test_middleware.py tests/common/test_packet.py tests/common/test_payload.py tests/common/test_server.py tests/common/test_socket.py tests/performance/README.md tests/performance/binary_b64_packet.py tests/performance/binary_packet.py tests/performance/json_packet.py tests/performance/payload.py tests/performance/run.sh tests/performance/server_receive.py tests/performance/text_packet.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977702.0 python_engineio-4.12.1/src/python_engineio.egg-info/dependency_links.txt0000775000175000017500000000000115010141646026113 0ustar00miguelmiguel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697051879.0 python_engineio-4.12.1/src/python_engineio.egg-info/not-zip-safe0000775000175000017500000000000114511572347024306 0ustar00miguelmiguel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977702.0 python_engineio-4.12.1/src/python_engineio.egg-info/requires.txt0000775000175000017500000000017315010141646024446 0ustar00miguelmiguelsimple-websocket>=0.10.0 [asyncio_client] aiohttp>=3.4 [client] requests>=2.21.0 websocket-client>=0.54.0 [docs] sphinx ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1746977702.0 python_engineio-4.12.1/src/python_engineio.egg-info/top_level.txt0000775000175000017500000000001115010141646024567 0ustar00miguelmiguelengineio ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3643086 python_engineio-4.12.1/tests/0000775000175000017500000000000015010141646015525 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/__init__.py0000664000175000017500000000000014707424343017636 0ustar00miguelmiguel././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3643086 python_engineio-4.12.1/tests/async/0000775000175000017500000000000015010141646016642 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/async/__init__.py0000664000175000017500000000000014707424343020753 0ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/async/index.html0000664000175000017500000000001614707424343020646 0ustar00miguelmiguel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732474591.0 python_engineio-4.12.1/tests/async/test_aiohttp.py0000664000175000017500000000351214720673337021742 0ustar00miguelmiguelfrom unittest import mock from engineio.async_drivers import aiohttp as async_aiohttp class TestAiohttp: def test_create_route(self): app = mock.MagicMock() mock_server = mock.MagicMock() async_aiohttp.create_route(app, mock_server, '/foo') app.router.add_get.assert_any_call('/foo', mock_server.handle_request) app.router.add_post.assert_any_call('/foo', mock_server.handle_request) def test_translate_request(self): request = mock.MagicMock() request._message.method = 'PUT' request._message.path = '/foo/bar?baz=1' request._message.version = (1, 1) request._message.headers = { 'a': 'b', 'c-c': 'd', 'c_c': 'e', 'content-type': 'application/json', 'content-length': 123, } request._payload = b'hello world' environ = async_aiohttp.translate_request(request) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', 'QUERY_STRING': 'baz=1', 'CONTENT_TYPE': 'application/json', 'CONTENT_LENGTH': 123, 'HTTP_A': 'b', # 'HTTP_C_C': 'd,e', 'RAW_URI': '/foo/bar?baz=1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'wsgi.input': b'hello world', 'aiohttp.request': request, } for k, v in expected_environ.items(): assert v == environ[k] assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' # @mock.patch('async_aiohttp.aiohttp.web.Response') def test_make_response(self): rv = async_aiohttp.make_response( '202 ACCEPTED', {'foo': 'bar'}, b'payload', {} ) assert rv.status == 202 assert rv.headers['foo'] == 'bar' assert rv.body == b'payload' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734199135.0 python_engineio-4.12.1/tests/async/test_asgi.py0000664000175000017500000005165114727343537021227 0ustar00miguelmiguelimport os from unittest import mock from engineio.async_drivers import asgi as async_asgi class TestAsgi: async def test_create_app(self): app = async_asgi.ASGIApp( 'eio', 'other_app', static_files='static_files', engineio_path='/foo/', ) assert app.engineio_server == 'eio' assert app.other_asgi_app == 'other_app' assert app.static_files == 'static_files' assert app.engineio_path == '/foo/' async def test_engineio_routing(self): mock_server = mock.MagicMock() mock_server.handle_request = mock.AsyncMock() app = async_asgi.ASGIApp(mock_server) scope = {'type': 'http', 'path': '/engine.io/'} await app(scope, 'receive', 'send') mock_server.handle_request.assert_awaited_once_with( scope, 'receive', 'send' ) mock_server.handle_request.reset_mock() scope = {'type': 'http', 'path': '/engine.io/'} await app(scope, 'receive', 'send') mock_server.handle_request.assert_awaited_once_with( scope, 'receive', 'send' ) mock_server.handle_request.reset_mock() scope = {'type': 'http', 'path': '/engine.iofoo/'} await app(scope, 'receive', mock.AsyncMock()) mock_server.handle_request.assert_not_awaited() app = async_asgi.ASGIApp(mock_server, engineio_path=None) mock_server.handle_request.reset_mock() scope = {'type': 'http', 'path': '/foo'} await app(scope, 'receive', 'send') mock_server.handle_request.assert_awaited_once_with( scope, 'receive', 'send' ) app = async_asgi.ASGIApp(mock_server, engineio_path='mysocket.io') mock_server.handle_request.reset_mock() scope = {'type': 'http', 'path': '/mysocket.io'} await app(scope, 'receive', 'send') mock_server.handle_request.assert_awaited_once_with( scope, 'receive', 'send' ) mock_server.handle_request.reset_mock() scope = {'type': 'http', 'path': '/mysocket.io/'} await app(scope, 'receive', 'send') mock_server.handle_request.assert_awaited_once_with( scope, 'receive', 'send' ) mock_server.handle_request.reset_mock() scope = {'type': 'http', 'path': '/mysocket.io/foo'} await app(scope, 'receive', 'send') mock_server.handle_request.assert_awaited_once_with( scope, 'receive', 'send' ) mock_server.handle_request.reset_mock() scope = {'type': 'http', 'path': '/mysocket.iofoo'} await app(scope, 'receive', mock.AsyncMock()) mock_server.handle_request.assert_not_awaited() async def test_other_app_routing(self): other_app = mock.AsyncMock() app = async_asgi.ASGIApp('eio', other_app) scope = {'type': 'http', 'path': '/foo'} await app(scope, 'receive', 'send') other_app.assert_awaited_once_with(scope, 'receive', 'send') async def test_other_app_lifespan_routing(self): other_app = mock.AsyncMock() app = async_asgi.ASGIApp('eio', other_app) scope = {'type': 'lifespan'} await app(scope, 'receive', 'send') other_app.assert_awaited_once_with(scope, 'receive', 'send') async def test_static_file_routing(self): root_dir = os.path.dirname(__file__) app = async_asgi.ASGIApp( 'eio', static_files={ '/': root_dir + '/index.html', '/foo': { 'content_type': 'text/plain', 'filename': root_dir + '/index.html', }, '/static': root_dir, '/static/test/': root_dir + '/', '/static2/test/': {'filename': root_dir + '/', 'content_type': 'image/gif'}, }, ) async def check_path(path, status_code, content_type, body): scope = {'type': 'http', 'path': path} receive = mock.AsyncMock(return_value={'type': 'http.request'}) send = mock.AsyncMock() await app(scope, receive, send) send.assert_any_await( { 'type': 'http.response.start', 'status': status_code, 'headers': [ (b'Content-Type', content_type.encode('utf-8')) ], } ) send.assert_any_await( {'type': 'http.response.body', 'body': body.encode('utf-8')} ) await check_path('/', 200, 'text/html', '\n') await check_path('/foo', 200, 'text/plain', '\n') await check_path('/foo/bar', 404, 'text/plain', 'Not Found') await check_path('/static/index.html', 200, 'text/html', '\n') await check_path('/static/foo.bar', 404, 'text/plain', 'Not Found') await check_path( '/static/test/index.html', 200, 'text/html', '\n' ) await check_path('/static/test/index.html', 200, 'text/html', '\n') await check_path('/static/test/files/', 200, 'text/html', 'file\n') await check_path('/static/test/files/file.txt', 200, 'text/plain', 'file\n') await check_path('/static/test/files/x.html', 404, 'text/plain', 'Not Found') await check_path('/static2/test/', 200, 'image/gif', '\n') await check_path('/static2/test/index.html', 200, 'image/gif', '\n') await check_path('/static2/test/files/', 200, 'image/gif', 'file\n') await check_path('/static2/test/files/file.txt', 200, 'image/gif', 'file\n') await check_path('/static2/test/files/x.html', 404, 'text/plain', 'Not Found') await check_path('/bar/foo', 404, 'text/plain', 'Not Found') await check_path('', 404, 'text/plain', 'Not Found') app.static_files[''] = 'index.html' await check_path('/static/test/', 200, 'text/html', '\n') app.static_files[''] = {'filename': 'index.html'} await check_path('/static/test/', 200, 'text/html', '\n') app.static_files[''] = { 'filename': 'index.html', 'content_type': 'image/gif', } await check_path('/static/test/', 200, 'image/gif', '\n') app.static_files[''] = {'filename': 'test.gif'} await check_path('/static/test/', 404, 'text/plain', 'Not Found') app.static_files = {} await check_path('/static/test/index.html', 404, 'text/plain', 'Not Found') async def test_lifespan_startup(self): app = async_asgi.ASGIApp('eio') scope = {'type': 'lifespan'} receive = mock.AsyncMock(side_effect=[{'type': 'lifespan.startup'}, {'type': 'lifespan.shutdown'}]) send = mock.AsyncMock() await app(scope, receive, send) send.assert_any_await( {'type': 'lifespan.startup.complete'} ) async def test_lifespan_startup_sync_function(self): up = False def startup(): nonlocal up up = True app = async_asgi.ASGIApp('eio', on_startup=startup) scope = {'type': 'lifespan'} receive = mock.AsyncMock(side_effect=[{'type': 'lifespan.startup'}, {'type': 'lifespan.shutdown'}]) send = mock.AsyncMock() await app(scope, receive, send) send.assert_any_await( {'type': 'lifespan.startup.complete'} ) assert up async def test_lifespan_startup_async_function(self): up = False async def startup(): nonlocal up up = True app = async_asgi.ASGIApp('eio', on_startup=startup) scope = {'type': 'lifespan'} receive = mock.AsyncMock(side_effect=[{'type': 'lifespan.startup'}, {'type': 'lifespan.shutdown'}]) send = mock.AsyncMock() await app(scope, receive, send) send.assert_any_await( {'type': 'lifespan.startup.complete'} ) assert up async def test_lifespan_startup_function_exception(self): up = False def startup(): raise Exception app = async_asgi.ASGIApp('eio', on_startup=startup) scope = {'type': 'lifespan'} receive = mock.AsyncMock(side_effect=[{'type': 'lifespan.startup'}]) send = mock.AsyncMock() await app(scope, receive, send) send.assert_awaited_once_with({'type': 'lifespan.startup.failed'}) assert not up async def test_lifespan_shutdown(self): app = async_asgi.ASGIApp('eio') scope = {'type': 'lifespan'} receive = mock.AsyncMock(return_value={'type': 'lifespan.shutdown'}) send = mock.AsyncMock() await app(scope, receive, send) send.assert_awaited_once_with( {'type': 'lifespan.shutdown.complete'} ) async def test_lifespan_shutdown_sync_function(self): down = False def shutdown(): nonlocal down down = True app = async_asgi.ASGIApp('eio', on_shutdown=shutdown) scope = {'type': 'lifespan'} receive = mock.AsyncMock(return_value={'type': 'lifespan.shutdown'}) send = mock.AsyncMock() await app(scope, receive, send) send.assert_awaited_once_with( {'type': 'lifespan.shutdown.complete'} ) assert down async def test_lifespan_shutdown_async_function(self): down = False async def shutdown(): nonlocal down down = True app = async_asgi.ASGIApp('eio', on_shutdown=shutdown) scope = {'type': 'lifespan'} receive = mock.AsyncMock(return_value={'type': 'lifespan.shutdown'}) send = mock.AsyncMock() await app(scope, receive, send) send.assert_awaited_once_with( {'type': 'lifespan.shutdown.complete'} ) assert down async def test_lifespan_shutdown_function_exception(self): down = False def shutdown(): raise Exception app = async_asgi.ASGIApp('eio', on_shutdown=shutdown) scope = {'type': 'lifespan'} receive = mock.AsyncMock(return_value={'type': 'lifespan.shutdown'}) send = mock.AsyncMock() await app(scope, receive, send) send.assert_awaited_once_with({'type': 'lifespan.shutdown.failed'}) assert not down async def test_lifespan_invalid(self): app = async_asgi.ASGIApp('eio') scope = {'type': 'lifespan'} receive = mock.AsyncMock(side_effect=[{'type': 'lifespan.foo'}, {'type': 'lifespan.shutdown'}]) send = mock.AsyncMock() await app(scope, receive, send) send.assert_awaited_once_with( {'type': 'lifespan.shutdown.complete'} ) async def test_not_found(self): app = async_asgi.ASGIApp('eio') scope = {'type': 'http', 'path': '/foo'} receive = mock.AsyncMock(return_value={'type': 'http.request'}) send = mock.AsyncMock() await app(scope, receive, send) send.assert_any_await( { 'type': 'http.response.start', 'status': 404, 'headers': [(b'Content-Type', b'text/plain')], } ) send.assert_any_await( {'type': 'http.response.body', 'body': b'Not Found'} ) async def test_translate_request(self): receive = mock.AsyncMock( return_value={'type': 'http.request', 'body': b'hello world'} ) send = mock.AsyncMock() environ = await async_asgi.translate_request( { 'type': 'http', 'method': 'PUT', 'headers': [ (b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), (b'content-type', b'application/json'), (b'content-length', b'123'), ], 'path': '/foo/bar', 'query_string': b'baz=1', }, receive, send, ) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', 'QUERY_STRING': 'baz=1', 'CONTENT_TYPE': 'application/json', 'CONTENT_LENGTH': '123', 'HTTP_A': 'b', # 'HTTP_C_C': 'd,e', 'RAW_URI': '/foo/bar?baz=1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'asgi.receive': receive, 'asgi.send': send, } for k, v in expected_environ.items(): assert v == environ[k] assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' body = await environ['wsgi.input'].read() assert body == b'hello world' async def test_translate_request_no_query_string(self): receive = mock.AsyncMock( return_value={'type': 'http.request', 'body': b'hello world'} ) send = mock.AsyncMock() environ = await async_asgi.translate_request( { 'type': 'http', 'method': 'PUT', 'headers': [ (b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), (b'content-type', b'application/json'), (b'content-length', b'123'), ], 'path': '/foo/bar', }, receive, send, ) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', 'QUERY_STRING': '', 'CONTENT_TYPE': 'application/json', 'CONTENT_LENGTH': '123', 'HTTP_A': 'b', # 'HTTP_C_C': 'd,e', 'RAW_URI': '/foo/bar', 'SERVER_PROTOCOL': 'HTTP/1.1', 'asgi.receive': receive, 'asgi.send': send, } for k, v in expected_environ.items(): assert v == environ[k] assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' body = await environ['wsgi.input'].read() assert body == b'hello world' async def test_translate_request_with_large_body(self): receive = mock.AsyncMock( side_effect=[ {'type': 'http.request', 'body': b'hello ', 'more_body': True}, {'type': 'http.request', 'body': b'world', 'more_body': True}, {'type': 'foo.bar'}, # should stop parsing here {'type': 'http.request', 'body': b'!!!'}, ] ) send = mock.AsyncMock() environ = await async_asgi.translate_request( { 'type': 'http', 'method': 'PUT', 'headers': [ (b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), (b'content-type', b'application/json'), (b'content-length', b'123'), ], 'path': '/foo/bar', 'query_string': b'baz=1', }, receive, send, ) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', 'QUERY_STRING': 'baz=1', 'CONTENT_TYPE': 'application/json', 'CONTENT_LENGTH': '123', 'HTTP_A': 'b', # 'HTTP_C_C': 'd,e', 'RAW_URI': '/foo/bar?baz=1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'asgi.receive': receive, 'asgi.send': send, } for k, v in expected_environ.items(): assert v == environ[k] assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' body = await environ['wsgi.input'].read() assert body == b'hello world' async def test_translate_websocket_request(self): receive = mock.AsyncMock(return_value={'type': 'websocket.connect'}) send = mock.AsyncMock() await async_asgi.translate_request( { 'type': 'websocket', 'headers': [ (b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), (b'content-type', b'application/json'), (b'content-length', b'123'), ], 'path': '/foo/bar', 'query_string': b'baz=1', }, receive, send, ) send.assert_not_awaited() async def test_translate_unknown_request(self): receive = mock.AsyncMock(return_value={'type': 'http.foo'}) send = mock.AsyncMock() environ = await async_asgi.translate_request( {'type': 'http', 'path': '/foo/bar', 'query_string': b'baz=1'}, receive, send, ) assert environ == {} async def test_translate_request_bad_unicode(self): receive = mock.AsyncMock(return_value={'type': 'http.request', 'body': b'foo'}) send = mock.AsyncMock() environ = await async_asgi.translate_request( { 'type': 'http.request', 'headers': [ (b'a', b'b'), (b'c', b'\xa0'), (b'e', b'f'), ], 'path': '/foo/bar', 'query_string': b'baz=1&bad=\xa0', }, receive, send, ) assert environ['HTTP_A'] == 'b' assert environ['HTTP_E'] == 'f' assert 'HTTP_C' not in environ assert environ['QUERY_STRING'] == '' assert environ['RAW_URI'] == '/foo/bar' async def test_make_response(self): environ = {'asgi.send': mock.AsyncMock(), 'asgi.scope': {'type': 'http'}} await async_asgi.make_response( '202 ACCEPTED', [('foo', 'bar')], b'payload', environ ) environ['asgi.send'].assert_any_await( { 'type': 'http.response.start', 'status': 202, 'headers': [(b'foo', b'bar')], } ) environ['asgi.send'].assert_any_await( {'type': 'http.response.body', 'body': b'payload'} ) async def test_make_response_websocket_accept(self): environ = { 'asgi.send': mock.AsyncMock(), 'asgi.scope': {'type': 'websocket'}, } await async_asgi.make_response( '200 OK', [('foo', 'bar')], b'payload', environ ) environ['asgi.send'].assert_awaited_with( {'type': 'websocket.accept', 'headers': [(b'foo', b'bar')]} ) async def test_make_response_websocket_reject(self): environ = { 'asgi.send': mock.AsyncMock(), 'asgi.scope': {'type': 'websocket'}, } await async_asgi.make_response( '401 UNAUTHORIZED', [('foo', 'bar')], b'payload', environ ) environ['asgi.send'].assert_awaited_with( {'type': 'websocket.close', 'reason': 'payload'} ) async def test_make_response_websocket_reject_no_payload(self): environ = { 'asgi.send': mock.AsyncMock(), 'asgi.scope': {'type': 'websocket'}, } await async_asgi.make_response( '401 UNAUTHORIZED', [('foo', 'bar')], None, environ ) environ['asgi.send'].assert_awaited_with( {'type': 'websocket.close'} ) async def test_sub_app_routing(self): class ASGIDispatcher: def __init__(self, routes): self.routes = routes async def __call__(self, scope, receive, send): path = scope['path'] for prefix, app in self.routes.items(): if path.startswith(prefix): await app(scope, receive, send) return assert False, 'No route found' other_app = mock.AsyncMock() mock_server = mock.MagicMock() mock_server.handle_request = mock.AsyncMock() eio_app = async_asgi.ASGIApp(mock_server, engineio_path=None) root_app = ASGIDispatcher({'/foo': other_app, '/eio': eio_app}) scope = {'type': 'http', 'path': '/foo/bar'} await root_app(scope, 'receive', 'send') other_app.assert_awaited_once_with(scope, 'receive', 'send') scope = {'type': 'http', 'path': '/eio/'} await root_app(scope, 'receive', 'send') eio_app.engineio_server.handle_request.assert_awaited_once_with( scope, 'receive', 'send') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/tests/async/test_client.py0000664000175000017500000015203214730320266021541 0ustar00miguelmiguelimport asyncio import ssl from unittest import mock try: import aiohttp except ImportError: aiohttp = None import pytest from engineio import async_client from engineio import base_client from engineio import exceptions from engineio import packet from engineio import payload class TestAsyncClient: async def test_is_asyncio_based(self): c = async_client.AsyncClient() assert c.is_asyncio_based() async def test_already_connected(self): c = async_client.AsyncClient() c.state = 'connected' with pytest.raises(ValueError): await c.connect('http://foo') async def test_invalid_transports(self): c = async_client.AsyncClient() with pytest.raises(ValueError): await c.connect('http://foo', transports=['foo', 'bar']) async def test_some_invalid_transports(self): c = async_client.AsyncClient() c._connect_websocket = mock.AsyncMock() await c.connect('http://foo', transports=['foo', 'websocket', 'bar']) assert c.transports == ['websocket'] async def test_connect_polling(self): c = async_client.AsyncClient() c._connect_polling = mock.AsyncMock(return_value='foo') assert await c.connect('http://foo') == 'foo' c._connect_polling.assert_awaited_once_with( 'http://foo', {}, 'engine.io' ) c = async_client.AsyncClient() c._connect_polling = mock.AsyncMock(return_value='foo') assert await c.connect('http://foo', transports=['polling']) == 'foo' c._connect_polling.assert_awaited_once_with( 'http://foo', {}, 'engine.io' ) c = async_client.AsyncClient() c._connect_polling = mock.AsyncMock(return_value='foo') assert ( await c.connect('http://foo', transports=['polling', 'websocket']) == 'foo' ) c._connect_polling.assert_awaited_once_with( 'http://foo', {}, 'engine.io' ) async def test_connect_websocket(self): c = async_client.AsyncClient() c._connect_websocket = mock.AsyncMock(return_value='foo') assert await c.connect('http://foo', transports=['websocket']) == 'foo' c._connect_websocket.assert_awaited_once_with( 'http://foo', {}, 'engine.io' ) c = async_client.AsyncClient() c._connect_websocket = mock.AsyncMock(return_value='foo') assert await c.connect('http://foo', transports='websocket') == 'foo' c._connect_websocket.assert_awaited_once_with( 'http://foo', {}, 'engine.io' ) async def test_connect_query_string(self): c = async_client.AsyncClient() c._connect_polling = mock.AsyncMock(return_value='foo') assert await c.connect('http://foo?bar=baz') == 'foo' c._connect_polling.assert_awaited_once_with( 'http://foo?bar=baz', {}, 'engine.io' ) async def test_connect_custom_headers(self): c = async_client.AsyncClient() c._connect_polling = mock.AsyncMock(return_value='foo') assert await c.connect('http://foo', headers={'Foo': 'Bar'}) == 'foo' c._connect_polling.assert_awaited_once_with( 'http://foo', {'Foo': 'Bar'}, 'engine.io' ) async def test_wait(self): c = async_client.AsyncClient() done = [] async def fake_read_look_task(): done.append(True) c.read_loop_task = fake_read_look_task() await c.wait() assert done == [True] async def test_wait_no_task(self): c = async_client.AsyncClient() c.read_loop_task = None await c.wait() async def test_send(self): c = async_client.AsyncClient() saved_packets = [] async def fake_send_packet(pkt): saved_packets.append(pkt) c._send_packet = fake_send_packet await c.send('foo') await c.send('foo') await c.send(b'foo') assert saved_packets[0].packet_type == packet.MESSAGE assert saved_packets[0].data == 'foo' assert not saved_packets[0].binary assert saved_packets[1].packet_type == packet.MESSAGE assert saved_packets[1].data == 'foo' assert not saved_packets[1].binary assert saved_packets[2].packet_type == packet.MESSAGE assert saved_packets[2].data == b'foo' assert saved_packets[2].binary async def test_disconnect_not_connected(self): c = async_client.AsyncClient() c.state = 'foo' c.sid = 'bar' await c.disconnect() assert c.state == 'disconnected' assert c.sid is None async def test_disconnect_polling(self): c = async_client.AsyncClient() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.queue.join = mock.AsyncMock() c.read_loop_task = mock.AsyncMock()() c.ws = mock.MagicMock() c.ws.close = mock.AsyncMock() c._trigger_event = mock.AsyncMock() await c.disconnect() c.ws.close.assert_not_awaited() assert c not in base_client.connected_clients c._trigger_event.assert_awaited_once_with( 'disconnect', c.reason.CLIENT_DISCONNECT, run_async=False ) async def test_disconnect_websocket(self): c = async_client.AsyncClient() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.queue.join = mock.AsyncMock() c.read_loop_task = mock.AsyncMock()() c.ws = mock.MagicMock() c.ws.close = mock.AsyncMock() c._trigger_event = mock.AsyncMock() await c.disconnect() c.ws.close.assert_awaited_once_with() assert c not in base_client.connected_clients c._trigger_event.assert_awaited_once_with( 'disconnect', c.reason.CLIENT_DISCONNECT, run_async=False ) async def test_disconnect_polling_abort(self): c = async_client.AsyncClient() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.queue.join = mock.AsyncMock() c.read_loop_task = mock.AsyncMock()() c.ws = mock.MagicMock() c.ws.close = mock.AsyncMock() await c.disconnect(abort=True) c.queue.join.assert_not_awaited() c.ws.close.assert_not_awaited() assert c not in base_client.connected_clients async def test_disconnect_websocket_abort(self): c = async_client.AsyncClient() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.queue.join = mock.AsyncMock() c.read_loop_task = mock.AsyncMock()() c.ws = mock.MagicMock() c.ws.close = mock.AsyncMock() await c.disconnect(abort=True) c.queue.join.assert_not_awaited() c.ws.assert_not_called() assert c not in base_client.connected_clients async def test_background_tasks(self): r = [] async def foo(arg): r.append(arg) c = async_client.AsyncClient() await c.start_background_task(foo, 'bar') assert r == ['bar'] async def test_sleep(self): c = async_client.AsyncClient() await c.sleep(0) async def test_create_queue(self): c = async_client.AsyncClient() q = c.create_queue() with pytest.raises(q.Empty): q.get_nowait() async def test_create_event(self): c = async_client.AsyncClient() e = c.create_event() assert not e.is_set() e.set() assert e.is_set() @mock.patch('engineio.client.time.time', return_value=123.456) async def test_polling_connection_failed(self, _time): c = async_client.AsyncClient() c._send_request = mock.AsyncMock(return_value=None) with pytest.raises(exceptions.ConnectionError): await c.connect('http://foo', headers={'Foo': 'Bar'}) c._send_request.assert_awaited_once_with( 'GET', 'http://foo/engine.io/?transport=polling&EIO=4&t=123.456', headers={'Foo': 'Bar'}, timeout=5, ) async def test_polling_connection_404(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 404 c._send_request.return_value.json = mock.AsyncMock( return_value={'foo': 'bar'} ) try: await c.connect('http://foo') except exceptions.ConnectionError as exc: assert len(exc.args) == 2 assert ( exc.args[0] == 'Unexpected status code 404 in server response' ) assert exc.args[1] == {'foo': 'bar'} async def test_polling_connection_404_no_json(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 404 c._send_request.return_value.json = mock.AsyncMock( side_effect=aiohttp.ContentTypeError('foo', 'bar') ) try: await c.connect('http://foo') except exceptions.ConnectionError as exc: assert len(exc.args) == 2 assert ( exc.args[0] == 'Unexpected status code 404 in server response' ) assert exc.args[1] is None async def test_polling_connection_invalid_packet(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock(return_value=b'foo') with pytest.raises(exceptions.ConnectionError): await c.connect('http://foo') async def test_polling_connection_no_open_packet(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock( return_value=payload.Payload( packets=[ packet.Packet( packet.CLOSE, { 'sid': '123', 'upgrades': [], 'pingInterval': 10, 'pingTimeout': 20, }, ) ] ).encode().encode('utf-8') ) with pytest.raises(exceptions.ConnectionError): await c.connect('http://foo') async def test_polling_connection_successful(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock( return_value=payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') ) c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.AsyncMock() c.on('connect', on_connect) await c.connect('http://foo') c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_awaited_once_with() assert c in base_client.connected_clients assert ( c.base_url == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' ) assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'polling' async def test_polling_https_noverify_connection_successful(self): c = async_client.AsyncClient(ssl_verify=False) c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock( return_value=payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') ) c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.AsyncMock() c.on('connect', on_connect) await c.connect('https://foo') c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_awaited_once_with() assert c in base_client.connected_clients assert ( c.base_url == 'https://foo/engine.io/?transport=polling&EIO=4&sid=123' ) assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'polling' async def test_polling_connection_with_more_packets(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock( return_value=payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ), packet.Packet(packet.NOOP), ] ).encode().encode('utf-8') ) c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() c._receive_packet = mock.AsyncMock() on_connect = mock.AsyncMock() c.on('connect', on_connect) await c.connect('http://foo') assert c._receive_packet.await_count == 1 assert ( c._receive_packet.await_args_list[0][0][0].packet_type == packet.NOOP ) async def test_polling_connection_upgraded(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock( return_value=payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': ['websocket'], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') ) c._connect_websocket = mock.AsyncMock(return_value=True) on_connect = mock.MagicMock() c.on('connect', on_connect) await c.connect('http://foo') c._connect_websocket.assert_awaited_once_with( 'http://foo', {}, 'engine.io' ) on_connect.assert_called_once_with() assert c in base_client.connected_clients assert ( c.base_url == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' ) assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == ['websocket'] async def test_polling_connection_not_upgraded(self): c = async_client.AsyncClient() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock( return_value=payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': ['websocket'], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') ) c._connect_websocket = mock.AsyncMock(return_value=False) c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) await c.connect('http://foo') c._connect_websocket.assert_awaited_once_with( 'http://foo', {}, 'engine.io' ) c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_connection_failed(self, _time): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock( side_effect=[aiohttp.client_exceptions.ServerConnectionError()] ) with pytest.raises(exceptions.ConnectionError): await c.connect( 'http://foo', transports=['websocket'], headers={'Foo': 'Bar'}, ) c.http.ws_connect.assert_awaited_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', headers={'Foo': 'Bar'}, timeout=5 ) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_connection_extra(self, _time): c = async_client.AsyncClient(websocket_extra_options={ 'headers': {'Baz': 'Qux'}, 'timeout': 10 }) c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock( side_effect=[aiohttp.client_exceptions.ServerConnectionError()] ) with pytest.raises(exceptions.ConnectionError): await c.connect( 'http://foo', transports=['websocket'], headers={'Foo': 'Bar'}, ) c.http.ws_connect.assert_awaited_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', headers={'Foo': 'Bar', 'Baz': 'Qux'}, timeout=10, ) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_upgrade_failed(self, _time): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock( side_effect=[aiohttp.client_exceptions.ServerConnectionError()] ) c.sid = '123' assert not await c.connect('http://foo', transports=['websocket']) c.http.ws_connect.assert_awaited_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&sid=123&t=123.456', headers={}, timeout=5, ) async def test_websocket_connection_no_open_packet(self): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.CLOSE ).encode() with pytest.raises(exceptions.ConnectionError): await c.connect('http://foo', transports=['websocket']) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_connection_successful(self, _time): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) await c.connect('ws://foo', transports=['websocket']) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients assert c.base_url == 'ws://foo/engine.io/?transport=websocket&EIO=4' assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'websocket' assert c.ws == ws c.http.ws_connect.assert_awaited_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', headers={}, timeout=5, ) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_https_noverify_connection_successful(self, _time): c = async_client.AsyncClient(ssl_verify=False) c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) await c.connect('wss://foo', transports=['websocket']) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients assert c.base_url == 'wss://foo/engine.io/?transport=websocket&EIO=4' assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'websocket' assert c.ws == ws _, kwargs = c.http.ws_connect.await_args assert 'ssl' in kwargs assert isinstance(kwargs['ssl'], ssl.SSLContext) assert kwargs['ssl'].verify_mode == ssl.CERT_NONE @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_connection_with_cookies(self, _time): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c.http._cookie_jar = [mock.MagicMock(), mock.MagicMock()] c.http._cookie_jar[0].key = 'key' c.http._cookie_jar[0].value = 'value' c.http._cookie_jar[1].key = 'key2' c.http._cookie_jar[1].value = 'value2' c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) await c.connect('ws://foo', transports=['websocket']) c.http.ws_connect.assert_awaited_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', headers={}, timeout=5, ) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_connection_with_cookie_header(self, _time): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c.http._cookie_jar = [] c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) await c.connect( 'ws://foo', headers={'Cookie': 'key=value; key2=value2; key3="value3="'}, transports=['websocket'], ) c.http.ws_connect.assert_awaited_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', headers={}, timeout=5, ) c.http.cookie_jar.update_cookies.assert_called_once_with( {'key': 'value', 'key2': 'value2', 'key3': '"value3="'} ) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_websocket_connection_with_cookies_and_headers(self, _time): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c.http._cookie_jar = [mock.MagicMock(), mock.MagicMock()] c.http._cookie_jar[0].key = 'key' c.http._cookie_jar[0].value = 'value' c.http._cookie_jar[1].key = 'key2' c.http._cookie_jar[1].value = 'value2' c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) await c.connect( 'ws://foo', headers={'Foo': 'Bar', 'Cookie': 'key3=value3'}, transports=['websocket'], ) c.http.ws_connect.assert_awaited_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', headers={'Foo': 'Bar'}, timeout=5, ) c.http.cookie_jar.update_cookies.assert_called_once_with( {'key3': 'value3'} ) async def test_websocket_upgrade_no_pong(self): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() ws.send_str = mock.AsyncMock() c.sid = '123' c.current_transport = 'polling' c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) assert not await c.connect('ws://foo', transports=['websocket']) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_not_called() c._write_loop.assert_not_called() on_connect.assert_not_called() assert c.transport() == 'polling' ws.send_str.assert_awaited_once_with('2probe') async def test_websocket_upgrade_successful(self): c = async_client.AsyncClient() c.http = mock.MagicMock(closed=False) c.http.ws_connect = mock.AsyncMock() ws = c.http.ws_connect.return_value ws.receive = mock.AsyncMock() ws.receive.return_value.data = packet.Packet( packet.PONG, 'probe' ).encode() ws.send_str = mock.AsyncMock() c.sid = '123' c.base_url = 'http://foo' c.current_transport = 'polling' c._read_loop_polling = mock.AsyncMock() c._read_loop_websocket = mock.AsyncMock() c._write_loop = mock.AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) assert await c.connect('ws://foo', transports=['websocket']) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_not_called() # was called by polling assert c not in base_client.connected_clients # was added by polling assert c.base_url == 'http://foo' # not changed assert c.sid == '123' # not changed assert c.transport() == 'websocket' assert c.ws == ws assert ws.send_str.await_args_list[0] == (('2probe',),) # ping assert ws.send_str.await_args_list[1] == (('5',),) # upgrade async def test_receive_unknown_packet(self): c = async_client.AsyncClient() await c._receive_packet(packet.Packet(encoded_packet='9')) # should be ignored async def test_receive_noop_packet(self): c = async_client.AsyncClient() await c._receive_packet(packet.Packet(packet.NOOP)) # should be ignored async def test_receive_ping_packet(self): c = async_client.AsyncClient() c._send_packet = mock.AsyncMock() await c._receive_packet(packet.Packet(packet.PING)) assert c._send_packet.await_args_list[0][0][0].encode() == '3' async def test_receive_message_packet(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock() await c._receive_packet(packet.Packet(packet.MESSAGE, {'foo': 'bar'})) c._trigger_event.assert_awaited_once_with( 'message', {'foo': 'bar'}, run_async=True ) async def test_receive_close_packet(self): c = async_client.AsyncClient() c.disconnect = mock.AsyncMock() await c._receive_packet(packet.Packet(packet.CLOSE)) c.disconnect.assert_awaited_once_with( abort=True, reason=c.reason.SERVER_DISCONNECT) async def test_send_packet_disconnected(self): c = async_client.AsyncClient() c.queue = c.create_queue() c.state = 'disconnected' await c._send_packet(packet.Packet(packet.NOOP)) assert c.queue.empty() async def test_send_packet(self): c = async_client.AsyncClient() c.queue = c.create_queue() c.state = 'connected' await c._send_packet(packet.Packet(packet.NOOP)) assert not c.queue.empty() pkt = await c.queue.get() assert pkt.packet_type == packet.NOOP async def test_trigger_event_function(self): result = [] def foo_handler(arg): result.append('ok') result.append(arg) c = async_client.AsyncClient() c.on('message', handler=foo_handler) await c._trigger_event('message', 'bar') assert result == ['ok', 'bar'] async def test_trigger_event_coroutine(self): result = [] async def foo_handler(arg): result.append('ok') result.append(arg) c = async_client.AsyncClient() c.on('message', handler=foo_handler) await c._trigger_event('message', 'bar') assert result == ['ok', 'bar'] async def test_trigger_event_function_error(self): def connect_handler(arg): return 1 / 0 def foo_handler(arg): return 1 / 0 c = async_client.AsyncClient() c.on('connect', handler=connect_handler) c.on('message', handler=foo_handler) assert not await c._trigger_event('connect', '123') assert await c._trigger_event('message', 'bar') is None async def test_trigger_event_coroutine_error(self): async def connect_handler(arg): return 1 / 0 async def foo_handler(arg): return 1 / 0 c = async_client.AsyncClient() c.on('connect', handler=connect_handler) c.on('message', handler=foo_handler) assert not await c._trigger_event('connect', '123') assert await c._trigger_event('message', 'bar') is None async def test_trigger_event_function_async(self): result = [] def foo_handler(arg): result.append('ok') result.append(arg) c = async_client.AsyncClient() c.on('message', handler=foo_handler) fut = await c._trigger_event('message', 'bar', run_async=True) await fut assert result == ['ok', 'bar'] async def test_trigger_event_coroutine_async(self): result = [] async def foo_handler(arg): result.append('ok') result.append(arg) c = async_client.AsyncClient() c.on('message', handler=foo_handler) fut = await c._trigger_event('message', 'bar', run_async=True) await fut assert result == ['ok', 'bar'] async def test_trigger_event_function_async_error(self): result = [] def foo_handler(arg): result.append(arg) return 1 / 0 c = async_client.AsyncClient() c.on('message', handler=foo_handler) fut = await c._trigger_event('message', 'bar', run_async=True) with pytest.raises(ZeroDivisionError): await fut assert result == ['bar'] async def test_trigger_event_coroutine_async_error(self): result = [] async def foo_handler(arg): result.append(arg) return 1 / 0 c = async_client.AsyncClient() c.on('message', handler=foo_handler) fut = await c._trigger_event('message', 'bar', run_async=True) with pytest.raises(ZeroDivisionError): await fut assert result == ['bar'] async def test_trigger_unknown_event(self): c = async_client.AsyncClient() await c._trigger_event('connect', run_async=False) await c._trigger_event('message', 123, run_async=True) # should do nothing async def test_trigger_legacy_disconnect_event(self): c = async_client.AsyncClient() @c.on('disconnect') def baz(): return 'baz' r = await c._trigger_event('disconnect', 'foo') assert r == 'baz' async def test_trigger_legacy_disconnect_event_async(self): c = async_client.AsyncClient() @c.on('disconnect') async def baz(): return 'baz' r = await c._trigger_event('disconnect', 'foo') assert r == 'baz' async def test_read_loop_polling_disconnected(self): c = async_client.AsyncClient() c.state = 'disconnected' c._trigger_event = mock.AsyncMock() c.write_loop_task = mock.AsyncMock()() await c._read_loop_polling() c._trigger_event.assert_not_awaited() # should not block @mock.patch('engineio.client.time.time', return_value=123.456) async def test_read_loop_polling_no_response(self, _time): c = async_client.AsyncClient() c.ping_interval = 25 c.ping_timeout = 5 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c._send_request = mock.AsyncMock(return_value=None) c._trigger_event = mock.AsyncMock() c.write_loop_task = mock.AsyncMock()() await c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_awaited_once_with(None) c._send_request.assert_awaited_once_with( 'GET', 'http://foo&t=123.456', timeout=30 ) c._trigger_event.assert_awaited_once_with( 'disconnect', c.reason.TRANSPORT_ERROR, run_async=False ) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_read_loop_polling_bad_status(self, _time): c = async_client.AsyncClient() c.ping_interval = 25 c.ping_timeout = 5 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 400 c.write_loop_task = mock.AsyncMock()() await c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_awaited_once_with(None) c._send_request.assert_awaited_once_with( 'GET', 'http://foo&t=123.456', timeout=30 ) @mock.patch('engineio.client.time.time', return_value=123.456) async def test_read_loop_polling_bad_packet(self, _time): c = async_client.AsyncClient() c.ping_interval = 25 c.ping_timeout = 60 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 c._send_request.return_value.read = mock.AsyncMock(return_value=b'foo') c.write_loop_task = mock.AsyncMock()() await c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_awaited_once_with(None) c._send_request.assert_awaited_once_with( 'GET', 'http://foo&t=123.456', timeout=65 ) async def test_read_loop_polling(self): c = async_client.AsyncClient() c.ping_interval = 25 c.ping_timeout = 5 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c._send_request = mock.AsyncMock() c._send_request.side_effect = [ mock.MagicMock( status=200, read=mock.AsyncMock( return_value=payload.Payload( packets=[ packet.Packet(packet.PING), packet.Packet(packet.NOOP), ] ).encode().encode('utf-8') ), ), None, ] c.write_loop_task = mock.AsyncMock()() c._receive_packet = mock.AsyncMock() await c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_awaited_once_with(None) assert c._send_request.await_count == 2 assert c._receive_packet.await_count == 2 assert c._receive_packet.await_args_list[0][0][0].encode() == '2' assert c._receive_packet.await_args_list[1][0][0].encode() == '6' async def test_read_loop_websocket_disconnected(self): c = async_client.AsyncClient() c.state = 'disconnected' c.write_loop_task = mock.AsyncMock()() await c._read_loop_websocket() # should not block async def test_read_loop_websocket_timeout(self): c = async_client.AsyncClient() c.ping_interval = 1 c.ping_timeout = 2 c.base_url = 'ws://foo' c.state = 'connected' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.ws = mock.MagicMock() c.ws.receive = mock.AsyncMock(side_effect=asyncio.TimeoutError()) c.write_loop_task = mock.AsyncMock()() await c._read_loop_websocket() assert c.state == 'disconnected' c.queue.put.assert_awaited_once_with(None) async def test_read_loop_websocket_no_response(self): c = async_client.AsyncClient() c.ping_interval = 1 c.ping_timeout = 2 c.base_url = 'ws://foo' c.state = 'connected' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.ws = mock.MagicMock() c.ws.receive = mock.AsyncMock( side_effect=aiohttp.client_exceptions.ServerDisconnectedError() ) c.write_loop_task = mock.AsyncMock()() await c._read_loop_websocket() assert c.state == 'disconnected' c.queue.put.assert_awaited_once_with(None) async def test_read_loop_websocket_unexpected_error(self): c = async_client.AsyncClient() c.ping_interval = 1 c.ping_timeout = 2 c.base_url = 'ws://foo' c.state = 'connected' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.ws = mock.MagicMock() c.ws.receive = mock.AsyncMock(side_effect=ValueError) c.write_loop_task = mock.AsyncMock()() await c._read_loop_websocket() assert c.state == 'disconnected' c.queue.put.assert_awaited_once_with(None) async def test_read_loop_websocket(self): c = async_client.AsyncClient() c.ping_interval = 1 c.ping_timeout = 2 c.base_url = 'ws://foo' c.state = 'connected' c.queue = mock.MagicMock() c.queue.put = mock.AsyncMock() c.ws = mock.MagicMock() c.ws.receive = mock.AsyncMock( side_effect=[ mock.MagicMock(data=packet.Packet(packet.PING).encode()), ValueError, ] ) c.write_loop_task = mock.AsyncMock()() c._receive_packet = mock.AsyncMock() await c._read_loop_websocket() assert c.state == 'disconnected' assert c._receive_packet.await_args_list[0][0][0].encode() == '2' c.queue.put.assert_awaited_once_with(None) async def test_write_loop_disconnected(self): c = async_client.AsyncClient() c.state = 'disconnected' await c._write_loop() # should not block async def test_write_loop_no_packets(self): c = async_client.AsyncClient() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.queue = mock.MagicMock() c.queue.get = mock.AsyncMock(return_value=None) await c._write_loop() c.queue.task_done.assert_called_once_with() c.queue.get.assert_awaited_once_with() async def test_write_loop_empty_queue(self): c = async_client.AsyncClient() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock(side_effect=RuntimeError) await c._write_loop() c.queue.get.assert_awaited_once_with() async def test_write_loop_polling_one_packet(self): c = async_client.AsyncClient() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] ) c.queue.get_nowait = mock.MagicMock(side_effect=RuntimeError) c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 await c._write_loop() assert c.queue.task_done.call_count == 1 p = payload.Payload( packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c._send_request.assert_awaited_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) async def test_write_loop_polling_three_packets(self): c = async_client.AsyncClient() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] ) c.queue.get_nowait = mock.MagicMock( side_effect=[ packet.Packet(packet.PING), packet.Packet(packet.NOOP), RuntimeError, ] ) c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 await c._write_loop() assert c.queue.task_done.call_count == 3 p = payload.Payload( packets=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), packet.Packet(packet.NOOP), ] ) c._send_request.assert_awaited_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) async def test_write_loop_polling_two_packets_done(self): c = async_client.AsyncClient() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] ) c.queue.get_nowait = mock.MagicMock( side_effect=[packet.Packet(packet.PING), None] ) c._send_request = mock.AsyncMock() c._send_request.return_value.status = 200 await c._write_loop() assert c.queue.task_done.call_count == 3 p = payload.Payload( packets=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), ] ) c._send_request.assert_awaited_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) assert c.state == 'connected' async def test_write_loop_polling_bad_connection(self): c = async_client.AsyncClient() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c._send_request = mock.AsyncMock(return_value=None) await c._write_loop() assert c.queue.task_done.call_count == 1 p = payload.Payload( packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c._send_request.assert_awaited_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) assert c.state == 'connected' async def test_write_loop_polling_bad_status(self): c = async_client.AsyncClient() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c._send_request = mock.AsyncMock() c._send_request.return_value.status = 500 await c._write_loop() assert c.queue.task_done.call_count == 1 p = payload.Payload( packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c._send_request.assert_awaited_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) assert c.state == 'connected' assert c.write_loop_task is None async def test_write_loop_websocket_one_packet(self): c = async_client.AsyncClient() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] ) c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c.ws = mock.MagicMock() c.ws.send_str = mock.AsyncMock() await c._write_loop() assert c.queue.task_done.call_count == 1 assert c.ws.send_str.await_count == 1 c.ws.send_str.assert_awaited_once_with('4{"foo":"bar"}') async def test_write_loop_websocket_three_packets(self): c = async_client.AsyncClient() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] ) c.queue.get_nowait = mock.MagicMock( side_effect=[ packet.Packet(packet.PING), packet.Packet(packet.NOOP), RuntimeError, ] ) c.ws = mock.MagicMock() c.ws.send_str = mock.AsyncMock() await c._write_loop() assert c.queue.task_done.call_count == 3 assert c.ws.send_str.await_count == 3 assert c.ws.send_str.await_args_list[0][0][0] == '4{"foo":"bar"}' assert c.ws.send_str.await_args_list[1][0][0] == '2' assert c.ws.send_str.await_args_list[2][0][0] == '6' async def test_write_loop_websocket_one_packet_binary(self): c = async_client.AsyncClient() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[packet.Packet(packet.MESSAGE, b'foo'), RuntimeError] ) c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c.ws = mock.MagicMock() c.ws.send_bytes = mock.AsyncMock() await c._write_loop() assert c.queue.task_done.call_count == 1 assert c.ws.send_bytes.await_count == 1 c.ws.send_bytes.assert_awaited_once_with(b'foo') async def test_write_loop_websocket_bad_connection(self): c = async_client.AsyncClient() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get = mock.AsyncMock( side_effect=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] ) c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c.ws = mock.MagicMock() c.ws.send_str = mock.AsyncMock( side_effect=aiohttp.client_exceptions.ServerDisconnectedError() ) await c._write_loop() assert c.state == 'connected' @mock.patch('engineio.base_client.original_signal_handler') async def test_signal_handler(self, original_handler): clients = [mock.MagicMock(), mock.MagicMock()] base_client.connected_clients = clients[:] base_client.connected_clients[0].is_asyncio_based.return_value = False base_client.connected_clients[1].is_asyncio_based.return_value = True base_client.connected_clients[1].disconnect = mock.AsyncMock() async_client.async_signal_handler() with pytest.raises(asyncio.CancelledError): await asyncio.sleep(0) clients[0].disconnect.assert_not_called() clients[1].disconnect.assert_called_once_with() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/async/test_sanic.py0000664000175000017500000000010614707424343021357 0ustar00miguelmiguelfrom engineio.async_drivers import sanic as async_sanic # noqa: F401 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/tests/async/test_server.py0000664000175000017500000013564314730320266021602 0ustar00miguelmiguelimport asyncio import gzip import io import logging from unittest import mock import zlib import pytest from engineio import async_server from engineio.async_drivers import aiohttp as async_aiohttp from engineio import exceptions from engineio import json from engineio import packet from engineio import payload class TestAsyncServer: @staticmethod def get_async_mock(environ={'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''}): if environ.get('QUERY_STRING'): if 'EIO=' not in environ['QUERY_STRING']: environ['QUERY_STRING'] = 'EIO=4&' + environ['QUERY_STRING'] else: environ['QUERY_STRING'] = 'EIO=4' a = mock.MagicMock() a._async = { 'asyncio': True, 'create_route': mock.MagicMock(), 'translate_request': mock.MagicMock(), 'make_response': mock.MagicMock(), 'websocket': 'w', } a._async['translate_request'].return_value = environ a._async['make_response'].return_value = 'response' return a def _get_mock_socket(self): mock_socket = mock.MagicMock() mock_socket.connected = False mock_socket.closed = False mock_socket.closing = False mock_socket.upgraded = False mock_socket.send = mock.AsyncMock() mock_socket.handle_get_request = mock.AsyncMock() mock_socket.handle_post_request = mock.AsyncMock() mock_socket.check_ping_timeout = mock.AsyncMock() mock_socket.close = mock.AsyncMock() mock_socket.session = {} return mock_socket @classmethod def setup_class(cls): async_server.AsyncServer._default_monitor_clients = False @classmethod def teardown_class(cls): async_server.AsyncServer._default_monitor_clients = True def setup_method(self): logging.getLogger('engineio').setLevel(logging.NOTSET) def teardown_method(self): # restore JSON encoder, in case a test changed it packet.Packet.json = json async def test_is_asyncio_based(self): s = async_server.AsyncServer() assert s.is_asyncio_based() async def test_async_modes(self): s = async_server.AsyncServer() assert s.async_modes() == ['aiohttp', 'sanic', 'tornado', 'asgi'] async def test_async_mode_aiohttp(self): s = async_server.AsyncServer(async_mode='aiohttp') assert s.async_mode == 'aiohttp' assert s._async['asyncio'] assert s._async['create_route'] == async_aiohttp.create_route assert s._async['translate_request'] == async_aiohttp.translate_request assert s._async['make_response'] == async_aiohttp.make_response assert s._async['websocket'].__name__ == 'WebSocket' @mock.patch('importlib.import_module') async def test_async_mode_auto_aiohttp(self, import_module): import_module.side_effect = [self.get_async_mock()] s = async_server.AsyncServer() assert s.async_mode == 'aiohttp' async def test_async_modes_wsgi(self): with pytest.raises(ValueError): async_server.AsyncServer(async_mode='eventlet') with pytest.raises(ValueError): async_server.AsyncServer(async_mode='gevent') with pytest.raises(ValueError): async_server.AsyncServer(async_mode='gevent_uwsgi') with pytest.raises(ValueError): async_server.AsyncServer(async_mode='threading') @mock.patch('importlib.import_module') async def test_attach(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() s.attach('app', engineio_path='abc') a._async['create_route'].assert_called_with('app', s, '/abc/') s.attach('app', engineio_path='/def/') a._async['create_route'].assert_called_with('app', s, '/def/') s.attach('app', engineio_path='/ghi') a._async['create_route'].assert_called_with('app', s, '/ghi/') s.attach('app', engineio_path='jkl/') a._async['create_route'].assert_called_with('app', s, '/jkl/') async def test_session(self): s = async_server.AsyncServer() s.sockets['foo'] = self._get_mock_socket() async def _func(): async with s.session('foo') as session: await s.sleep(0) session['username'] = 'bar' assert await s.get_session('foo') == {'username': 'bar'} await _func() async def test_disconnect(self): s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() await s.disconnect('foo') assert mock_socket.close.await_count == 1 mock_socket.close.assert_awaited_once_with( reason=s.reason.SERVER_DISCONNECT) assert 'foo' not in s.sockets async def test_disconnect_all(self): s = async_server.AsyncServer() s.sockets['foo'] = mock_foo = self._get_mock_socket() s.sockets['bar'] = mock_bar = self._get_mock_socket() await s.disconnect() assert mock_foo.close.await_count == 1 assert mock_bar.close.await_count == 1 mock_foo.close.assert_awaited_once_with( reason=s.reason.SERVER_DISCONNECT) mock_bar.close.assert_awaited_once_with( reason=s.reason.SERVER_DISCONNECT) assert 'foo' not in s.sockets assert 'bar' not in s.sockets @mock.patch('importlib.import_module') async def test_jsonp_not_supported(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'j=abc'} ) import_module.side_effect = [a] s = async_server.AsyncServer() response = await s.handle_request('request') assert response == 'response' a._async['translate_request'].assert_called_once_with('request') assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_jsonp_index(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'j=233'} ) import_module.side_effect = [a] s = async_server.AsyncServer() response = await s.handle_request('request') assert response == 'response' a._async['translate_request'].assert_called_once_with('request') assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '200 OK' assert ( a._async['make_response'] .call_args[0][2] .startswith(b'___eio[233]("') ) assert a._async['make_response'].call_args[0][2].endswith(b'");') @mock.patch('importlib.import_module') async def test_connect(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert len(s.sockets) == 1 assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '200 OK' assert ('Content-Type', 'text/plain; charset=UTF-8') in a._async[ 'make_response' ].call_args[0][1] packets = payload.Payload( encoded_payload=a._async['make_response'].call_args[0][2].decode( 'utf-8')).packets assert len(packets) == 1 assert packets[0].packet_type == packet.OPEN assert 'upgrades' in packets[0].data assert packets[0].data['upgrades'] == ['websocket'] assert 'sid' in packets[0].data assert packets[0].data['pingTimeout'] == 20000 assert packets[0].data['pingInterval'] == 25000 assert packets[0].data['maxPayload'] == 1000000 @mock.patch('importlib.import_module') async def test_connect_async_request_response_handlers( self, import_module): a = self.get_async_mock() a._async['translate_request'] = mock.AsyncMock( return_value=a._async['translate_request'].return_value ) a._async['make_response'] = mock.AsyncMock( return_value=a._async['make_response'].return_value ) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert len(s.sockets) == 1 assert a._async['make_response'].await_count == 1 assert a._async['make_response'].await_args[0][0] == '200 OK' assert ('Content-Type', 'text/plain; charset=UTF-8') in a._async[ 'make_response' ].await_args[0][1] packets = payload.Payload( encoded_payload=a._async['make_response'].await_args[0][ 2].decode('utf-8')).packets assert len(packets) == 1 assert packets[0].packet_type == packet.OPEN assert 'upgrades' in packets[0].data assert packets[0].data['upgrades'] == ['websocket'] assert 'sid' in packets[0].data @mock.patch('importlib.import_module') async def test_connect_no_upgrades(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(allow_upgrades=False) await s.handle_request('request') packets = payload.Payload( encoded_payload=a._async['make_response'].call_args[0][2].decode( 'utf-8')).packets assert packets[0].data['upgrades'] == [] @mock.patch('importlib.import_module') async def test_connect_bad_eio_version(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=1'} ) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' assert b'unsupported version' in \ a._async['make_response'].call_args[0][2] @mock.patch('importlib.import_module') async def test_connect_custom_ping_times(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(ping_timeout=123, ping_interval=456, max_http_buffer_size=12345678) await s.handle_request('request') packets = payload.Payload( encoded_payload=a._async['make_response'].call_args[0][2].decode( 'utf-8')).packets assert packets[0].data['pingTimeout'] == 123000 assert packets[0].data['pingInterval'] == 456000 assert packets[0].data['maxPayload'] == 12345678 @mock.patch('importlib.import_module') @mock.patch('engineio.async_server.async_socket.AsyncSocket') async def test_connect_bad_poll(self, AsyncSocket, import_module): a = self.get_async_mock() import_module.side_effect = [a] AsyncSocket.return_value = self._get_mock_socket() AsyncSocket.return_value.poll.side_effect = [exceptions.QueueEmpty] s = async_server.AsyncServer() await s.handle_request('request') assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') @mock.patch('engineio.async_server.async_socket.AsyncSocket') async def test_connect_transport_websocket(self, AsyncSocket, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=websocket', 'HTTP_UPGRADE': 'websocket', } ) import_module.side_effect = [a] AsyncSocket.return_value = self._get_mock_socket() s = async_server.AsyncServer() s.generate_id = mock.MagicMock(return_value='123') # force socket to stay open, so that we can check it later AsyncSocket().closed = False await s.handle_request('request') assert ( s.sockets['123'].send.await_args[0][0].packet_type == packet.OPEN ) @mock.patch('importlib.import_module') @mock.patch('engineio.async_server.async_socket.AsyncSocket') async def test_http_upgrade_case_insensitive(self, AsyncSocket, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=websocket', 'HTTP_UPGRADE': 'WebSocket', } ) import_module.side_effect = [a] AsyncSocket.return_value = self._get_mock_socket() s = async_server.AsyncServer() s.generate_id = mock.MagicMock(return_value='123') # force socket to stay open, so that we can check it later AsyncSocket().closed = False await s.handle_request('request') assert ( s.sockets['123'].send.await_args[0][0].packet_type == packet.OPEN ) @mock.patch('importlib.import_module') @mock.patch('engineio.async_server.async_socket.AsyncSocket') async def test_connect_transport_websocket_closed( self, AsyncSocket, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=websocket', 'HTTP_UPGRADE': 'websocket' } ) import_module.side_effect = [a] AsyncSocket.return_value = self._get_mock_socket() s = async_server.AsyncServer() s.generate_id = mock.MagicMock(return_value='123') # this mock handler just closes the socket, as it would happen on a # real websocket exchange async def mock_handle(environ): s.sockets['123'].closed = True AsyncSocket().handle_get_request = mock_handle await s.handle_request('request') assert '123' not in s.sockets # socket should close on its own @mock.patch('importlib.import_module') async def test_connect_transport_invalid(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_connect_transport_websocket_without_upgrade( self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=websocket'} ) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_connect_cors_headers(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Credentials', 'true') in headers @mock.patch('importlib.import_module') async def test_connect_cors_allowed_origin(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'b'} ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins=['a', 'b']) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Origin', 'b') in headers @mock.patch('importlib.import_module') async def test_connect_cors_allowed_origin_with_callable( self, import_module): def cors(origin): return origin == 'a' environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'a' } a = self.get_async_mock(environ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins=cors) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Origin', 'a') in headers environ['HTTP_ORIGIN'] = 'b' await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_connect_cors_not_allowed_origin(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'c'} ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins=['a', 'b']) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Origin', 'c') not in headers assert ('Access-Control-Allow-Origin', '*') not in headers @mock.patch('importlib.import_module') async def test_connect_cors_not_allowed_origin_async_response( self, import_module ): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'c'} ) a._async['make_response'] = mock.AsyncMock( return_value=a._async['make_response'].return_value ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins=['a', 'b']) await s.handle_request('request') assert ( a._async['make_response'].await_args[0][0] == '400 BAD REQUEST' ) headers = a._async['make_response'].await_args[0][1] assert ('Access-Control-Allow-Origin', 'c') not in headers assert ('Access-Control-Allow-Origin', '*') not in headers @mock.patch('importlib.import_module') async def test_connect_cors_all_origins(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins='*') await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Origin', 'foo') in headers assert ('Access-Control-Allow-Credentials', 'true') in headers @mock.patch('importlib.import_module') async def test_connect_cors_one_origin(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'a'} ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins='a') await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Origin', 'a') in headers assert ('Access-Control-Allow-Credentials', 'true') in headers @mock.patch('importlib.import_module') async def test_connect_cors_one_origin_not_allowed(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'b'} ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins='a') await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Origin', 'b') not in headers assert ('Access-Control-Allow-Origin', '*') not in headers @mock.patch('importlib.import_module') async def test_connect_cors_headers_default_origin(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'foo', 'HTTP_ORIGIN': 'http://foo', } ) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Origin', 'http://foo') in headers @mock.patch('importlib.import_module') async def test_connect_cors_no_credentials(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(cors_credentials=False) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ('Access-Control-Allow-Credentials', 'true') not in headers @mock.patch('importlib.import_module') async def test_connect_cors_options(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'OPTIONS', 'QUERY_STRING': ''} ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_credentials=False) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] assert ( 'Access-Control-Allow-Methods', 'OPTIONS, GET, POST', ) in headers @mock.patch('importlib.import_module') async def test_connect_cors_disabled(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'http://foo', } ) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins=[]) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: assert not header[0].startswith('Access-Control-') @mock.patch('importlib.import_module') async def test_connect_cors_default_no_origin(self, import_module): a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''}) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins=[]) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: assert header[0] != 'Access-Control-Allow-Origin' @mock.patch('importlib.import_module') async def test_connect_cors_all_no_origin(self, import_module): a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''}) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins='*') await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: assert header[0] != 'Access-Control-Allow-Origin' @mock.patch('importlib.import_module') async def test_connect_cors_disabled_no_origin(self, import_module): a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''}) import_module.side_effect = [a] s = async_server.AsyncServer(cors_allowed_origins=[]) await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: assert header[0] != 'Access-Control-Allow-Origin' @mock.patch('importlib.import_module') async def test_connect_event(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() s.generate_id = mock.MagicMock(return_value='123') def mock_connect(sid, environ): return True s.on('connect', handler=mock_connect) await s.handle_request('request') assert len(s.sockets) == 1 @mock.patch('importlib.import_module') async def test_connect_event_rejects(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() s.generate_id = mock.MagicMock(return_value='123') def mock_connect(sid, environ): return False s.on('connect')(mock_connect) await s.handle_request('request') assert len(s.sockets) == 0 assert a._async['make_response'].call_args[0][0] == '401 UNAUTHORIZED' assert a._async['make_response'].call_args[0][2] == b'"Unauthorized"' @mock.patch('importlib.import_module') async def test_connect_event_rejects_with_message(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() s.generate_id = mock.MagicMock(return_value='123') def mock_connect(sid, environ): return {'not': 'allowed'} s.on('connect')(mock_connect) await s.handle_request('request') assert len(s.sockets) == 0 assert a._async['make_response'].call_args[0][0] == '401 UNAUTHORIZED' assert ( a._async['make_response'].call_args[0][2] == b'{"not": "allowed"}' ) @mock.patch('importlib.import_module') async def test_method_not_found(self, import_module): a = self.get_async_mock({'REQUEST_METHOD': 'PUT', 'QUERY_STRING': ''}) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert len(s.sockets) == 0 assert ( a._async['make_response'].call_args[0][0] == '405 METHOD NOT FOUND' ) @mock.patch('importlib.import_module') async def test_get_request_with_bad_sid(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert len(s.sockets) == 0 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_get_request_bad_websocket_transport(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=websocket&sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.upgraded = False await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_get_request_bad_polling_transport(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=polling&sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.upgraded = True await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_post_request_with_bad_sid(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() await s.handle_request('request') assert len(s.sockets) == 0 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') async def test_send(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() await s.send('foo', 'hello') assert mock_socket.send.await_count == 1 assert ( mock_socket.send.await_args[0][0].packet_type == packet.MESSAGE ) assert mock_socket.send.await_args[0][0].data == 'hello' @mock.patch('importlib.import_module') async def test_send_unknown_socket(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer() # just ensure no exceptions are raised await s.send('foo', 'hello') @mock.patch('importlib.import_module') async def test_get_request(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = [ packet.Packet(packet.MESSAGE, data='hello') ] await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' packets = payload.Payload( encoded_payload=a._async['make_response'].call_args[0][2].decode( 'utf-8') ).packets assert len(packets) == 1 assert packets[0].packet_type == packet.MESSAGE @mock.patch('importlib.import_module') async def test_get_request_custom_response(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = 'resp' r = await s.handle_request('request') assert r == 'resp' @mock.patch('importlib.import_module') async def test_get_request_closes_socket(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() async def mock_get_request(*args, **kwargs): mock_socket.closed = True return 'resp' mock_socket.handle_get_request = mock_get_request r = await s.handle_request('request') assert r == 'resp' assert 'foo' not in s.sockets @mock.patch('importlib.import_module') async def test_get_request_error(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() async def mock_get_request(*args, **kwargs): raise exceptions.QueueEmpty() mock_socket.handle_get_request = mock_get_request await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' assert len(s.sockets) == 0 @mock.patch('importlib.import_module') async def test_post_request(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = self._get_mock_socket() await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '200 OK' @mock.patch('importlib.import_module') async def test_post_request_error(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} ) import_module.side_effect = [a] s = async_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() async def mock_post_request(*args, **kwargs): raise exceptions.ContentTooLongError() mock_socket.handle_post_request = mock_post_request await s.handle_request('request') assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @staticmethod def _gzip_decompress(b): bytesio = io.BytesIO(b) with gzip.GzipFile(fileobj=bytesio, mode='r') as gz: return gz.read() @mock.patch('importlib.import_module') async def test_gzip_compression(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_ACCEPT_ENCODING': 'gzip,deflate', } ) import_module.side_effect = [a] s = async_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = [ packet.Packet(packet.MESSAGE, data='hello') ] await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] assert ('Content-Encoding', 'gzip') in headers self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') async def test_deflate_compression(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_ACCEPT_ENCODING': 'deflate;q=1,gzip', } ) import_module.side_effect = [a] s = async_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = [ packet.Packet(packet.MESSAGE, data='hello') ] await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] assert ('Content-Encoding', 'deflate') in headers zlib.decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') async def test_gzip_compression_threshold(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_ACCEPT_ENCODING': 'gzip', } ) import_module.side_effect = [a] s = async_server.AsyncServer(compression_threshold=1000) s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = [ packet.Packet(packet.MESSAGE, data='hello') ] await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] for header, value in headers: assert header != 'Content-Encoding' with pytest.raises(IOError): print(a._async['make_response'].call_args[0][2]) self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') async def test_compression_disabled(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_ACCEPT_ENCODING': 'gzip', } ) import_module.side_effect = [a] s = async_server.AsyncServer( http_compression=False, compression_threshold=0 ) s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = [ packet.Packet(packet.MESSAGE, data='hello') ] await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] for header, value in headers: assert header != 'Content-Encoding' with pytest.raises(IOError): self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') async def test_compression_unknown(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_ACCEPT_ENCODING': 'rar', } ) import_module.side_effect = [a] s = async_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = [ packet.Packet(packet.MESSAGE, data='hello') ] await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] for header, value in headers: assert header != 'Content-Encoding' with pytest.raises(IOError): self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') async def test_compression_no_encoding(self, import_module): a = self.get_async_mock( { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_ACCEPT_ENCODING': '', } ) import_module.side_effect = [a] s = async_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.return_value = [ packet.Packet(packet.MESSAGE, data='hello') ] await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] for header, value in headers: assert header != 'Content-Encoding' with pytest.raises(IOError): self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') async def test_cookie(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(cookie='sid') s.generate_id = mock.MagicMock(return_value='123') await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] assert ('Set-Cookie', 'sid=123; path=/; SameSite=Lax') in headers @mock.patch('importlib.import_module') async def test_cookie_dict(self, import_module): def get_path(): return '/a' a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(cookie={ 'name': 'test', 'path': get_path, 'SameSite': 'None', 'Secure': True, 'HttpOnly': True }) s.generate_id = mock.MagicMock(return_value='123') await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] assert ('Set-Cookie', 'test=123; path=/a; SameSite=None; Secure; ' 'HttpOnly') in headers @mock.patch('importlib.import_module') async def test_no_cookie(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(cookie=None) s.generate_id = mock.MagicMock(return_value='123') await s.handle_request('request') headers = a._async['make_response'].call_args[0][1] for header, value in headers: assert header != 'Set-Cookie' async def test_logger(self): s = async_server.AsyncServer(logger=False) assert s.logger.getEffectiveLevel() == logging.ERROR s.logger.setLevel(logging.NOTSET) s = async_server.AsyncServer(logger=True) assert s.logger.getEffectiveLevel() == logging.INFO s.logger.setLevel(logging.WARNING) s = async_server.AsyncServer(logger=True) assert s.logger.getEffectiveLevel() == logging.WARNING s.logger.setLevel(logging.NOTSET) my_logger = logging.Logger('foo') s = async_server.AsyncServer(logger=my_logger) assert s.logger == my_logger async def test_custom_json(self): # Warning: this test cannot run in parallel with other tests, as it # changes the JSON encoding/decoding functions class CustomJSON: @staticmethod def dumps(*args, **kwargs): return '*** encoded ***' @staticmethod def loads(*args, **kwargs): return '+++ decoded +++' async_server.AsyncServer(json=CustomJSON) pkt = packet.Packet(packet.MESSAGE, data={'foo': 'bar'}) assert pkt.encode() == '4*** encoded ***' pkt2 = packet.Packet(encoded_packet=pkt.encode()) assert pkt2.data == '+++ decoded +++' # restore the default JSON module packet.Packet.json = json async def test_background_tasks(self): r = [] async def foo(arg): r.append(arg) async def main(): s = async_server.AsyncServer() task = s.start_background_task(foo, 'bar') await task await main() assert r == ['bar'] async def test_sleep(self): s = async_server.AsyncServer() await s.sleep(0) async def test_trigger_event_function(self): result = [] def foo_handler(arg): result.append('ok') result.append(arg) s = async_server.AsyncServer() s.on('message', handler=foo_handler) await s._trigger_event('message', 'bar') assert result == ['ok', 'bar'] async def test_trigger_event_coroutine(self): result = [] async def foo_handler(arg): result.append('ok') result.append(arg) s = async_server.AsyncServer() s.on('message', handler=foo_handler) await s._trigger_event('message', 'bar') assert result == ['ok', 'bar'] async def test_trigger_event_function_error(self): def connect_handler(arg): return 1 / 0 def foo_handler(arg): return 1 / 0 s = async_server.AsyncServer() s.on('connect', handler=connect_handler) s.on('message', handler=foo_handler) assert not await s._trigger_event('connect', '123') assert await s._trigger_event('message', 'bar') is None async def test_trigger_event_coroutine_error(self): async def connect_handler(arg): return 1 / 0 async def foo_handler(arg): return 1 / 0 s = async_server.AsyncServer() s.on('connect', handler=connect_handler) s.on('message', handler=foo_handler) assert not await s._trigger_event('connect', '123') assert await s._trigger_event('message', 'bar') is None async def test_trigger_event_function_async(self): result = [] def foo_handler(arg): result.append('ok') result.append(arg) s = async_server.AsyncServer() s.on('message', handler=foo_handler) fut = await s._trigger_event('message', 'bar', run_async=True) await fut assert result == ['ok', 'bar'] async def test_trigger_event_coroutine_async(self): result = [] async def foo_handler(arg): result.append('ok') result.append(arg) s = async_server.AsyncServer() s.on('message', handler=foo_handler) fut = await s._trigger_event('message', 'bar', run_async=True) await fut assert result == ['ok', 'bar'] async def test_trigger_event_function_async_error(self): result = [] def foo_handler(arg): result.append(arg) return 1 / 0 s = async_server.AsyncServer() s.on('message', handler=foo_handler) fut = await s._trigger_event('message', 'bar', run_async=True) await fut assert result == ['bar'] async def test_trigger_event_coroutine_async_error(self): result = [] async def foo_handler(arg): result.append(arg) return 1 / 0 s = async_server.AsyncServer() s.on('message', handler=foo_handler) fut = await s._trigger_event('message', 'bar', run_async=True) await fut assert result == ['bar'] async def test_trigger_legacy_disconnect_event(self): s = async_server.AsyncServer() @s.on('disconnect') def baz(sid): return sid r = await s._trigger_event('disconnect', 'foo', 'bar') assert r == 'foo' async def test_trigger_legacy_disconnect_event_async(self): s = async_server.AsyncServer() @s.on('disconnect') async def baz(sid): return sid r = await s._trigger_event('disconnect', 'foo', 'bar') assert r == 'foo' async def test_create_queue(self): s = async_server.AsyncServer() q = s.create_queue() empty = s.get_queue_empty_exception() with pytest.raises(empty): q.get_nowait() async def test_create_event(self): s = async_server.AsyncServer() e = s.create_event() assert not e.is_set() e.set() assert e.is_set() @mock.patch('importlib.import_module') async def test_service_task_started(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(monitor_clients=True) s._service_task = mock.AsyncMock() await s.handle_request('request') await asyncio.sleep(0) s._service_task.assert_awaited_once_with() @mock.patch('importlib.import_module') async def test_shutdown(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = async_server.AsyncServer(monitor_clients=True) await s.handle_request('request') await asyncio.sleep(0) assert s.service_task_handle is not None await s.shutdown() assert s.service_task_handle is None @mock.patch('importlib.import_module') async def test_transports_disallowed(self, import_module): a = self.get_async_mock( {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=polling'} ) import_module.side_effect = [a] s = async_server.AsyncServer(transports='websocket') response = await s.handle_request('request') assert response == 'response' a._async['translate_request'].assert_called_once_with('request') assert a._async['make_response'].call_count == 1 assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/tests/async/test_socket.py0000664000175000017500000004756314730320266021567 0ustar00miguelmiguelimport asyncio import time from unittest import mock import pytest from engineio import async_socket from engineio import exceptions from engineio import packet from engineio import payload class TestSocket: def _get_read_mock_coro(self, payload): mock_input = mock.MagicMock() mock_input.read = mock.AsyncMock() mock_input.read.return_value = payload return mock_input def _get_mock_server(self): mock_server = mock.Mock() mock_server.ping_timeout = 0.2 mock_server.ping_interval = 0.2 mock_server.ping_interval_grace_period = 0.001 mock_server.async_handlers = False mock_server.max_http_buffer_size = 128 mock_server._async = { 'asyncio': True, 'create_route': mock.MagicMock(), 'translate_request': mock.MagicMock(), 'make_response': mock.MagicMock(), 'websocket': 'w', } mock_server._async['translate_request'].return_value = 'request' mock_server._async['make_response'].return_value = 'response' mock_server._trigger_event = mock.AsyncMock() def bg_task(target, *args, **kwargs): return asyncio.ensure_future(target(*args, **kwargs)) def create_queue(*args, **kwargs): queue = asyncio.Queue(*args, **kwargs) queue.Empty = asyncio.QueueEmpty return queue mock_server.start_background_task = bg_task mock_server.create_queue = create_queue return mock_server async def test_create(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') assert s.server == mock_server assert s.sid == 'sid' assert not s.upgraded assert not s.closed assert hasattr(s.queue, 'get') assert hasattr(s.queue, 'put') assert hasattr(s.queue, 'task_done') assert hasattr(s.queue, 'join') async def test_empty_poll(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') with pytest.raises(exceptions.QueueEmpty): await s.poll() async def test_poll(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') await s.send(pkt1) await s.send(pkt2) assert await s.poll() == [pkt1, pkt2] async def test_poll_none(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') await s.queue.put(None) assert await s.poll() == [] async def test_poll_none_after_packet(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') pkt = packet.Packet(packet.MESSAGE, data='hello') await s.send(pkt) await s.queue.put(None) assert await s.poll() == [pkt] assert await s.poll() == [] async def test_schedule_ping(self): mock_server = self._get_mock_server() mock_server.ping_interval = 0.01 s = async_socket.AsyncSocket(mock_server, 'sid') s.send = mock.AsyncMock() async def schedule_ping(): s.schedule_ping() await asyncio.sleep(0.05) await schedule_ping() assert s.last_ping is not None assert s.send.await_args_list[0][0][0].encode() == '2' async def test_schedule_ping_closed_socket(self): mock_server = self._get_mock_server() mock_server.ping_interval = 0.01 s = async_socket.AsyncSocket(mock_server, 'sid') s.send = mock.AsyncMock() s.closed = True async def schedule_ping(): s.schedule_ping() await asyncio.sleep(0.05) await schedule_ping() assert s.last_ping is None s.send.assert_not_awaited() async def test_pong(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.schedule_ping = mock.MagicMock() await s.receive(packet.Packet(packet.PONG, data='abc')) s.schedule_ping.assert_called_once_with() async def test_message_sync_handler(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') await s.receive(packet.Packet(packet.MESSAGE, data='foo')) mock_server._trigger_event.assert_awaited_once_with( 'message', 'sid', 'foo', run_async=False ) async def test_message_async_handler(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') mock_server.async_handlers = True await s.receive(packet.Packet(packet.MESSAGE, data='foo')) mock_server._trigger_event.assert_awaited_once_with( 'message', 'sid', 'foo', run_async=True ) async def test_invalid_packet(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') with pytest.raises(exceptions.UnknownPacketError): await s.receive(packet.Packet(packet.OPEN)) async def test_timeout(self): mock_server = self._get_mock_server() mock_server.ping_interval = 6 mock_server.ping_interval_grace_period = 2 s = async_socket.AsyncSocket(mock_server, 'sid') s.last_ping = time.time() - 9 s.close = mock.AsyncMock() await s.send('packet') s.close.assert_awaited_once_with( wait=False, abort=False, reason=mock_server.reason.PING_TIMEOUT) async def test_polling_read(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'foo') pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') await s.send(pkt1) await s.send(pkt2) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} packets = await s.handle_get_request(environ) assert packets == [pkt1, pkt2] async def test_polling_read_error(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'foo') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} with pytest.raises(exceptions.QueueEmpty): await s.handle_get_request(environ) async def test_polling_write(self): mock_server = self._get_mock_server() mock_server.max_http_buffer_size = 1000 pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') s = async_socket.AsyncSocket(mock_server, 'foo') s.receive = mock.AsyncMock() environ = { 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', 'CONTENT_LENGTH': len(p), 'wsgi.input': self._get_read_mock_coro(p), } await s.handle_post_request(environ) assert s.receive.await_count == 2 async def test_polling_write_too_large(self): mock_server = self._get_mock_server() pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') mock_server.max_http_buffer_size = len(p) - 1 s = async_socket.AsyncSocket(mock_server, 'foo') s.receive = mock.AsyncMock() environ = { 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', 'CONTENT_LENGTH': len(p), 'wsgi.input': self._get_read_mock_coro(p), } with pytest.raises(exceptions.ContentTooLongError): await s.handle_post_request(environ) async def test_upgrade_handshake(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'foo') s._upgrade_websocket = mock.AsyncMock() environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_CONNECTION': 'Foo,Upgrade,Bar', 'HTTP_UPGRADE': 'websocket', } await s.handle_get_request(environ) s._upgrade_websocket.assert_awaited_once_with(environ) async def test_upgrade(self): mock_server = self._get_mock_server() mock_server._async['websocket'] = mock.MagicMock() mock_ws = mock.AsyncMock() mock_server._async['websocket'].return_value = mock_ws s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True environ = "foo" await s._upgrade_websocket(environ) mock_server._async['websocket'].assert_called_once_with( s._websocket_handler, mock_server ) mock_ws.assert_awaited_once_with(environ) async def test_upgrade_twice(self): mock_server = self._get_mock_server() mock_server._async['websocket'] = mock.MagicMock() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.upgraded = True environ = "foo" with pytest.raises(IOError): await s._upgrade_websocket(environ) async def test_upgrade_packet(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True await s.receive(packet.Packet(packet.UPGRADE)) r = await s.poll() assert len(r) == 1 assert r[0].encode() == packet.Packet(packet.NOOP).encode() async def test_upgrade_no_probe(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True ws = mock.MagicMock() ws.wait = mock.AsyncMock() ws.wait.return_value = packet.Packet(packet.NOOP).encode() await s._websocket_handler(ws) assert not s.upgraded async def test_upgrade_no_upgrade_packet(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = mock.AsyncMock(return_value=None) ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() probe = 'probe' ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.NOOP).encode(), ] await s._websocket_handler(ws) ws.send.assert_awaited_once_with( packet.Packet(packet.PONG, data=probe).encode() ) assert (await s.queue.get()).packet_type == packet.NOOP assert not s.upgraded async def test_upgrade_not_supported(self): mock_server = self._get_mock_server() mock_server._async['websocket'] = None s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True environ = "foo" await s._upgrade_websocket(environ) mock_server._bad_request.assert_called_once_with() async def test_close_packet(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.close = mock.AsyncMock() await s.receive(packet.Packet(packet.CLOSE)) s.close.assert_awaited_once_with( wait=False, abort=True, reason=mock_server.reason.CLIENT_DISCONNECT) async def test_websocket_read_write(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = False s.queue.join = mock.AsyncMock(return_value=None) foo = 'foo' bar = 'bar' s.poll = mock.AsyncMock( side_effect=[[packet.Packet(packet.MESSAGE, data=bar)], None] ) ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() ws.wait.side_effect = [ packet.Packet(packet.MESSAGE, data=foo).encode(), None, ] ws.close = mock.AsyncMock() await s._websocket_handler(ws) assert s.connected assert s.upgraded assert mock_server._trigger_event.await_count == 2 mock_server._trigger_event.assert_has_awaits( [ mock.call('message', 'sid', 'foo', run_async=False), mock.call('disconnect', 'sid', mock_server.reason.TRANSPORT_CLOSE, run_async=False), ] ) ws.send.assert_awaited_with('4bar') ws.close.assert_awaited() async def test_websocket_upgrade_read_write(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = mock.AsyncMock(return_value=None) foo = 'foo' bar = 'bar' probe = 'probe' s.poll = mock.AsyncMock( side_effect=[ [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty, ] ) ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE).encode(), packet.Packet(packet.MESSAGE, data=foo).encode(), None, ] ws.close = mock.AsyncMock() await s._websocket_handler(ws) assert s.upgraded assert mock_server._trigger_event.await_count == 2 mock_server._trigger_event.assert_has_awaits( [ mock.call('message', 'sid', 'foo', run_async=False), mock.call('disconnect', 'sid', mock_server.reason.TRANSPORT_CLOSE, run_async=False), ] ) ws.send.assert_awaited_with('4bar') ws.close.assert_awaited() async def test_websocket_upgrade_with_payload(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = mock.AsyncMock(return_value=None) probe = 'probe' ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE, data='2').encode(), ] ws.close = mock.AsyncMock() await s._websocket_handler(ws) assert s.upgraded async def test_websocket_upgrade_with_backlog(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = mock.AsyncMock(return_value=None) probe = 'probe' foo = 'foo' ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE, data='2').encode(), ] ws.close = mock.AsyncMock() s.upgrading = True await s.send(packet.Packet(packet.MESSAGE, data=foo)) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=sid'} packets = await s.handle_get_request(environ) assert len(packets) == 1 assert packets[0].encode() == '6' packets = await s.poll() assert len(packets) == 1 assert packets[0].encode() == '4foo' await s._websocket_handler(ws) assert s.upgraded assert not s.upgrading packets = await s.handle_get_request(environ) assert len(packets) == 1 assert packets[0].encode() == '6' async def test_websocket_read_write_wait_fail(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = False s.queue.join = mock.AsyncMock(return_value=None) foo = 'foo' bar = 'bar' s.poll = mock.AsyncMock( side_effect=[ [packet.Packet(packet.MESSAGE, data=bar)], [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty, ] ) ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() ws.wait.side_effect = [ packet.Packet(packet.MESSAGE, data=foo).encode(), RuntimeError, ] ws.send.side_effect = [None, RuntimeError] ws.close = mock.AsyncMock() await s._websocket_handler(ws) assert s.closed async def test_websocket_upgrade_with_large_packet(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = mock.AsyncMock(return_value=None) probe = 'probe' ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE, data='2' * 128).encode(), ] with pytest.raises(ValueError): await s._websocket_handler(ws) assert not s.upgraded async def test_websocket_ignore_invalid_packet(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.connected = False s.queue.join = mock.AsyncMock(return_value=None) foo = 'foo' bar = 'bar' s.poll = mock.AsyncMock( side_effect=[ [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty, ] ) ws = mock.MagicMock() ws.send = mock.AsyncMock() ws.wait = mock.AsyncMock() ws.wait.side_effect = [ packet.Packet(packet.OPEN).encode(), packet.Packet(packet.MESSAGE, data=foo).encode(), None, ] ws.close = mock.AsyncMock() await s._websocket_handler(ws) assert s.connected assert mock_server._trigger_event.await_count == 2 mock_server._trigger_event.assert_has_awaits( [ mock.call('message', 'sid', foo, run_async=False), mock.call('disconnect', 'sid', mock_server.reason.TRANSPORT_CLOSE, run_async=False), ] ) ws.send.assert_awaited_with('4bar') ws.close.assert_awaited() async def test_send_after_close(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') await s.close(wait=False) with pytest.raises(exceptions.SocketIsClosedError): await s.send(packet.Packet(packet.NOOP)) async def test_close_after_close(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') await s.close(wait=False) assert s.closed assert mock_server._trigger_event.await_count == 1 mock_server._trigger_event.assert_awaited_once_with( 'disconnect', 'sid', mock_server.reason.SERVER_DISCONNECT, run_async=False ) await s.close() assert mock_server._trigger_event.await_count == 1 async def test_close_and_wait(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.queue = mock.MagicMock() s.queue.put = mock.AsyncMock() s.queue.join = mock.AsyncMock() await s.close(wait=True) s.queue.join.assert_awaited_once_with() async def test_close_without_wait(self): mock_server = self._get_mock_server() s = async_socket.AsyncSocket(mock_server, 'sid') s.queue = mock.MagicMock() s.queue.put = mock.AsyncMock() s.queue.join = mock.AsyncMock() await s.close(wait=False) assert s.queue.join.await_count == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732493725.0 python_engineio-4.12.1/tests/async/test_tornado.py0000664000175000017500000000413414720740635021735 0ustar00miguelmiguelfrom unittest import mock try: import tornado.web except ImportError: pass from engineio.async_drivers import tornado as async_tornado class TestTornado: async def test_get_tornado_handler(self): mock_server = mock.MagicMock() handler = async_tornado.get_tornado_handler(mock_server) assert issubclass(handler, tornado.websocket.WebSocketHandler) async def test_translate_request(self): mock_handler = mock.MagicMock() mock_handler.request.method = 'PUT' mock_handler.request.path = '/foo/bar' mock_handler.request.query = 'baz=1' mock_handler.request.version = '1.1' mock_handler.request.headers = { 'a': 'b', 'c': 'd', 'content-type': 'application/json', 'content-length': 123, } mock_handler.request.body = b'hello world' environ = async_tornado.translate_request(mock_handler) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', 'QUERY_STRING': 'baz=1', 'CONTENT_TYPE': 'application/json', 'CONTENT_LENGTH': 123, 'HTTP_A': 'b', 'HTTP_C': 'd', 'RAW_URI': '/foo/bar?baz=1', 'SERVER_PROTOCOL': 'HTTP/1.1', # 'wsgi.input': b'hello world', 'tornado.handler': mock_handler, } for k, v in expected_environ.items(): assert v == environ[k] payload = await environ['wsgi.input'].read(1) payload += await environ['wsgi.input'].read() assert payload == b'hello world' async def test_make_response(self): mock_handler = mock.MagicMock() mock_environ = {'tornado.handler': mock_handler} async_tornado.make_response( '202 ACCEPTED', [('foo', 'bar')], b'payload', mock_environ ) mock_handler.set_status.assert_called_once_with(202) mock_handler.set_header.assert_called_once_with('foo', 'bar') mock_handler.write.assert_called_once_with(b'payload') mock_handler.finish.assert_called_once_with() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3643086 python_engineio-4.12.1/tests/common/0000775000175000017500000000000015010141646017015 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/common/__init__.py0000664000175000017500000000000014707424343021126 0ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/common/index.html0000664000175000017500000000001614707424343021021 0ustar00miguelmiguel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/tests/common/test_client.py0000664000175000017500000017117514730320266021725 0ustar00miguelmiguelimport logging import ssl import time from unittest import mock import pytest import websocket from engineio import base_client from engineio import client from engineio import exceptions from engineio import json from engineio import packet from engineio import payload class TestClient: def test_is_asyncio_based(self): c = client.Client() assert not c.is_asyncio_based() def test_create(self): c = client.Client() assert c.handlers == {} for attr in [ 'base_url', 'transports', 'sid', 'upgrades', 'ping_interval', 'ping_timeout', 'http', 'ws', 'read_loop_task', 'write_loop_task', 'queue', ]: assert getattr(c, attr) is None, attr + ' is not None' assert c.state == 'disconnected' def test_custom_json(self): client.Client() assert packet.Packet.json == json client.Client(json='foo') assert packet.Packet.json == 'foo' packet.Packet.json = json def test_logger(self): c = client.Client(logger=False) assert c.logger.getEffectiveLevel() == logging.ERROR c.logger.setLevel(logging.NOTSET) c = client.Client(logger=True) assert c.logger.getEffectiveLevel() == logging.INFO c.logger.setLevel(logging.WARNING) c = client.Client(logger=True) assert c.logger.getEffectiveLevel() == logging.WARNING c.logger.setLevel(logging.NOTSET) my_logger = logging.Logger('foo') c = client.Client(logger=my_logger) assert c.logger == my_logger def test_custom_timeout(self): c = client.Client() assert c.request_timeout == 5 c = client.Client(request_timeout=27) assert c.request_timeout == 27 def test_timestamp_requests(self): c = client.Client() assert c.timestamp_requests assert c._get_url_timestamp().startswith('&t=') c = client.Client(timestamp_requests=False) assert not c.timestamp_requests assert c._get_url_timestamp() == '' def test_on_event(self): c = client.Client() @c.on('connect') def foo(): pass c.on('disconnect', foo) assert c.handlers['connect'] == foo assert c.handlers['disconnect'] == foo def test_on_event_invalid(self): c = client.Client() with pytest.raises(ValueError): c.on('invalid') def test_already_connected(self): c = client.Client() c.state = 'connected' with pytest.raises(ValueError): c.connect('http://foo') def test_invalid_transports(self): c = client.Client() with pytest.raises(ValueError): c.connect('http://foo', transports=['foo', 'bar']) def test_some_invalid_transports(self): c = client.Client() c._connect_websocket = mock.MagicMock() c.connect('http://foo', transports=['foo', 'websocket', 'bar']) assert c.transports == ['websocket'] def test_connect_polling(self): c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') assert c.connect('http://foo') == 'foo' c._connect_polling.assert_called_once_with( 'http://foo', {}, 'engine.io' ) c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') assert c.connect('http://foo', transports=['polling']) == 'foo' c._connect_polling.assert_called_once_with( 'http://foo', {}, 'engine.io' ) c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') assert ( c.connect('http://foo', transports=['polling', 'websocket']) == 'foo' ) c._connect_polling.assert_called_once_with( 'http://foo', {}, 'engine.io' ) def test_connect_websocket(self): c = client.Client() c._connect_websocket = mock.MagicMock(return_value='foo') assert c.connect('http://foo', transports=['websocket']) == 'foo' c._connect_websocket.assert_called_once_with( 'http://foo', {}, 'engine.io' ) c = client.Client() c._connect_websocket = mock.MagicMock(return_value='foo') assert c.connect('http://foo', transports='websocket') == 'foo' c._connect_websocket.assert_called_once_with( 'http://foo', {}, 'engine.io' ) def test_connect_query_string(self): c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') assert c.connect('http://foo?bar=baz') == 'foo' c._connect_polling.assert_called_once_with( 'http://foo?bar=baz', {}, 'engine.io' ) def test_connect_custom_headers(self): c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') assert c.connect('http://foo', headers={'Foo': 'Bar'}) == 'foo' c._connect_polling.assert_called_once_with( 'http://foo', {'Foo': 'Bar'}, 'engine.io' ) def test_wait(self): c = client.Client() c.read_loop_task = mock.MagicMock() c.wait() c.read_loop_task.join.assert_called_once_with() def test_wait_no_task(self): c = client.Client() c.read_loop_task = None c.wait() # should not block def test_send(self): c = client.Client() saved_packets = [] def fake_send_packet(pkt): saved_packets.append(pkt) c._send_packet = fake_send_packet c.send('foo') c.send('foo') c.send(b'foo') assert saved_packets[0].packet_type == packet.MESSAGE assert saved_packets[0].data == 'foo' assert not saved_packets[0].binary assert saved_packets[1].packet_type == packet.MESSAGE assert saved_packets[1].data == 'foo' assert not saved_packets[1].binary assert saved_packets[2].packet_type == packet.MESSAGE assert saved_packets[2].data == b'foo' assert saved_packets[2].binary def test_disconnect_not_connected(self): c = client.Client() c.state = 'foo' c.sid = 'bar' c.disconnect() assert c.state == 'disconnected' assert c.sid is None def test_disconnect_polling(self): c = client.Client() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'polling' c.queue = mock.MagicMock() c.read_loop_task = mock.MagicMock() c.ws = mock.MagicMock() c._trigger_event = mock.MagicMock() c.disconnect() c.read_loop_task.join.assert_called_once_with() c.ws.mock.assert_not_called() assert c not in base_client.connected_clients c._trigger_event.assert_called_once_with( 'disconnect', c.reason.CLIENT_DISCONNECT, run_async=False) def test_disconnect_websocket(self): c = client.Client() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'websocket' c.queue = mock.MagicMock() c.read_loop_task = mock.MagicMock() c.ws = mock.MagicMock() c._trigger_event = mock.MagicMock() c.disconnect() c.read_loop_task.join.assert_called_once_with() c.ws.close.assert_called_once_with() assert c not in base_client.connected_clients c._trigger_event.assert_called_once_with( 'disconnect', c.reason.CLIENT_DISCONNECT, run_async=False) def test_disconnect_polling_abort(self): c = client.Client() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'polling' c.queue = mock.MagicMock() c.read_loop_task = mock.MagicMock() c.ws = mock.MagicMock() c.disconnect(abort=True) c.queue.join.assert_not_called() c.read_loop_task.join.assert_not_called() c.ws.mock.assert_not_called() assert c not in base_client.connected_clients def test_disconnect_websocket_abort(self): c = client.Client() base_client.connected_clients.append(c) c.state = 'connected' c.current_transport = 'websocket' c.queue = mock.MagicMock() c.read_loop_task = mock.MagicMock() c.ws = mock.MagicMock() c.disconnect(abort=True) c.queue.join.assert_not_called() c.read_loop_task.join.assert_not_called() c.ws.mock.assert_not_called() assert c not in base_client.connected_clients def test_current_transport(self): c = client.Client() c.current_transport = 'foo' assert c.transport() == 'foo' def test_background_tasks(self): flag = {} def bg_task(): flag['task'] = True c = client.Client() task = c.start_background_task(bg_task) task.join() assert 'task' in flag assert flag['task'] def test_sleep(self): c = client.Client() t = time.time() c.sleep(0.1) assert time.time() - t > 0.1 def test_create_queue(self): c = client.Client() q = c.create_queue() with pytest.raises(q.Empty): q.get(timeout=0.01) def test_create_event(self): c = client.Client() e = c.create_event() assert not e.is_set() e.set() assert e.is_set() @mock.patch('engineio.client.time.time', return_value=123.456) @mock.patch('engineio.client.Client._send_request', return_value=None) def test_polling_connection_failed(self, _send_request, _time): c = client.Client() with pytest.raises(exceptions.ConnectionError): c.connect('http://foo', headers={'Foo': 'Bar'}) _send_request.assert_called_once_with( 'GET', 'http://foo/engine.io/?transport=polling&EIO=4&t=123.456', headers={'Foo': 'Bar'}, timeout=5, ) @mock.patch('engineio.client.Client._send_request') def test_polling_connection_404(self, _send_request): _send_request.return_value.status_code = 404 _send_request.return_value.json.return_value = {'foo': 'bar'} c = client.Client() try: c.connect('http://foo') except exceptions.ConnectionError as exc: assert len(exc.args) == 2 assert ( exc.args[0] == 'Unexpected status code 404 in server response' ) assert exc.args[1] == {'foo': 'bar'} @mock.patch('engineio.client.Client._send_request') def test_polling_connection_404_no_json(self, _send_request): _send_request.return_value.status_code = 404 _send_request.return_value.json.side_effect = json.JSONDecodeError( 'error', '', 0) c = client.Client() try: c.connect('http://foo') except exceptions.ConnectionError as exc: assert len(exc.args) == 2 assert ( exc.args[0] == 'Unexpected status code 404 in server response' ) assert exc.args[1] is None @mock.patch('engineio.client.Client._send_request') def test_polling_connection_invalid_packet(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = b'foo' c = client.Client() with pytest.raises(exceptions.ConnectionError): c.connect('http://foo') @mock.patch('engineio.client.Client._send_request') def test_polling_connection_no_open_packet(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = payload.Payload( packets=[ packet.Packet( packet.CLOSE, { 'sid': '123', 'upgrades': [], 'pingInterval': 10, 'pingTimeout': 20, }, ) ] ).encode().encode('utf-8') c = client.Client() with pytest.raises(exceptions.ConnectionError): c.connect('http://foo') @mock.patch('engineio.client.Client._send_request') def test_polling_connection_successful(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') c = client.Client() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('http://foo') time.sleep(0.1) c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients assert ( c.base_url == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' ) assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'polling' @mock.patch('engineio.client.Client._send_request') def test_polling_https_noverify_connection_successful(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') c = client.Client(ssl_verify=False) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('https://foo') time.sleep(0.1) c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients assert ( c.base_url == 'https://foo/engine.io/?transport=polling&EIO=4&sid=123' ) assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'polling' @mock.patch('engineio.client.Client._send_request') def test_polling_connection_with_more_packets(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ), packet.Packet(packet.NOOP), ] ).encode().encode('utf-8') c = client.Client() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() c._receive_packet = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('http://foo') time.sleep(0.1) assert c._receive_packet.call_count == 1 assert ( c._receive_packet.call_args_list[0][0][0].packet_type == packet.NOOP ) @mock.patch('engineio.client.Client._send_request') def test_polling_connection_upgraded(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': ['websocket'], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') c = client.Client() c._connect_websocket = mock.MagicMock(return_value=True) on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('http://foo') c._connect_websocket.assert_called_once_with( 'http://foo', {}, 'engine.io' ) on_connect.assert_called_once_with() assert c in base_client.connected_clients assert ( c.base_url == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' ) assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == ['websocket'] @mock.patch('engineio.client.Client._send_request') def test_polling_connection_not_upgraded(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = payload.Payload( packets=[ packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': ['websocket'], 'pingInterval': 1000, 'pingTimeout': 2000, }, ) ] ).encode().encode('utf-8') c = client.Client() c._connect_websocket = mock.MagicMock(return_value=False) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('http://foo') time.sleep(0.1) c._connect_websocket.assert_called_once_with( 'http://foo', {}, 'engine.io' ) c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients @mock.patch('engineio.client.time.time', return_value=123.456) @mock.patch( 'engineio.client.websocket.create_connection', side_effect=[ConnectionError], ) def test_websocket_connection_failed(self, create_connection, _time): c = client.Client() with pytest.raises(exceptions.ConnectionError): c.connect( 'http://foo', transports=['websocket'], headers={'Foo': 'Bar'} ) create_connection.assert_called_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', header={'Foo': 'Bar'}, cookie=None, enable_multithread=True, timeout=5 ) @mock.patch('engineio.client.time.time', return_value=123.456) @mock.patch( 'engineio.client.websocket.create_connection', side_effect=[ConnectionError], ) def test_websocket_connection_extra(self, create_connection, _time): c = client.Client(websocket_extra_options={'header': {'Baz': 'Qux'}, 'timeout': 10}) with pytest.raises(exceptions.ConnectionError): c.connect( 'http://foo', transports=['websocket'], headers={'Foo': 'Bar'} ) create_connection.assert_called_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', header={'Foo': 'Bar', 'Baz': 'Qux'}, cookie=None, enable_multithread=True, timeout=10 ) @mock.patch('engineio.client.time.time', return_value=123.456) @mock.patch( 'engineio.client.websocket.create_connection', side_effect=[websocket.WebSocketException], ) def test_websocket_connection_failed_with_websocket_error( self, create_connection, _time ): c = client.Client() with pytest.raises(exceptions.ConnectionError): c.connect( 'http://foo', transports=['websocket'], headers={'Foo': 'Bar'} ) create_connection.assert_called_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', header={'Foo': 'Bar'}, cookie=None, enable_multithread=True, timeout=5 ) @mock.patch('engineio.client.time.time', return_value=123.456) @mock.patch( 'engineio.client.websocket.create_connection', side_effect=[ConnectionError], ) def test_websocket_upgrade_failed(self, create_connection, _time): c = client.Client() c.ping_interval = 1 c.ping_timeout = 2 c.sid = '123' assert not c.connect('http://foo', transports=['websocket']) create_connection.assert_called_once_with( 'ws://foo/engine.io/?transport=websocket&EIO=4&sid=123&t=123.456', header={}, cookie=None, enable_multithread=True, timeout=5 ) @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_no_open_packet(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.CLOSE ).encode() c = client.Client() with pytest.raises(exceptions.ConnectionError): c.connect('http://foo', transports=['websocket']) @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_successful(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c = client.Client() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) time.sleep(0.1) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients assert c.base_url == 'ws://foo/engine.io/?transport=websocket&EIO=4' assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'websocket' assert c.ws == create_connection.return_value assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'header': {}, 'cookie': None, 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_https_noverify_connection_successful( self, create_connection ): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c = client.Client(ssl_verify=False) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('wss://foo', transports=['websocket']) time.sleep(0.1) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() assert c in base_client.connected_clients assert c.base_url == 'wss://foo/engine.io/?transport=websocket&EIO=4' assert c.sid == '123' assert c.ping_interval == 1 assert c.ping_timeout == 2 assert c.upgrades == [] assert c.transport() == 'websocket' assert c.ws == create_connection.return_value assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'header': {}, 'cookie': None, 'enable_multithread': True, 'timeout': 5, 'sslopt': {'cert_reqs': ssl.CERT_NONE}, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_cookies(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [mock.MagicMock(), mock.MagicMock()] http.cookies[0].name = 'key' http.cookies[0].value = 'value' http.cookies[1].name = 'key2' http.cookies[1].value = 'value2' http.auth = None http.proxies = None http.cert = None c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) time.sleep(0.1) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'header': {}, 'cookie': 'key=value; key2=value2', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_cookie_header(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = None http.proxies = None http.cert = None c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect( 'ws://foo', headers={'Foo': 'bar', 'Cookie': 'key=value'}, transports=['websocket'], ) time.sleep(0.1) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'header': {'Foo': 'bar'}, 'cookie': 'key=value', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_cookies_and_headers( self, create_connection ): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [mock.MagicMock(), mock.MagicMock()] http.cookies[0].name = 'key' http.cookies[0].value = 'value' http.cookies[1].name = 'key2' http.cookies[1].value = 'value2' http.auth = None http.proxies = None http.cert = None c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect( 'ws://foo', headers={'Cookie': 'key3=value3'}, transports=['websocket'], ) time.sleep(0.1) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'header': {}, 'enable_multithread': True, 'timeout': 5, 'cookie': 'key=value; key2=value2; key3=value3', } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_auth(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = ('foo', 'bar') http.proxies = None http.cert = None c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'header': {'Authorization': 'Basic Zm9vOmJhcg=='}, 'cookie': '', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_cert(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = None http.proxies = None http.cert = 'foo.crt' c = client.Client(http_session=http, ssl_verify=False) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'sslopt': {'cert_reqs': ssl.CERT_NONE, 'certfile': 'foo.crt'}, 'header': {}, 'cookie': '', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_cert_and_key(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = None http.proxies = None http.cert = ('foo.crt', 'key.pem') c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'sslopt': {'certfile': 'foo.crt', 'keyfile': 'key.pem'}, 'header': {}, 'cookie': '', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_verify_with_cert_and_key( self, create_connection ): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = None http.proxies = None http.cert = ('foo.crt', 'key.pem') http.verify = 'ca-bundle.crt' c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'sslopt': { 'certfile': 'foo.crt', 'keyfile': 'key.pem', 'ca_certs': 'ca-bundle.crt' }, 'header': {}, 'cookie': '', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_proxies(self, create_connection): all_urls = [ 'ws://foo', 'ws://foo', 'ws://foo', 'ws://foo', 'ws://foo', 'wss://foo', 'wss://foo', ] all_proxies = [ {'http': 'foo.com:1234'}, {'https': 'foo.com:1234'}, {'http': 'foo.com:1234', 'ws': 'bar.com:4321'}, {}, {'http': 'user:pass@foo.com:1234'}, {'https': 'foo.com:1234'}, {'https': 'foo.com:1234', 'wss': 'bar.com:4321'}, ] all_results = [ ('foo.com', 1234, None), None, ('bar.com', 4321, None), None, ('foo.com', 1234, ('user', 'pass')), ('foo.com', 1234, None), ('bar.com', 4321, None), ] for url, proxies, results in zip(all_urls, all_proxies, all_results): create_connection.reset_mock() create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = None http.proxies = proxies http.cert = None c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect(url, transports=['websocket']) assert len(create_connection.call_args_list) == 1 expected_results = { 'header': {}, 'cookie': '', 'enable_multithread': True, 'timeout': 5, } if results: expected_results.update({ 'http_proxy_host': results[0], 'http_proxy_port': results[1], 'http_proxy_auth': results[2]}) assert create_connection.call_args[1] == expected_results @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_without_verify(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = None http.proxies = None http.cert = None http.verify = False c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'sslopt': {"cert_reqs": ssl.CERT_NONE}, 'header': {}, 'cookie': '', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_verify(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() http = mock.MagicMock() http.cookies = [] http.auth = None http.proxies = None http.cert = None http.verify = 'ca-bundle.crt' c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('ws://foo', transports=['websocket']) assert len(create_connection.call_args_list) == 1 assert create_connection.call_args[1] == { 'sslopt': {'ca_certs': 'ca-bundle.crt'}, 'header': {}, 'cookie': '', 'enable_multithread': True, 'timeout': 5, } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_upgrade_no_pong(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.OPEN, { 'sid': '123', 'upgrades': [], 'pingInterval': 1000, 'pingTimeout': 2000, }, ).encode() c = client.Client() c.sid = '123' c.current_transport = 'polling' c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) assert not c.connect('ws://foo', transports=['websocket']) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_not_called() c._write_loop.assert_not_called() on_connect.assert_not_called() assert c.transport() == 'polling' @mock.patch('engineio.client.websocket.create_connection') def test_websocket_upgrade_successful(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( packet.PONG, 'probe').encode() c = client.Client() c.ping_interval = 1 c.ping_timeout = 2 c.sid = '123' c.base_url = 'http://foo' c.current_transport = 'polling' c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) assert c.connect('ws://foo', transports=['websocket']) time.sleep(0.1) c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_not_called() # was called by polling assert c not in base_client.connected_clients # was added by polling assert c.base_url == 'http://foo' # not changed assert c.sid == '123' # not changed assert c.transport() == 'websocket' assert c.ws == create_connection.return_value assert create_connection.return_value.send.call_args_list[0] == ( (packet.Packet(packet.PING, 'probe').encode(),), ) # ping assert create_connection.return_value.send.call_args_list[1] == ( (packet.Packet(packet.UPGRADE).encode(),), ) # upgrade def test_receive_unknown_packet(self): c = client.Client() c._receive_packet(packet.Packet(encoded_packet='9')) # should be ignored def test_receive_noop_packet(self): c = client.Client() c._receive_packet(packet.Packet(packet.NOOP)) # should be ignored def test_receive_ping_packet(self): c = client.Client() c._send_packet = mock.MagicMock() c._receive_packet(packet.Packet(packet.PING)) assert c._send_packet.call_args_list[0][0][0].encode() == '3' # PONG def test_receive_message_packet(self): c = client.Client() c._trigger_event = mock.MagicMock() c._receive_packet(packet.Packet(packet.MESSAGE, {'foo': 'bar'})) c._trigger_event.assert_called_once_with( 'message', {'foo': 'bar'}, run_async=True ) def test_receive_close_packet(self): c = client.Client() c.disconnect = mock.MagicMock() c._receive_packet(packet.Packet(packet.CLOSE)) c.disconnect.assert_called_once_with( abort=True, reason=c.reason.SERVER_DISCONNECT) def test_send_packet_disconnected(self): c = client.Client() c.queue = c.create_queue() c.state = 'disconnected' c._send_packet(packet.Packet(packet.NOOP)) assert c.queue.empty() def test_send_packet(self): c = client.Client() c.queue = c.create_queue() c.state = 'connected' c._send_packet(packet.Packet(packet.NOOP)) assert not c.queue.empty() pkt = c.queue.get() assert pkt.packet_type == packet.NOOP def test_trigger_event(self): c = client.Client() f = {} @c.on('connect') def foo(): return 'foo' @c.on('message') def bar(data): f['bar'] = data return 'bar' @c.on('disconnect') def baz(reason): return reason r = c._trigger_event('connect', run_async=False) assert r == 'foo' r = c._trigger_event('message', 123, run_async=True) r.join() assert f['bar'] == 123 r = c._trigger_event('message', 321) assert r == 'bar' r = c._trigger_event('disconnect', 'foo') assert r == 'foo' def test_trigger_legacy_disconnect_event(self): c = client.Client() @c.on('disconnect') def baz(): return 'baz' r = c._trigger_event('disconnect', 'foo') assert r == 'baz' def test_trigger_unknown_event(self): c = client.Client() c._trigger_event('connect', run_async=False) c._trigger_event('message', 123, run_async=True) # should do nothing def test_trigger_event_error(self): c = client.Client() @c.on('connect') def foo(): return 1 / 0 @c.on('message') def bar(data): return 1 / 0 r = c._trigger_event('connect', run_async=False) assert r is None r = c._trigger_event('message', 123, run_async=False) assert r is None def test_engineio_url(self): c = client.Client() assert ( c._get_engineio_url('http://foo', 'bar', 'polling') == 'http://foo/bar/?transport=polling&EIO=4' ) assert ( c._get_engineio_url('http://foo', 'bar', 'websocket') == 'ws://foo/bar/?transport=websocket&EIO=4' ) assert ( c._get_engineio_url('ws://foo', 'bar', 'polling') == 'http://foo/bar/?transport=polling&EIO=4' ) assert ( c._get_engineio_url('ws://foo', 'bar', 'websocket') == 'ws://foo/bar/?transport=websocket&EIO=4' ) assert ( c._get_engineio_url('https://foo', 'bar', 'polling') == 'https://foo/bar/?transport=polling&EIO=4' ) assert ( c._get_engineio_url('https://foo', 'bar', 'websocket') == 'wss://foo/bar/?transport=websocket&EIO=4' ) assert ( c._get_engineio_url('http://foo?baz=1', 'bar', 'polling') == 'http://foo/bar/?baz=1&transport=polling&EIO=4' ) assert ( c._get_engineio_url('http://foo#baz', 'bar', 'polling') == 'http://foo/bar/?transport=polling&EIO=4' ) def test_read_loop_polling_disconnected(self): c = client.Client() c.state = 'disconnected' c._trigger_event = mock.MagicMock() c.write_loop_task = mock.MagicMock() c._read_loop_polling() c.write_loop_task.join.assert_called_once_with() c._trigger_event.assert_not_called() @mock.patch('engineio.client.time.time', return_value=123.456) def test_read_loop_polling_no_response(self, _time): c = client.Client() c.ping_interval = 25 c.ping_timeout = 5 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c._send_request = mock.MagicMock(return_value=None) c._trigger_event = mock.MagicMock() c.write_loop_task = mock.MagicMock() c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() c._send_request.assert_called_once_with( 'GET', 'http://foo&t=123.456', timeout=30 ) c._trigger_event.assert_called_once_with( 'disconnect', c.reason.TRANSPORT_ERROR, run_async=False) @mock.patch('engineio.client.time.time', return_value=123.456) def test_read_loop_polling_bad_status(self, _time): c = client.Client() c.ping_interval = 25 c.ping_timeout = 5 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 400 c.write_loop_task = mock.MagicMock() c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() c._send_request.assert_called_once_with( 'GET', 'http://foo&t=123.456', timeout=30 ) @mock.patch('engineio.client.time.time', return_value=123.456) def test_read_loop_polling_bad_packet(self, _time): c = client.Client() c.ping_interval = 25 c.ping_timeout = 60 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 200 c._send_request.return_value.content = b'foo' c.write_loop_task = mock.MagicMock() c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() c._send_request.assert_called_once_with( 'GET', 'http://foo&t=123.456', timeout=65 ) def test_read_loop_polling(self): c = client.Client() c.ping_interval = 25 c.ping_timeout = 5 c.state = 'connected' c.base_url = 'http://foo' c.queue = mock.MagicMock() c._send_request = mock.MagicMock() c._send_request.side_effect = [ mock.MagicMock( status_code=200, content=payload.Payload( packets=[ packet.Packet(packet.PING), packet.Packet(packet.NOOP), ] ).encode().encode('utf-8'), ), None, ] c.write_loop_task = mock.MagicMock() c._receive_packet = mock.MagicMock() c._read_loop_polling() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) assert c._send_request.call_count == 2 assert c._receive_packet.call_count == 2 assert c._receive_packet.call_args_list[0][0][0].encode() == '2' assert c._receive_packet.call_args_list[1][0][0].encode() == '6' def test_read_loop_websocket_disconnected(self): c = client.Client() c.state = 'disconnected' c.write_loop_task = mock.MagicMock() c._read_loop_websocket() c.write_loop_task.join.assert_called_once_with() def test_read_loop_websocket_timeout(self): c = client.Client() c.state = 'connected' c.queue = mock.MagicMock() c.ws = mock.MagicMock() c.ws.recv.side_effect = websocket.WebSocketTimeoutException c.write_loop_task = mock.MagicMock() c._read_loop_websocket() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() def test_read_loop_websocket_no_response(self): c = client.Client() c.state = 'connected' c.queue = mock.MagicMock() c.ws = mock.MagicMock() c.ws.recv.side_effect = websocket.WebSocketConnectionClosedException c.write_loop_task = mock.MagicMock() c._read_loop_websocket() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() def test_read_loop_websocket_unexpected_error(self): c = client.Client() c.state = 'connected' c.queue = mock.MagicMock() c.ws = mock.MagicMock() c.ws.recv.side_effect = ValueError c.write_loop_task = mock.MagicMock() c._read_loop_websocket() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() def test_read_loop_websocket(self): c = client.Client() c.ping_interval = 1 c.ping_timeout = 2 c.state = 'connected' c.queue = mock.MagicMock() c.ws = mock.MagicMock() c.ws.recv.side_effect = [ packet.Packet(packet.PING).encode(), ValueError, ] c.write_loop_task = mock.MagicMock() c._receive_packet = mock.MagicMock() c._read_loop_websocket() assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() assert c._receive_packet.call_args_list[0][0][0].encode() == '2' def test_write_loop_disconnected(self): c = client.Client() c.state = 'disconnected' c._write_loop() # should not block def test_write_loop_no_packets(self): c = client.Client() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.queue = mock.MagicMock() c.queue.get.return_value = None c._write_loop() c.queue.task_done.assert_called_once_with() c.queue.get.assert_called_once_with(timeout=7) def test_write_loop_empty_queue(self): c = client.Client() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = RuntimeError c._write_loop() c.queue.get.assert_called_once_with(timeout=7) def test_write_loop_polling_one_packet(self): c = client.Client() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 200 c._write_loop() assert c.queue.task_done.call_count == 1 p = payload.Payload( packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c._send_request.assert_called_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) def test_write_loop_polling_three_packets(self): c = client.Client() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), packet.Packet(packet.NOOP), RuntimeError, RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 200 c._write_loop() assert c.queue.task_done.call_count == 3 p = payload.Payload( packets=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), packet.Packet(packet.NOOP), ] ) c._send_request.assert_called_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) def test_write_loop_polling_two_packets_done(self): c = client.Client() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), None, RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 200 c._write_loop() assert c.queue.task_done.call_count == 3 p = payload.Payload( packets=[ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), ] ) c._send_request.assert_called_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) assert c.state == 'connected' def test_write_loop_polling_bad_connection(self): c = client.Client() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value = None c._write_loop() assert c.queue.task_done.call_count == 1 p = payload.Payload( packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c._send_request.assert_called_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) assert c.state == 'connected' def test_write_loop_polling_bad_status(self): c = client.Client() c.base_url = 'http://foo' c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 500 c._write_loop() assert c.queue.task_done.call_count == 1 p = payload.Payload( packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] ) c._send_request.assert_called_once_with( 'POST', 'http://foo', body=p.encode(), headers={'Content-Type': 'text/plain'}, timeout=5, ) assert c.state == 'connected' assert c.write_loop_task is None def test_write_loop_websocket_one_packet(self): c = client.Client() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, RuntimeError, ] c.ws = mock.MagicMock() c._write_loop() assert c.queue.task_done.call_count == 1 assert c.ws.send.call_count == 1 assert c.ws.send_binary.call_count == 0 c.ws.send.assert_called_once_with('4{"foo":"bar"}') def test_write_loop_websocket_three_packets(self): c = client.Client() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), packet.Packet(packet.NOOP), RuntimeError, RuntimeError, ] c.ws = mock.MagicMock() c._write_loop() assert c.queue.task_done.call_count == 3 assert c.ws.send.call_count == 3 assert c.ws.send_binary.call_count == 0 assert c.ws.send.call_args_list[0][0][0] == '4{"foo":"bar"}' assert c.ws.send.call_args_list[1][0][0] == '2' assert c.ws.send.call_args_list[2][0][0] == '6' def test_write_loop_websocket_one_packet_binary(self): c = client.Client() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, b'foo'), RuntimeError, RuntimeError, ] c.ws = mock.MagicMock() c._write_loop() assert c.queue.task_done.call_count == 1 assert c.ws.send.call_count == 0 assert c.ws.send_binary.call_count == 1 c.ws.send_binary.assert_called_once_with(b'foo') def test_write_loop_websocket_bad_connection(self): c = client.Client() c.state = 'connected' c.ping_interval = 1 c.ping_timeout = 2 c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, RuntimeError, ] c.ws = mock.MagicMock() c.ws.send.side_effect = websocket.WebSocketConnectionClosedException c._write_loop() assert c.state == 'connected' @mock.patch('engineio.base_client.original_signal_handler') def test_signal_handler(self, original_handler): clients = [mock.MagicMock(), mock.MagicMock()] base_client.connected_clients = clients[:] base_client.connected_clients[0].is_asyncio_based.return_value = False base_client.connected_clients[1].is_asyncio_based.return_value = True base_client.signal_handler('sig', 'frame') clients[0].disconnect.assert_called_once_with() clients[1].disconnect.assert_not_called() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732474535.0 python_engineio-4.12.1/tests/common/test_middleware.py0000664000175000017500000001654414720673247022573 0ustar00miguelmiguelimport os from unittest import mock import engineio class TestWSGIApp: def test_wsgi_routing(self): mock_wsgi_app = mock.MagicMock() mock_eio_app = 'foo' m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app) environ = {'PATH_INFO': '/foo'} start_response = "foo" m(environ, start_response) mock_wsgi_app.assert_called_once_with(environ, start_response) def test_eio_routing(self): mock_wsgi_app = 'foo' mock_eio_app = mock.Mock() mock_eio_app.handle_request = mock.MagicMock() m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app) environ = {'PATH_INFO': '/engine.io/'} start_response = "foo" m(environ, start_response) mock_eio_app.handle_request.assert_called_once_with( environ, start_response ) def test_static_files(self): root_dir = os.path.dirname(__file__) m = engineio.WSGIApp( 'foo', None, static_files={ '/': root_dir + '/index.html', '/foo': { 'content_type': 'text/plain', 'filename': root_dir + '/index.html', }, '/static': root_dir, '/static/test/': root_dir + '/', '/static2/test/': {'filename': root_dir + '/', 'content_type': 'image/gif'}, }, ) def check_path(path, status_code, content_type, body): environ = {'PATH_INFO': path} start_response = mock.MagicMock() r = m(environ, start_response) assert r == [body.encode('utf-8')] start_response.assert_called_once_with( status_code, [('Content-Type', content_type)] ) check_path('/', '200 OK', 'text/html', '\n') check_path('/foo', '200 OK', 'text/plain', '\n') check_path('/foo/bar', '404 Not Found', 'text/plain', 'Not Found') check_path( '/static/index.html', '200 OK', 'text/html', '\n' ) check_path( '/static/foo.bar', '404 Not Found', 'text/plain', 'Not Found' ) check_path( '/static/test/index.html', '200 OK', 'text/html', '\n' ) check_path('/static/test/', '200 OK', 'text/html', '\n') check_path('/static/test/index.html', '200 OK', 'text/html', '\n') check_path('/static/test/files/', '200 OK', 'text/html', 'file\n') check_path('/static/test/files/file.txt', '200 OK', 'text/plain', 'file\n') check_path('/static/test/files/x.html', '404 Not Found', 'text/plain', 'Not Found') check_path('/static2/test/', '200 OK', 'image/gif', '\n') check_path('/static2/test/index.html', '200 OK', 'image/gif', '\n') check_path('/static2/test/files/', '200 OK', 'image/gif', 'file\n') check_path('/static2/test/files/file.txt', '200 OK', 'image/gif', 'file\n') check_path('/static2/test/files/x.html', '404 Not Found', 'text/plain', 'Not Found') check_path('/bar/foo', '404 Not Found', 'text/plain', 'Not Found') check_path('', '404 Not Found', 'text/plain', 'Not Found') m.static_files[''] = 'index.html' check_path('/static/test/', '200 OK', 'text/html', '\n') m.static_files[''] = {'filename': 'index.html'} check_path('/static/test/', '200 OK', 'text/html', '\n') m.static_files[''] = { 'filename': 'index.html', 'content_type': 'image/gif', } check_path('/static/test/', '200 OK', 'image/gif', '\n') m.static_files[''] = {'filename': 'test.gif'} check_path('/static/test/', '404 Not Found', 'text/plain', 'Not Found') m.static_files = {} check_path( '/static/test/index.html', '404 Not Found', 'text/plain', 'Not Found', ) def test_404(self): mock_wsgi_app = None mock_eio_app = mock.Mock() m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app) environ = {'PATH_INFO': '/foo/bar'} start_response = mock.MagicMock() r = m(environ, start_response) assert r == [b'Not Found'] start_response.assert_called_once_with( "404 Not Found", [('Content-Type', 'text/plain')] ) def test_custom_eio_path(self): mock_wsgi_app = None mock_eio_app = mock.Mock() mock_eio_app.handle_request = mock.MagicMock() m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app, engineio_path='foo') environ = {'PATH_INFO': '/engine.io/'} start_response = mock.MagicMock() r = m(environ, start_response) assert r == [b'Not Found'] start_response.assert_called_once_with( "404 Not Found", [('Content-Type', 'text/plain')] ) environ = {'PATH_INFO': '/foo/'} m(environ, start_response) mock_eio_app.handle_request.assert_called_once_with( environ, start_response ) def test_custom_eio_path_slashes(self): mock_wsgi_app = None mock_eio_app = mock.Mock() mock_eio_app.handle_request = mock.MagicMock() m = engineio.WSGIApp( mock_eio_app, mock_wsgi_app, engineio_path='/foo/' ) environ = {'PATH_INFO': '/foo/'} start_response = mock.MagicMock() m(environ, start_response) mock_eio_app.handle_request.assert_called_once_with( environ, start_response ) def test_custom_eio_path_leading_slash(self): mock_wsgi_app = None mock_eio_app = mock.Mock() mock_eio_app.handle_request = mock.MagicMock() m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app, engineio_path='/foo') environ = {'PATH_INFO': '/foo/'} start_response = mock.MagicMock() m(environ, start_response) mock_eio_app.handle_request.assert_called_once_with( environ, start_response ) def test_custom_eio_path_trailing_slash(self): mock_wsgi_app = None mock_eio_app = mock.Mock() mock_eio_app.handle_request = mock.MagicMock() m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app, engineio_path='foo/') environ = {'PATH_INFO': '/foo/'} start_response = mock.MagicMock() m(environ, start_response) mock_eio_app.handle_request.assert_called_once_with( environ, start_response ) def test_gunicorn_socket(self): mock_wsgi_app = None mock_eio_app = mock.Mock() m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app) environ = {'gunicorn.socket': 123, 'PATH_INFO': '/foo/bar'} start_response = mock.MagicMock() m(environ, start_response) assert 'eventlet.input' in environ assert environ['eventlet.input'].get_socket() == 123 def test_legacy_middleware_class(self): m = engineio.Middleware('eio', 'wsgi', 'eio_path') assert m.engineio_app == 'eio' assert m.wsgi_app == 'wsgi' assert m.static_files == {} assert m.engineio_path == '/eio_path/' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732474535.0 python_engineio-4.12.1/tests/common/test_packet.py0000664000175000017500000001173014720673247021715 0ustar00miguelmiguelimport pytest from engineio import packet class TestPacket: def test_encode_default_packet(self): pkt = packet.Packet() assert pkt.packet_type == packet.NOOP assert pkt.data is None assert not pkt.binary assert pkt.encode() == '6' def test_decode_default_packet(self): pkt = packet.Packet(encoded_packet='6') assert pkt.encode() == '6' def test_encode_text_packet(self): data = 'text' pkt = packet.Packet(packet.MESSAGE, data=data) assert pkt.packet_type == packet.MESSAGE assert pkt.data == data assert not pkt.binary assert pkt.encode() == '4text' def test_decode_text_packet(self): pkt = packet.Packet(encoded_packet=b'4text') assert pkt.encode() == b'4text' def test_encode_empty_text_packet(self): data = '' pkt = packet.Packet(packet.MESSAGE, data=data) assert pkt.packet_type == packet.MESSAGE assert pkt.data == data assert not pkt.binary assert pkt.encode() == '4' def test_decode_empty_text_packet(self): pkt = packet.Packet(encoded_packet=b'4') assert pkt.encode() == b'4' def test_encode_binary_packet(self): pkt = packet.Packet(packet.MESSAGE, data=b'\x01\x02\x03') assert pkt.packet_type == packet.MESSAGE assert pkt.data == b'\x01\x02\x03' assert pkt.binary assert pkt.encode() == b'\x01\x02\x03' def test_encode_binary_bytearray_packet(self): pkt = packet.Packet(packet.MESSAGE, data=bytearray(b'\x01\x02\x03')) assert pkt.packet_type == packet.MESSAGE assert pkt.data == b'\x01\x02\x03' assert pkt.binary assert pkt.encode() == b'\x01\x02\x03' def test_encode_binary_b64_packet(self): pkt = packet.Packet(packet.MESSAGE, data=b'\x01\x02\x03\x04') assert pkt.packet_type == packet.MESSAGE assert pkt.data == b'\x01\x02\x03\x04' assert pkt.binary assert pkt.encode(b64=True) == 'bAQIDBA==' def test_encode_empty_binary_packet(self): pkt = packet.Packet(packet.MESSAGE, data=b'') assert pkt.packet_type == packet.MESSAGE assert pkt.data == b'' assert pkt.binary assert pkt.encode() == b'' def test_decode_binary_packet(self): pkt = packet.Packet(encoded_packet=b'\x04\x01\x02\x03') assert pkt.encode() == b'\x04\x01\x02\x03' def test_decode_binary_bytearray_packet(self): pkt = packet.Packet(encoded_packet=bytearray(b'\x04\x01\x02\x03')) assert pkt.encode() == b'\x04\x01\x02\x03' def test_decode_binary_b64_packet(self): pkt = packet.Packet(encoded_packet='bBAECAw==') assert pkt.encode() == b'\x04\x01\x02\x03' def test_decode_empty_binary_packet(self): pkt = packet.Packet(encoded_packet=b'') assert pkt.encode() == b'' def test_encode_json_packet(self): pkt = packet.Packet(packet.MESSAGE, data={'a': 123, 'b': '456'}) assert pkt.packet_type == packet.MESSAGE assert pkt.data == {'a': 123, 'b': '456'} assert not pkt.binary assert pkt.encode() in [ '4{"a":123,"b":"456"}', '4{"b":"456","a":123}', ] def test_decode_json_packet(self): pkt = packet.Packet(encoded_packet='4{"a":123,"b":"456"}') assert pkt.encode() in [ '4{"a":123,"b":"456"}', '4{"b":"456","a":123}', ] def test_decode_json_packet_long_int(self): pkt = packet.Packet(encoded_packet='4{"a":' + '1' * 100 + '}') assert pkt.packet_type == packet.MESSAGE assert pkt.data == {'a': int('1' * 100)} pkt = packet.Packet(encoded_packet='4{"a":' + '1' * 101 + '}') assert pkt.packet_type == packet.MESSAGE assert pkt.data == '{"a":' + '1' * 101 + '}' def test_encode_number_packet(self): pkt = packet.Packet(packet.MESSAGE, data=123) assert pkt.packet_type == packet.MESSAGE assert pkt.data == 123 assert not pkt.binary assert pkt.encode() == '4123' def test_decode_number_packet(self): pkt = packet.Packet(encoded_packet='4123') assert pkt.packet_type == packet.MESSAGE # integer payloads are parsed as strings, see # https://github.com/miguelgrinberg/python-engineio/issues/75 # for background on this decision assert pkt.data == '123' assert not pkt.binary assert pkt.encode() == '4123' def test_binary_non_message_packet(self): with pytest.raises(ValueError): packet.Packet(packet.NOOP, b'\x01\x02\x03') def test_decode_invalid_empty_text_packet(self): with pytest.raises(ValueError): packet.Packet(encoded_packet='') def test_encode_cache(self): pkt = packet.Packet(packet.MESSAGE, data=123) assert pkt.encode() == '4123' pkt.data = 456 assert pkt.encode() == '4123' pkt.encode_cache = None assert pkt.encode() == '4456' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732474535.0 python_engineio-4.12.1/tests/common/test_payload.py0000664000175000017500000000456214720673247022104 0ustar00miguelmiguelimport pytest from engineio import packet from engineio import payload class TestPayload: def test_encode_empty_payload(self): p = payload.Payload() assert p.packets == [] assert p.encode() == '' def test_decode_empty_payload(self): p = payload.Payload(encoded_payload='') assert p.encode() == '' def test_encode_payload_text(self): pkt = packet.Packet(packet.MESSAGE, data='abc') p = payload.Payload([pkt]) assert p.packets == [pkt] assert p.encode() == '4abc' def test_encode_payload_text_multiple(self): pkt = packet.Packet(packet.MESSAGE, data='abc') pkt2 = packet.Packet(packet.MESSAGE, data='def') p = payload.Payload([pkt, pkt2]) assert p.packets == [pkt, pkt2] assert p.encode() == '4abc\x1e4def' def test_encode_payload_binary(self): pkt = packet.Packet(packet.MESSAGE, data=b'\x00\x01\x02') p = payload.Payload([pkt]) assert p.packets == [pkt] assert p.encode() == 'bAAEC' def test_encode_payload_binary_multiple(self): pkt = packet.Packet(packet.MESSAGE, data=b'\x00\x01\x02') pkt2 = packet.Packet(packet.MESSAGE, data=b'\x03\x04\x05\x06') p = payload.Payload([pkt, pkt2]) assert p.packets == [pkt, pkt2] assert p.encode() == 'bAAEC\x1ebAwQFBg==' def test_encode_payload_text_binary_multiple(self): pkt = packet.Packet(packet.MESSAGE, data='abc') pkt2 = packet.Packet(packet.MESSAGE, data=b'\x03\x04\x05\x06') p = payload.Payload([pkt, pkt2, pkt2, pkt]) assert p.packets == [pkt, pkt2, pkt2, pkt] assert p.encode() == '4abc\x1ebAwQFBg==\x1ebAwQFBg==\x1e4abc' def test_encode_jsonp_payload(self): pkt = packet.Packet(packet.MESSAGE, data='abc') p = payload.Payload([pkt]) assert p.packets == [pkt] assert p.encode(jsonp_index=233) == '___eio[233]("4abc");' def test_decode_jsonp_payload(self): p = payload.Payload(encoded_payload='d=4abc') assert p.encode() == '4abc' def test_decode_invalid_payload(self): with pytest.raises(ValueError): payload.Payload(encoded_payload='bad payload') def test_decode_multi_payload_with_too_many_packets(self): with pytest.raises(ValueError): payload.Payload(encoded_payload='4abc\x1e4def\x1e' * 9 + '6') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734451382.0 python_engineio-4.12.1/tests/common/test_server.py0000664000175000017500000013323414730320266021747 0ustar00miguelmiguelimport gzip import importlib import io import logging import sys import time from unittest import mock import zlib import pytest from engineio import exceptions from engineio import json from engineio import packet from engineio import payload from engineio import server original_import_module = importlib.import_module def _mock_import(module, *args, **kwargs): if module.startswith('engineio.'): return original_import_module(module, *args, **kwargs) return module class TestServer: _mock_async = mock.MagicMock() _mock_async._async = { 'thread': 't', 'queue': 'q', 'queue_empty': RuntimeError, 'websocket': 'w', } def _get_mock_socket(self): mock_socket = mock.MagicMock() mock_socket.closed = False mock_socket.closing = False mock_socket.upgraded = False mock_socket.session = {} return mock_socket @classmethod def setup_class(cls): server.Server._default_monitor_clients = False @classmethod def teardown_class(cls): server.Server._default_monitor_clients = True def setup_method(self): logging.getLogger('engineio').setLevel(logging.NOTSET) def teardown_method(self): # restore JSON encoder, in case a test changed it packet.Packet.json = json def test_is_asyncio_based(self): s = server.Server() assert not s.is_asyncio_based() def test_async_modes(self): s = server.Server() assert s.async_modes() == [ 'eventlet', 'gevent_uwsgi', 'gevent', 'threading', ] def test_create(self): kwargs = { 'ping_timeout': 1, 'ping_interval': 2, 'max_http_buffer_size': 3, 'allow_upgrades': False, 'http_compression': False, 'compression_threshold': 4, 'cookie': 'foo', 'cors_allowed_origins': ['foo', 'bar', 'baz'], 'cors_credentials': False, 'async_handlers': False, } s = server.Server(**kwargs) for arg in kwargs.keys(): assert getattr(s, arg) == kwargs[arg] assert s.ping_interval_grace_period == 0 def test_create_with_grace_period(self): s = server.Server(ping_interval=(1, 2)) assert s.ping_interval == 1 assert s.ping_interval_grace_period == 2 def test_create_ignores_kwargs(self): server.Server(foo='bar') # this should not raise def test_async_mode_threading(self): sys.modules['simple_websocket'] = mock.MagicMock() s = server.Server(async_mode='threading') assert s.async_mode == 'threading' from engineio.async_drivers import threading as async_threading import queue assert s._async['thread'] == async_threading.DaemonThread assert s._async['queue'] == queue.Queue assert s._async['websocket'] == async_threading.SimpleWebSocketWSGI del sys.modules['simple_websocket'] del sys.modules['engineio.async_drivers.threading'] def test_async_mode_eventlet(self): sys.modules['eventlet'] = mock.MagicMock() sys.modules['eventlet'].green = mock.MagicMock() sys.modules['eventlet.green'] = sys.modules['eventlet'].green sys.modules['eventlet.green'].threading = mock.MagicMock() sys.modules['eventlet.green.threading'] = \ sys.modules['eventlet.green'].threading sys.modules['eventlet'].websocket = mock.MagicMock() sys.modules['eventlet.websocket'] = sys.modules['eventlet'].websocket s = server.Server(async_mode='eventlet') assert s.async_mode == 'eventlet' from eventlet import queue from engineio.async_drivers import eventlet as async_eventlet assert s._async['thread'] == async_eventlet.EventletThread assert s._async['queue'] == queue.Queue assert s._async['websocket'] == async_eventlet.WebSocketWSGI del sys.modules['eventlet'] del sys.modules['eventlet.green'] del sys.modules['eventlet.green.threading'] del sys.modules['eventlet.websocket'] del sys.modules['engineio.async_drivers.eventlet'] @mock.patch('importlib.import_module', side_effect=_mock_import) def test_async_mode_gevent_uwsgi(self, import_module): sys.modules['gevent'] = mock.MagicMock() sys.modules['gevent'].queue = mock.MagicMock() sys.modules['gevent.queue'] = sys.modules['gevent'].queue sys.modules['gevent.queue'].JoinableQueue = 'foo' sys.modules['gevent.queue'].Empty = RuntimeError sys.modules['gevent.event'] = mock.MagicMock() sys.modules['gevent.event'].Event = 'bar' sys.modules['uwsgi'] = mock.MagicMock() s = server.Server(async_mode='gevent_uwsgi') assert s.async_mode == 'gevent_uwsgi' from engineio.async_drivers import gevent_uwsgi as async_gevent_uwsgi assert s._async['thread'] == async_gevent_uwsgi.Thread assert s._async['queue'] == 'foo' assert s._async['queue_empty'] == RuntimeError assert s._async['event'] == 'bar' assert s._async['websocket'] == async_gevent_uwsgi.uWSGIWebSocket del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] del sys.modules['uwsgi'] del sys.modules['engineio.async_drivers.gevent_uwsgi'] @mock.patch('importlib.import_module', side_effect=_mock_import) def test_async_mode_gevent_uwsgi_without_uwsgi(self, import_module): sys.modules['gevent'] = mock.MagicMock() sys.modules['gevent'].queue = mock.MagicMock() sys.modules['gevent.queue'] = sys.modules['gevent'].queue sys.modules['gevent.queue'].JoinableQueue = 'foo' sys.modules['gevent.queue'].Empty = RuntimeError sys.modules['gevent.event'] = mock.MagicMock() sys.modules['gevent.event'].Event = 'bar' sys.modules['uwsgi'] = None with pytest.raises(ValueError): server.Server(async_mode='gevent_uwsgi') del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] del sys.modules['uwsgi'] @mock.patch('importlib.import_module', side_effect=_mock_import) def test_async_mode_gevent_uwsgi_without_websocket(self, import_module): sys.modules['gevent'] = mock.MagicMock() sys.modules['gevent'].queue = mock.MagicMock() sys.modules['gevent.queue'] = sys.modules['gevent'].queue sys.modules['gevent.queue'].JoinableQueue = 'foo' sys.modules['gevent.queue'].Empty = RuntimeError sys.modules['gevent.event'] = mock.MagicMock() sys.modules['gevent.event'].Event = 'bar' sys.modules['uwsgi'] = mock.MagicMock() del sys.modules['uwsgi'].websocket_handshake s = server.Server(async_mode='gevent_uwsgi') assert s.async_mode == 'gevent_uwsgi' from engineio.async_drivers import gevent_uwsgi as async_gevent_uwsgi assert s._async['thread'] == async_gevent_uwsgi.Thread assert s._async['queue'] == 'foo' assert s._async['queue_empty'] == RuntimeError assert s._async['event'] == 'bar' assert s._async['websocket'] is None del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] del sys.modules['uwsgi'] del sys.modules['engineio.async_drivers.gevent_uwsgi'] @mock.patch('importlib.import_module', side_effect=_mock_import) def test_async_mode_gevent(self, import_module): sys.modules['gevent'] = mock.MagicMock() sys.modules['gevent'].queue = mock.MagicMock() sys.modules['gevent.queue'] = sys.modules['gevent'].queue sys.modules['gevent.queue'].JoinableQueue = 'foo' sys.modules['gevent.queue'].Empty = RuntimeError sys.modules['gevent.event'] = mock.MagicMock() sys.modules['gevent.event'].Event = 'bar' sys.modules['geventwebsocket'] = 'geventwebsocket' s = server.Server(async_mode='gevent') assert s.async_mode == 'gevent' from engineio.async_drivers import gevent as async_gevent assert s._async['thread'] == async_gevent.Thread assert s._async['queue'] == 'foo' assert s._async['queue_empty'] == RuntimeError assert s._async['event'] == 'bar' assert s._async['websocket'] == async_gevent.WebSocketWSGI del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] del sys.modules['geventwebsocket'] del sys.modules['engineio.async_drivers.gevent'] @mock.patch('importlib.import_module', side_effect=_mock_import) def test_async_mode_aiohttp(self, import_module): sys.modules['aiohttp'] = mock.MagicMock() with pytest.raises(ValueError): server.Server(async_mode='aiohttp') @mock.patch('importlib.import_module', side_effect=[ImportError]) def test_async_mode_invalid(self, import_module): with pytest.raises(ValueError): server.Server(async_mode='foo') @mock.patch( 'importlib.import_module', side_effect=[_mock_async], ) def test_async_mode_auto_eventlet(self, import_module): s = server.Server() assert s.async_mode == 'eventlet' @mock.patch( 'importlib.import_module', side_effect=[ImportError, _mock_async] ) def test_async_mode_auto_gevent_uwsgi(self, import_module): s = server.Server() assert s.async_mode == 'gevent_uwsgi' @mock.patch( 'importlib.import_module', side_effect=[ImportError, ImportError, _mock_async], ) def test_async_mode_auto_gevent(self, import_module): s = server.Server() assert s.async_mode == 'gevent' @mock.patch( 'importlib.import_module', side_effect=[ImportError, ImportError, ImportError, _mock_async], ) def test_async_mode_auto_threading(self, import_module): s = server.Server() assert s.async_mode == 'threading' def test_generate_id(self): s = server.Server() assert s.generate_id() != s.generate_id() def test_on_event(self): s = server.Server() @s.on('connect') def foo(): pass s.on('disconnect', foo) assert s.handlers['connect'] == foo assert s.handlers['disconnect'] == foo def test_on_event_invalid(self): s = server.Server() with pytest.raises(ValueError): s.on('invalid') def test_trigger_event(self): s = server.Server(async_mode='threading') f = {} @s.on('connect') def foo(sid, environ): return sid + environ @s.on('message') def bar(sid, data): f['bar'] = sid + data return 'bar' @s.on('disconnect') def baz(sid, reason): return sid + reason r = s._trigger_event('connect', 1, 2, run_async=False) assert r == 3 r = s._trigger_event('message', 3, 4, run_async=True) r.join() assert f['bar'] == 7 r = s._trigger_event('message', 5, 6) assert r == 'bar' r = s._trigger_event('disconnect', 'foo', 'bar') assert r == 'foobar' def test_trigger_legacy_disconnect_event(self): s = server.Server(async_mode='threading') @s.on('disconnect') def baz(sid): return sid r = s._trigger_event('disconnect', 'foo', 'bar') assert r == 'foo' def test_trigger_event_error(self): s = server.Server() @s.on('connect') def foo(sid, environ): return 1 / 0 @s.on('message') def bar(sid, data): return 1 / 0 r = s._trigger_event('connect', 1, 2, run_async=False) assert not r r = s._trigger_event('message', 3, 4, run_async=False) assert r is None def test_session(self): s = server.Server() mock_socket = self._get_mock_socket() s.sockets['foo'] = mock_socket with s.session('foo') as session: assert session == {} session['username'] = 'bar' assert s.get_session('foo') == {'username': 'bar'} def test_close_one_socket(self): s = server.Server() mock_socket = self._get_mock_socket() s.sockets['foo'] = mock_socket s.disconnect('foo') assert mock_socket.close.call_count == 1 assert 'foo' not in s.sockets def test_close_all_sockets(self): s = server.Server() mock_sockets = {} for sid in ['foo', 'bar', 'baz']: mock_sockets[sid] = self._get_mock_socket() s.sockets[sid] = mock_sockets[sid] s.disconnect() for socket in mock_sockets.values(): assert socket.close.call_count == 1 assert s.sockets == {} def test_upgrades(self): s = server.Server() s.sockets['foo'] = self._get_mock_socket() assert s._upgrades('foo', 'polling') == ['websocket'] assert s._upgrades('foo', 'websocket') == [] s.sockets['foo'].upgraded = True assert s._upgrades('foo', 'polling') == [] assert s._upgrades('foo', 'websocket') == [] s.allow_upgrades = False s.sockets['foo'].upgraded = True assert s._upgrades('foo', 'polling') == [] assert s._upgrades('foo', 'websocket') == [] def test_transport(self): s = server.Server() s.sockets['foo'] = self._get_mock_socket() s.sockets['foo'].upgraded = False s.sockets['bar'] = self._get_mock_socket() s.sockets['bar'].upgraded = True assert s.transport('foo') == 'polling' assert s.transport('bar') == 'websocket' def test_bad_session(self): s = server.Server() s.sockets['foo'] = 'client' with pytest.raises(KeyError): s._get_socket('bar') def test_closed_socket(self): s = server.Server() s.sockets['foo'] = self._get_mock_socket() s.sockets['foo'].closed = True with pytest.raises(KeyError): s._get_socket('foo') def test_jsonp_with_bad_index(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&j=abc'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_jsonp_index(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&j=233'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' assert r[0].startswith(b'___eio[233]("') assert r[0].endswith(b'");') def test_connect(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) assert len(s.sockets) == 1 assert start_response.call_count == 1 assert start_response.call_args[0][0] == '200 OK' assert ( 'Content-Type', 'text/plain; charset=UTF-8', ) in start_response.call_args[0][1] assert len(r) == 1 packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets assert len(packets) == 1 assert packets[0].packet_type == packet.OPEN assert 'upgrades' in packets[0].data assert packets[0].data['upgrades'] == ['websocket'] assert 'sid' in packets[0].data assert packets[0].data['pingTimeout'] == 20000 assert packets[0].data['pingInterval'] == 25000 assert packets[0].data['maxPayload'] == 1000000 def test_connect_no_upgrades(self): s = server.Server(allow_upgrades=False) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets assert packets[0].data['upgrades'] == [] def test_connect_bad_eio_version(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=1'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) assert start_response.call_args[0][0], '400 BAD REQUEST' assert b'unsupported version' in r[0] def test_connect_custom_ping_times(self): s = server.Server(ping_timeout=123, ping_interval=456, max_http_buffer_size=12345678) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets assert packets[0].data['pingTimeout'] == 123000 assert packets[0].data['pingInterval'] == 456000 assert packets[0].data['maxPayload'] == 12345678 @mock.patch( 'engineio.socket.Socket.poll', side_effect=exceptions.QueueEmpty ) def test_connect_bad_poll(self, poll): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' @mock.patch( 'engineio.socket.Socket', return_value=mock.MagicMock(connected=False, closed=False), ) def test_connect_transport_websocket(self, Socket): s = server.Server() s.generate_id = mock.MagicMock(return_value='123') environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=websocket', 'HTTP_UPGRADE': 'websocket', } start_response = mock.MagicMock() # force socket to stay open, so that we can check it later Socket().closed = False s.handle_request(environ, start_response) assert s.sockets['123'].send.call_args[0][0].packet_type == packet.OPEN @mock.patch( 'engineio.socket.Socket', return_value=mock.MagicMock(connected=False, closed=False), ) def test_http_upgrade_case_insensitive(self, Socket): s = server.Server() s.generate_id = mock.MagicMock(return_value='123') environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=websocket', 'HTTP_UPGRADE': 'WebSocket', } start_response = mock.MagicMock() # force socket to stay open, so that we can check it later Socket().closed = False s.handle_request(environ, start_response) assert s.sockets['123'].send.call_args[0][0].packet_type == packet.OPEN @mock.patch( 'engineio.socket.Socket', return_value=mock.MagicMock(connected=False, closed=False), ) def test_connect_transport_websocket_closed(self, Socket): s = server.Server() s.generate_id = mock.MagicMock(return_value='123') environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=websocket', 'HTTP_UPGRADE': 'websocket', } start_response = mock.MagicMock() def mock_handle(environ, start_response): s.sockets['123'].closed = True Socket().handle_get_request = mock_handle s.handle_request(environ, start_response) assert '123' not in s.sockets def test_connect_transport_invalid(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_connect_transport_websocket_without_upgrade(self): s = server.Server() environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=websocket', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_connect_cors_headers(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Credentials', 'true') in headers def test_connect_cors_allowed_origin(self): s = server.Server(cors_allowed_origins=['a', 'b']) environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ORIGIN': 'b', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'b') in headers def test_connect_cors_allowed_origin_with_callable(self): def cors(origin): return origin == 'a' s = server.Server(cors_allowed_origins=cors) environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ORIGIN': 'a', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'a') in headers environ['HTTP_ORIGIN'] = 'b' s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_connect_cors_not_allowed_origin(self): s = server.Server(cors_allowed_origins=['a', 'b']) environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ORIGIN': 'c', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'c') not in headers assert ('Access-Control-Allow-Origin', '*') not in headers def test_connect_cors_headers_all_origins(self): s = server.Server(cors_allowed_origins='*') environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ORIGIN': 'foo', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'foo') in headers assert ('Access-Control-Allow-Credentials', 'true') in headers def test_connect_cors_headers_one_origin(self): s = server.Server(cors_allowed_origins='a') environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ORIGIN': 'a', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'a') in headers assert ('Access-Control-Allow-Credentials', 'true') in headers def test_connect_cors_headers_one_origin_not_allowed(self): s = server.Server(cors_allowed_origins='a') environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ORIGIN': 'b', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'b') not in headers assert ('Access-Control-Allow-Origin', '*') not in headers def test_connect_cors_headers_default_origin(self): s = server.Server() environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'foo', 'HTTP_ORIGIN': 'http://foo', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'http://foo') in headers def test_connect_cors_headers_default_origin_proxy_server(self): s = server.Server() environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'foo', 'HTTP_ORIGIN': 'https://foo', 'HTTP_X_FORWARDED_PROTO': 'https, ftp', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'https://foo') in headers def test_connect_cors_headers_default_origin_proxy_server2(self): s = server.Server() environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'foo', 'HTTP_ORIGIN': 'https://bar', 'HTTP_X_FORWARDED_PROTO': 'https, ftp', 'HTTP_X_FORWARDED_HOST': 'bar , baz', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Origin', 'https://bar') in headers def test_connect_cors_no_credentials(self): s = server.Server(cors_credentials=False) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Credentials', 'true') not in headers def test_cors_options(self): s = server.Server() environ = {'REQUEST_METHOD': 'OPTIONS', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ( 'Access-Control-Allow-Methods', 'OPTIONS, GET, POST', ) in headers def test_cors_request_headers(self): s = server.Server() environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS': 'Foo, Bar', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] assert ('Access-Control-Allow-Headers', 'Foo, Bar') in headers def test_connect_cors_disabled(self): s = server.Server(cors_allowed_origins=[]) environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4', 'HTTP_ORIGIN': 'http://foo', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] for header in headers: assert not header[0].startswith('Access-Control-') def test_connect_cors_default_no_origin(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) headers = start_response.call_args[0][1] for header in headers: assert header[0] != 'Access-Control-Allow-Origin' def test_connect_cors_all_no_origin(self): s = server.Server(cors_allowed_origins='*') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) headers = start_response.call_args[0][1] for header in headers: assert header[0] != 'Access-Control-Allow-Origin' def test_connect_cors_disabled_no_origin(self): s = server.Server(cors_allowed_origins=[]) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) headers = start_response.call_args[0][1] for header in headers: assert header[0] != 'Access-Control-Allow-Origin' def test_connect_event(self): s = server.Server() s.generate_id = mock.MagicMock(return_value='123') mock_event = mock.MagicMock(return_value=None) s.on('connect')(mock_event) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) mock_event.assert_called_once_with('123', environ) assert len(s.sockets) == 1 def test_connect_event_rejects(self): s = server.Server() s.generate_id = mock.MagicMock(return_value='123') mock_event = mock.MagicMock(return_value=False) s.on('connect')(mock_event) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() ret = s.handle_request(environ, start_response) assert len(s.sockets) == 0 assert start_response.call_args[0][0] == '401 UNAUTHORIZED' assert ret == [b'"Unauthorized"'] def test_connect_event_rejects_with_message(self): s = server.Server() s.generate_id = mock.MagicMock(return_value='123') mock_event = mock.MagicMock(return_value='not allowed') s.on('connect')(mock_event) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() ret = s.handle_request(environ, start_response) assert len(s.sockets) == 0 assert start_response.call_args[0][0] == '401 UNAUTHORIZED' assert ret == [b'"not allowed"'] def test_method_not_found(self): s = server.Server() environ = {'REQUEST_METHOD': 'PUT', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '405 METHOD NOT FOUND' def test_get_request_with_bad_sid(self): s = server.Server() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_post_request_with_bad_sid(self): s = server.Server() environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_send(self): s = server.Server() mock_socket = self._get_mock_socket() s.sockets['foo'] = mock_socket s.send('foo', 'hello') assert mock_socket.send.call_count == 1 assert mock_socket.send.call_args[0][0].packet_type == packet.MESSAGE assert mock_socket.send.call_args[0][0].data == 'hello' def test_send_unknown_socket(self): s = server.Server() # just ensure no exceptions are raised s.send('foo', 'hello') def test_get_request(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( return_value=[packet.Packet(packet.MESSAGE, data='hello')] ) s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' assert len(r) == 1 packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets assert len(packets) == 1 assert packets[0].packet_type == packet.MESSAGE def test_get_request_custom_response(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock(side_effect=['resp']) s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() assert s.handle_request(environ, start_response) == 'resp' def test_get_request_closes_socket(self): s = server.Server() mock_socket = self._get_mock_socket() def mock_get_request(*args, **kwargs): mock_socket.closed = True return 'resp' mock_socket.handle_get_request = mock_get_request s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() assert s.handle_request(environ, start_response) == 'resp' assert 'foo' not in s.sockets def test_get_request_error(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( side_effect=[exceptions.QueueEmpty] ) s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' assert len(s.sockets) == 0 def test_get_request_bad_websocket_transport(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.upgraded = False s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=websocket&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_get_request_bad_polling_transport(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.upgraded = True s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&transport=polling&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_post_request(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_post_request = mock.MagicMock() s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '200 OK' def test_post_request_error(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_post_request = mock.MagicMock( side_effect=[exceptions.EngineIOError] ) s.sockets['foo'] = mock_socket environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' assert 'foo' not in s.sockets @staticmethod def _gzip_decompress(b): bytesio = io.BytesIO(b) with gzip.GzipFile(fileobj=bytesio, mode='r') as gz: return gz.read() def test_gzip_compression(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( return_value=[packet.Packet(packet.MESSAGE, data='hello')] ) s.sockets['foo'] = mock_socket environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo', 'HTTP_ACCEPT_ENCODING': 'gzip,deflate', } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) assert ('Content-Encoding', 'gzip') in start_response.call_args[0][1] self._gzip_decompress(r[0]) def test_deflate_compression(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( return_value=[packet.Packet(packet.MESSAGE, data='hello')] ) s.sockets['foo'] = mock_socket environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo', 'HTTP_ACCEPT_ENCODING': 'deflate;q=1,gzip', } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) assert ('Content-Encoding', 'deflate') in start_response.call_args[0][ 1 ] zlib.decompress(r[0]) def test_gzip_compression_threshold(self): s = server.Server(compression_threshold=1000) mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( return_value=[packet.Packet(packet.MESSAGE, data='hello')] ) s.sockets['foo'] = mock_socket environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo', 'HTTP_ACCEPT_ENCODING': 'gzip', } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: assert header != 'Content-Encoding' with pytest.raises(IOError): self._gzip_decompress(r[0]) def test_compression_disabled(self): s = server.Server(http_compression=False, compression_threshold=0) mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( return_value=[packet.Packet(packet.MESSAGE, data='hello')] ) s.sockets['foo'] = mock_socket environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo', 'HTTP_ACCEPT_ENCODING': 'gzip', } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: assert header != 'Content-Encoding' with pytest.raises(IOError): self._gzip_decompress(r[0]) def test_compression_unknown(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( return_value=[packet.Packet(packet.MESSAGE, data='hello')] ) s.sockets['foo'] = mock_socket environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo', 'HTTP_ACCEPT_ENCODING': 'rar', } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: assert header != 'Content-Encoding' with pytest.raises(IOError): self._gzip_decompress(r[0]) def test_compression_no_encoding(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( return_value=[packet.Packet(packet.MESSAGE, data='hello')] ) s.sockets['foo'] = mock_socket environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo', 'HTTP_ACCEPT_ENCODING': '', } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: assert header != 'Content-Encoding' with pytest.raises(IOError): self._gzip_decompress(r[0]) def test_cookie(self): s = server.Server(cookie='sid') s.generate_id = mock.MagicMock(return_value='123') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert ('Set-Cookie', 'sid=123; path=/; SameSite=Lax') \ in start_response.call_args[0][1] def test_cookie_dict(self): def get_path(): return '/a' s = server.Server(cookie={ 'name': 'test', 'path': get_path, 'SameSite': 'None', 'Secure': True, 'HttpOnly': True }) s.generate_id = mock.MagicMock(return_value='123') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert ('Set-Cookie', 'test=123; path=/a; SameSite=None; Secure; ' 'HttpOnly') in start_response.call_args[0][1] def test_no_cookie(self): s = server.Server(cookie=None) s.generate_id = mock.MagicMock(return_value='123') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: assert header != 'Set-Cookie' def test_logger(self): s = server.Server(logger=False) assert s.logger.getEffectiveLevel() == logging.ERROR s.logger.setLevel(logging.NOTSET) s = server.Server(logger=True) assert s.logger.getEffectiveLevel() == logging.INFO s.logger.setLevel(logging.WARNING) s = server.Server(logger=True) assert s.logger.getEffectiveLevel() == logging.WARNING s.logger.setLevel(logging.NOTSET) my_logger = logging.Logger('foo') s = server.Server(logger=my_logger) assert s.logger == my_logger def test_custom_json(self): # Warning: this test cannot run in parallel with other tests, as it # changes the JSON encoding/decoding functions class CustomJSON: @staticmethod def dumps(*args, **kwargs): return '*** encoded ***' @staticmethod def loads(*args, **kwargs): return '+++ decoded +++' server.Server(json=CustomJSON) pkt = packet.Packet(packet.MESSAGE, data={'foo': 'bar'}) assert pkt.encode() == '4*** encoded ***' pkt2 = packet.Packet(encoded_packet=pkt.encode()) assert pkt2.data == '+++ decoded +++' # restore the default JSON module packet.Packet.json = json def test_background_tasks(self): flag = {} def bg_task(): flag['task'] = True s = server.Server(async_mode='threading') task = s.start_background_task(bg_task) task.join() assert 'task' in flag assert flag['task'] def test_sleep(self): s = server.Server() t = time.time() s.sleep(0.1) assert time.time() - t > 0.1 def test_create_queue(self): s = server.Server() q = s.create_queue() empty = s.get_queue_empty_exception() with pytest.raises(empty): q.get(timeout=0.01) def test_create_event(self): s = server.Server() e = s.create_event() assert not e.is_set() e.set() assert e.is_set() def test_log_error_once(self): s = server.Server(logger=mock.MagicMock()) s._log_error_once('foo', 'foo-key') s._log_error_once('foo', 'foo-key') s.logger.error.assert_called_with( 'foo (further occurrences of this error will be logged with ' 'level INFO)') s.logger.info.assert_called_with('foo') def test_service_task_started(self): s = server.Server(async_mode='threading', monitor_clients=True) s._service_task = mock.MagicMock() environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) for _ in range(3): if s._service_task.call_count > 0: break time.sleep(0.05) s._service_task.assert_called_once_with() def test_shutdown(self): s = server.Server(async_mode='threading', monitor_clients=True) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) assert s.service_task_handle is not None s.shutdown() assert s.service_task_handle is None def test_transports_invalid(self): with pytest.raises(ValueError): server.Server(transports='invalid') with pytest.raises(ValueError): server.Server(transports=['invalid', 'foo']) def test_transports_disallowed(self): s = server.Server(transports='websocket') environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=polling', } start_response = mock.MagicMock() s.handle_request(environ, start_response) assert start_response.call_args[0][0] == '400 BAD REQUEST' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735499564.0 python_engineio-4.12.1/tests/common/test_socket.py0000664000175000017500000004517614734317454021751 0ustar00miguelmiguelimport io import time from unittest import mock import pytest from engineio import exceptions from engineio import packet from engineio import payload from engineio import socket class TestSocket: def setup_method(self): self.bg_tasks = [] def _get_mock_server(self): mock_server = mock.Mock() mock_server.ping_timeout = 0.2 mock_server.ping_interval = 0.2 mock_server.ping_interval_grace_period = 0.001 mock_server.async_handlers = True mock_server.max_http_buffer_size = 128 try: import queue except ImportError: import Queue as queue import threading mock_server._async = { 'threading': threading.Thread, 'queue': queue.Queue, 'websocket': None, } def bg_task(target, *args, **kwargs): th = threading.Thread(target=target, args=args, kwargs=kwargs) self.bg_tasks.append(th) th.start() return th def create_queue(*args, **kwargs): return queue.Queue(*args, **kwargs) mock_server.start_background_task = bg_task mock_server.create_queue = create_queue mock_server.get_queue_empty_exception.return_value = queue.Empty return mock_server def _join_bg_tasks(self): for task in self.bg_tasks: task.join() def test_create(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') assert s.server == mock_server assert s.sid == 'sid' assert not s.upgraded assert not s.closed assert hasattr(s.queue, 'get') assert hasattr(s.queue, 'put') assert hasattr(s.queue, 'task_done') assert hasattr(s.queue, 'join') def test_empty_poll(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') with pytest.raises(exceptions.QueueEmpty): s.poll() def test_poll(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') s.send(pkt1) s.send(pkt2) assert s.poll() == [pkt1, pkt2] def test_poll_none(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.queue.put(None) assert s.poll() == [] def test_poll_none_after_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') pkt = packet.Packet(packet.MESSAGE, data='hello') s.send(pkt) s.queue.put(None) assert s.poll() == [pkt] assert s.poll() == [] def test_schedule_ping(self): mock_server = self._get_mock_server() mock_server.ping_interval = 0.01 s = socket.Socket(mock_server, 'sid') s.send = mock.MagicMock() s.schedule_ping() time.sleep(0.05) assert s.last_ping is not None assert s.send.call_args_list[0][0][0].encode() == '2' def test_schedule_ping_closed_socket(self): mock_server = self._get_mock_server() mock_server.ping_interval = 0.01 s = socket.Socket(mock_server, 'sid') s.send = mock.MagicMock() s.closed = True s.schedule_ping() time.sleep(0.05) assert s.last_ping is None s.send.assert_not_called() def test_pong(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.schedule_ping = mock.MagicMock() s.receive(packet.Packet(packet.PONG)) s.schedule_ping.assert_called_once_with() def test_message_async_handler(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.receive(packet.Packet(packet.MESSAGE, data='foo')) mock_server._trigger_event.assert_called_once_with( 'message', 'sid', 'foo', run_async=True ) def test_message_sync_handler(self): mock_server = self._get_mock_server() mock_server.async_handlers = False s = socket.Socket(mock_server, 'sid') s.receive(packet.Packet(packet.MESSAGE, data='foo')) mock_server._trigger_event.assert_called_once_with( 'message', 'sid', 'foo', run_async=False ) def test_invalid_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') with pytest.raises(exceptions.UnknownPacketError): s.receive(packet.Packet(packet.OPEN)) def test_timeout(self): mock_server = self._get_mock_server() mock_server.ping_interval = 6 mock_server.ping_interval_grace_period = 2 s = socket.Socket(mock_server, 'sid') s.last_ping = time.time() - 9 s.close = mock.MagicMock() s.send('packet') s.close.assert_called_once_with(wait=False, abort=False, reason=mock_server.reason.PING_TIMEOUT) def test_polling_read(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'foo') pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') s.send(pkt1) s.send(pkt2) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} start_response = mock.MagicMock() packets = s.handle_get_request(environ, start_response) assert packets == [pkt1, pkt2] def test_polling_read_error(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'foo') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} start_response = mock.MagicMock() with pytest.raises(exceptions.QueueEmpty): s.handle_get_request(environ, start_response) def test_polling_write(self): mock_server = self._get_mock_server() mock_server.max_http_buffer_size = 1000 pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') s = socket.Socket(mock_server, 'foo') s.receive = mock.MagicMock() environ = { 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', 'CONTENT_LENGTH': len(p), 'wsgi.input': io.BytesIO(p), } s.handle_post_request(environ) assert s.receive.call_count == 2 def test_polling_write_too_large(self): mock_server = self._get_mock_server() pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') mock_server.max_http_buffer_size = len(p) - 1 s = socket.Socket(mock_server, 'foo') s.receive = mock.MagicMock() environ = { 'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', 'CONTENT_LENGTH': len(p), 'wsgi.input': io.BytesIO(p), } with pytest.raises(exceptions.ContentTooLongError): s.handle_post_request(environ) def test_upgrade_handshake(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'foo') s._upgrade_websocket = mock.MagicMock() environ = { 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', 'HTTP_CONNECTION': 'Foo,Upgrade,Bar', 'HTTP_UPGRADE': 'websocket', } start_response = mock.MagicMock() s.handle_get_request(environ, start_response) s._upgrade_websocket.assert_called_once_with(environ, start_response) def test_upgrade(self): mock_server = self._get_mock_server() mock_server._async['websocket'] = mock.MagicMock() mock_ws = mock.MagicMock() mock_server._async['websocket'].return_value = mock_ws s = socket.Socket(mock_server, 'sid') s.connected = True environ = "foo" start_response = "bar" s._upgrade_websocket(environ, start_response) mock_server._async['websocket'].assert_called_once_with( s._websocket_handler, mock_server ) mock_ws.assert_called_once_with(environ, start_response) def test_upgrade_twice(self): mock_server = self._get_mock_server() mock_server._async['websocket'] = mock.MagicMock() s = socket.Socket(mock_server, 'sid') s.connected = True s.upgraded = True environ = "foo" start_response = "bar" with pytest.raises(IOError): s._upgrade_websocket(environ, start_response) def test_upgrade_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.receive(packet.Packet(packet.UPGRADE)) r = s.poll() assert len(r) == 1 assert r[0].encode() == packet.Packet(packet.NOOP).encode() def test_upgrade_no_probe(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True ws = mock.MagicMock() ws.wait.return_value = packet.Packet(packet.NOOP).encode() s._websocket_handler(ws) assert not s.upgraded def test_upgrade_no_upgrade_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) ws = mock.MagicMock() probe = 'probe' ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.NOOP).encode(), ] s._websocket_handler(ws) ws.send.assert_called_once_with( packet.Packet(packet.PONG, data=probe).encode() ) assert s.queue.get().packet_type == packet.NOOP assert not s.upgraded def test_close_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.close = mock.MagicMock() s.receive(packet.Packet(packet.CLOSE)) s.close.assert_called_once_with( wait=False, abort=True, reason=mock_server.reason.CLIENT_DISCONNECT) def test_invalid_packet_type(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') pkt = packet.Packet(packet_type=99) with pytest.raises(exceptions.UnknownPacketError): s.receive(pkt) def test_upgrade_not_supported(self): mock_server = self._get_mock_server() mock_server._async['websocket'] = None s = socket.Socket(mock_server, 'sid') s.connected = True environ = "foo" start_response = "bar" s._upgrade_websocket(environ, start_response) mock_server._bad_request.assert_called_once_with() def test_websocket_read_write(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = False s.queue.join = mock.MagicMock(return_value=None) foo = 'foo' bar = 'bar' s.poll = mock.MagicMock( side_effect=[ [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty, ] ) ws = mock.MagicMock() ws.wait.side_effect = [ packet.Packet(packet.MESSAGE, data=foo).encode(), None, ] s._websocket_handler(ws) self._join_bg_tasks() assert s.connected assert s.upgraded assert mock_server._trigger_event.call_count == 2 mock_server._trigger_event.assert_has_calls( [ mock.call('message', 'sid', 'foo', run_async=True), mock.call('disconnect', 'sid', mock_server.reason.TRANSPORT_CLOSE, run_async=False) ] ) ws.send.assert_called_with('4bar') def test_websocket_upgrade_read_write(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) foo = 'foo' bar = 'bar' probe = 'probe' s.poll = mock.MagicMock( side_effect=[ [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty, ] ) ws = mock.MagicMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE).encode(), packet.Packet(packet.MESSAGE, data=foo).encode(), None, ] s._websocket_handler(ws) self._join_bg_tasks() assert s.upgraded assert mock_server._trigger_event.call_count == 2 mock_server._trigger_event.assert_has_calls( [ mock.call('message', 'sid', 'foo', run_async=True), mock.call('disconnect', 'sid', mock_server.reason.TRANSPORT_CLOSE, run_async=False) ] ) ws.send.assert_called_with('4bar') def test_websocket_upgrade_with_payload(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) probe = 'probe' ws = mock.MagicMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE, data='2').encode(), ] s._websocket_handler(ws) self._join_bg_tasks() assert s.upgraded def test_websocket_upgrade_with_backlog(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) probe = 'probe' foo = 'foo' ws = mock.MagicMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE, data='2').encode(), ] s.upgrading = True s.send(packet.Packet(packet.MESSAGE, data=foo)) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=sid'} start_response = mock.MagicMock() packets = s.handle_get_request(environ, start_response) assert len(packets) == 1 assert packets[0].encode() == '6' packets = s.poll() assert len(packets) == 1 assert packets[0].encode() == '4foo' s._websocket_handler(ws) self._join_bg_tasks() assert s.upgraded assert not s.upgrading packets = s.handle_get_request(environ, start_response) assert len(packets) == 1 assert packets[0].encode() == '6' def test_websocket_read_write_wait_fail(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = False s.queue.join = mock.MagicMock(return_value=None) foo = 'foo' bar = 'bar' s.poll = mock.MagicMock( side_effect=[ [packet.Packet(packet.MESSAGE, data=bar)], [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty, ] ) ws = mock.MagicMock() ws.wait.side_effect = [ packet.Packet(packet.MESSAGE, data=foo).encode(), RuntimeError, ] ws.send.side_effect = [None, RuntimeError] s._websocket_handler(ws) self._join_bg_tasks() assert s.closed def test_websocket_upgrade_with_large_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) probe = 'probe' ws = mock.MagicMock() ws.wait.side_effect = [ packet.Packet(packet.PING, data=probe).encode(), packet.Packet(packet.UPGRADE, data='2' * 128).encode(), ] with pytest.raises(ValueError): s._websocket_handler(ws) self._join_bg_tasks() assert not s.upgraded def test_websocket_ignore_invalid_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = False s.queue.join = mock.MagicMock(return_value=None) foo = 'foo' bar = 'bar' s.poll = mock.MagicMock( side_effect=[ [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty, ] ) ws = mock.MagicMock() ws.wait.side_effect = [ packet.Packet(packet.OPEN).encode(), packet.Packet(packet.MESSAGE, data=foo).encode(), None, ] s._websocket_handler(ws) self._join_bg_tasks() assert s.connected assert mock_server._trigger_event.call_count == 2 mock_server._trigger_event.assert_has_calls( [ mock.call('message', 'sid', foo, run_async=True), mock.call('disconnect', 'sid', mock_server.reason.TRANSPORT_CLOSE, run_async=False) ] ) ws.send.assert_called_with('4bar') def test_send_after_close(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.close(wait=False) with pytest.raises(exceptions.SocketIsClosedError): s.send(packet.Packet(packet.NOOP)) def test_close_after_close(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.close(wait=False) assert s.closed assert mock_server._trigger_event.call_count == 1 mock_server._trigger_event.assert_called_once_with( 'disconnect', 'sid', mock_server.reason.SERVER_DISCONNECT, run_async=False ) s.close() assert mock_server._trigger_event.call_count == 1 def test_close_and_wait(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.queue = mock.MagicMock() s.close(wait=True) s.queue.join.assert_called_once_with() def test_close_without_wait(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.queue = mock.MagicMock() s.close(wait=False) assert s.queue.join.call_count == 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1746977702.3653085 python_engineio-4.12.1/tests/performance/0000775000175000017500000000000015010141646020026 5ustar00miguelmiguel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/performance/README.md0000664000175000017500000000016314707424343021317 0ustar00miguelmiguelPerformance =========== This directory contains several scripts and tools to test the performance of the project. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/performance/binary_b64_packet.py0000664000175000017500000000064714707424343023707 0ustar00miguelmiguelimport time from engineio import packet def test(): p = packet.Packet(packet.MESSAGE, b'hello world') start = time.time() count = 0 while True: p = packet.Packet(encoded_packet=p.encode(b64=True)) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('binary_b64_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/performance/binary_packet.py0000664000175000017500000000063314707424343023227 0ustar00miguelmiguelimport time from engineio import packet def test(): p = packet.Packet(packet.MESSAGE, b'hello world') start = time.time() count = 0 while True: p = packet.Packet(encoded_packet=p.encode()) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('binary_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/performance/json_packet.py0000664000175000017500000000063514707424343022716 0ustar00miguelmiguelimport time from engineio import packet def test(): p = packet.Packet(packet.MESSAGE, {'hello': 'world'}) start = time.time() count = 0 while True: p = packet.Packet(encoded_packet=p.encode()) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('json_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/performance/payload.py0000664000175000017500000000071314707424343022044 0ustar00miguelmiguelimport time from engineio import packet, payload def test(): p = payload.Payload( packets=[packet.Packet(packet.MESSAGE, b'hello world')] * 10) start = time.time() count = 0 while True: p = payload.Payload(encoded_payload=p.encode()) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('payload:', count, 'payloads processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697326317.0 python_engineio-4.12.1/tests/performance/run.sh0000775000175000017500000000022714512622355021201 0ustar00miguelmiguel#!/bin/bash python text_packet.py python binary_packet.py python binary_b64_packet.py python json_packet.py python payload.py python server_receive.py ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/performance/server_receive.py0000664000175000017500000000170214707424343023422 0ustar00miguelmiguelimport io import sys import time import engineio def test(eio_version, payload): s = engineio.Server() start = time.time() count = 0 s.handle_request({ 'REQUEST_METHOD': 'GET', 'QUERY_STRING': eio_version, }, lambda s, h: None) sid = list(s.sockets.keys())[0] while True: environ = { 'REQUEST_METHOD': 'POST', 'QUERY_STRING': eio_version + '&sid=' + sid, 'CONTENT_LENGTH': '6', 'wsgi.input': io.BytesIO(payload) } s.handle_request(environ, lambda s, h: None) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': eio_version = 'EIO=4' payload = b'4hello' if len(sys.argv) > 1 and sys.argv[1] == '3': eio_version = 'EIO=3' payload = b'\x00\x06\xff4hello' count = test(eio_version, payload) print('server_receive:', count, 'packets received.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730029795.0 python_engineio-4.12.1/tests/performance/text_packet.py0000664000175000017500000000063014707424343022724 0ustar00miguelmiguelimport time from engineio import packet def test(): p = packet.Packet(packet.MESSAGE, 'hello world') start = time.time() count = 0 while True: p = packet.Packet(encoded_packet=p.encode()) count += 1 if time.time() - start >= 5: break return count if __name__ == '__main__': count = test() print('text_packet:', count, 'packets processed.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732474535.0 python_engineio-4.12.1/tox.ini0000664000175000017500000000131714720673247015716 0ustar00miguelmiguel[tox] envlist=flake8,py38,py39,py310,py311,py312,py313,pypy3,docs skip_missing_interpreters=True [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 3.13: py313 pypy-3: pypy3 [testenv] commands= pip install -e . pytest -p no:logging --cov=engineio --cov-branch --cov-report=term-missing --cov-report=xml deps= pytest pytest-cov pytest-asyncio aiohttp tornado requests websocket-client [testenv:pypy3] [testenv:flake8] deps= flake8 commands= flake8 --exclude=".*" --ignore=W503,E402,E722 src/engineio tests examples [testenv:docs] changedir=docs deps= sphinx allowlist_externals= make commands= make html