├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── SECURITY.md ├── compliance ├── README.rst ├── asyncio │ ├── client.py │ └── server.py ├── config │ ├── fuzzingclient.json │ └── fuzzingserver.json └── sync │ ├── client.py │ └── server.py ├── docs ├── Makefile ├── _static │ ├── favicon.ico │ ├── tidelift.png │ └── websockets.svg ├── conf.py ├── deploy │ ├── architecture.svg │ ├── fly.rst │ ├── haproxy.rst │ ├── heroku.rst │ ├── index.rst │ ├── koyeb.rst │ ├── kubernetes.rst │ ├── nginx.rst │ ├── render.rst │ └── supervisor.rst ├── faq │ ├── asyncio.rst │ ├── client.rst │ ├── common.rst │ ├── index.rst │ ├── misc.rst │ └── server.rst ├── howto │ ├── autoreload.rst │ ├── debugging.rst │ ├── django.rst │ ├── encryption.rst │ ├── extensions.rst │ ├── index.rst │ ├── patterns.rst │ ├── sansio.rst │ └── upgrade.rst ├── index.rst ├── intro │ ├── examples.rst │ ├── index.rst │ ├── tutorial1.rst │ ├── tutorial2.rst │ └── tutorial3.rst ├── make.bat ├── project │ ├── changelog.rst │ ├── contributing.rst │ ├── index.rst │ ├── license.rst │ ├── sponsoring.rst │ ├── support.rst │ └── tidelift.rst ├── reference │ ├── asyncio │ │ ├── client.rst │ │ ├── common.rst │ │ └── server.rst │ ├── datastructures.rst │ ├── exceptions.rst │ ├── extensions.rst │ ├── features.rst │ ├── index.rst │ ├── legacy │ │ ├── client.rst │ │ ├── common.rst │ │ └── server.rst │ ├── sansio │ │ ├── client.rst │ │ ├── common.rst │ │ └── server.rst │ ├── sync │ │ ├── client.rst │ │ ├── common.rst │ │ └── server.rst │ ├── types.rst │ └── variables.rst ├── requirements.txt ├── spelling_wordlist.txt └── topics │ ├── authentication.rst │ ├── authentication.svg │ ├── broadcast.rst │ ├── compression.rst │ ├── data-flow.svg │ ├── design.rst │ ├── index.rst │ ├── keepalive.rst │ ├── lifecycle.graffle │ ├── lifecycle.svg │ ├── logging.rst │ ├── memory.rst │ ├── performance.rst │ ├── protocol.graffle │ ├── protocol.svg │ ├── proxies.rst │ ├── routing.rst │ └── security.rst ├── example ├── asyncio │ ├── client.py │ ├── echo.py │ ├── hello.py │ └── server.py ├── deployment │ ├── fly │ │ ├── Procfile │ │ ├── app.py │ │ ├── fly.toml │ │ └── requirements.txt │ ├── haproxy │ │ ├── app.py │ │ ├── haproxy.cfg │ │ └── supervisord.conf │ ├── heroku │ │ ├── Procfile │ │ ├── app.py │ │ └── requirements.txt │ ├── koyeb │ │ ├── Procfile │ │ ├── app.py │ │ └── requirements.txt │ ├── kubernetes │ │ ├── Dockerfile │ │ ├── app.py │ │ ├── benchmark.py │ │ └── deployment.yaml │ ├── nginx │ │ ├── app.py │ │ ├── nginx.conf │ │ └── supervisord.conf │ ├── render │ │ ├── app.py │ │ └── requirements.txt │ └── supervisor │ │ ├── app.py │ │ └── supervisord.conf ├── django │ ├── authentication.py │ ├── notifications.py │ └── signals.py ├── faq │ ├── health_check_server.py │ ├── shutdown_client.py │ └── shutdown_server.py ├── legacy │ ├── basic_auth_client.py │ ├── basic_auth_server.py │ ├── unix_client.py │ └── unix_server.py ├── quick │ ├── client.py │ ├── counter.css │ ├── counter.html │ ├── counter.js │ ├── counter.py │ ├── server.py │ ├── show_time.html │ ├── show_time.js │ ├── show_time.py │ └── sync_time.py ├── ruff.toml ├── sync │ ├── client.py │ ├── echo.py │ ├── hello.py │ └── server.py ├── tls │ ├── client.py │ ├── localhost.pem │ └── server.py └── tutorial │ ├── start │ ├── connect4.css │ ├── connect4.js │ ├── connect4.py │ └── favicon.ico │ ├── step1 │ ├── app.py │ ├── connect4.css │ ├── connect4.js │ ├── connect4.py │ ├── favicon.ico │ ├── index.html │ └── main.js │ ├── step2 │ ├── app.py │ ├── connect4.css │ ├── connect4.js │ ├── connect4.py │ ├── favicon.ico │ ├── index.html │ └── main.js │ └── step3 │ ├── Procfile │ ├── app.py │ ├── connect4.css │ ├── connect4.js │ ├── connect4.py │ ├── favicon.ico │ ├── index.html │ ├── main.js │ └── requirements.txt ├── experiments ├── authentication │ ├── app.py │ ├── cookie.html │ ├── cookie.js │ ├── cookie_iframe.html │ ├── cookie_iframe.js │ ├── favicon.ico │ ├── first_message.html │ ├── first_message.js │ ├── index.html │ ├── query_param.html │ ├── query_param.js │ ├── script.js │ ├── style.css │ ├── test.html │ ├── test.js │ ├── user_info.html │ └── user_info.js ├── broadcast │ ├── clients.py │ └── server.py ├── compression │ ├── benchmark.py │ ├── client.py │ ├── corpus.py │ └── server.py ├── json_log_formatter.py ├── optimization │ ├── parse_frames.py │ ├── parse_handshake.py │ └── streams.py ├── profiling │ └── compression.py └── routing.py ├── fuzzing ├── fuzz_http11_request_parser.py ├── fuzz_http11_response_parser.py └── fuzz_websocket_parser.py ├── logo ├── favicon.ico ├── github-social-preview.html ├── github-social-preview.png ├── horizontal.svg ├── icon.html ├── icon.svg ├── old.svg ├── tidelift.png └── vertical.svg ├── pyproject.toml ├── setup.py ├── src └── websockets │ ├── __init__.py │ ├── __main__.py │ ├── asyncio │ ├── __init__.py │ ├── async_timeout.py │ ├── client.py │ ├── compatibility.py │ ├── connection.py │ ├── messages.py │ ├── router.py │ └── server.py │ ├── auth.py │ ├── cli.py │ ├── client.py │ ├── connection.py │ ├── datastructures.py │ ├── exceptions.py │ ├── extensions │ ├── __init__.py │ ├── base.py │ └── permessage_deflate.py │ ├── frames.py │ ├── headers.py │ ├── http.py │ ├── http11.py │ ├── imports.py │ ├── legacy │ ├── __init__.py │ ├── auth.py │ ├── client.py │ ├── exceptions.py │ ├── framing.py │ ├── handshake.py │ ├── http.py │ ├── protocol.py │ └── server.py │ ├── protocol.py │ ├── py.typed │ ├── server.py │ ├── speedups.c │ ├── speedups.pyi │ ├── streams.py │ ├── sync │ ├── __init__.py │ ├── client.py │ ├── connection.py │ ├── messages.py │ ├── router.py │ ├── server.py │ └── utils.py │ ├── typing.py │ ├── uri.py │ ├── utils.py │ └── version.py ├── tests ├── __init__.py ├── asyncio │ ├── __init__.py │ ├── connection.py │ ├── server.py │ ├── test_client.py │ ├── test_connection.py │ ├── test_messages.py │ ├── test_router.py │ ├── test_server.py │ └── utils.py ├── extensions │ ├── __init__.py │ ├── test_base.py │ ├── test_permessage_deflate.py │ └── utils.py ├── legacy │ ├── __init__.py │ ├── test_auth.py │ ├── test_client_server.py │ ├── test_exceptions.py │ ├── test_framing.py │ ├── test_handshake.py │ ├── test_http.py │ ├── test_protocol.py │ └── utils.py ├── maxi_cov.py ├── protocol.py ├── proxy.py ├── requirements.txt ├── sync │ ├── __init__.py │ ├── connection.py │ ├── server.py │ ├── test_client.py │ ├── test_connection.py │ ├── test_messages.py │ ├── test_router.py │ ├── test_server.py │ ├── test_utils.py │ └── utils.py ├── test_auth.py ├── test_cli.py ├── test_client.py ├── test_connection.py ├── test_datastructures.py ├── test_exceptions.py ├── test_exports.py ├── test_frames.py ├── test_headers.py ├── test_http.py ├── test_http11.py ├── test_imports.py ├── test_localhost.cnf ├── test_localhost.pem ├── test_protocol.py ├── test_server.py ├── test_streams.py ├── test_uri.py ├── test_utils.py └── utils.py └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: python-websockets 2 | open_collective: websockets 3 | tidelift: pypi/websockets 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report an issue 3 | about: Let us know about a problem with websockets 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "saturday" 8 | time: "07:00" 9 | timezone: "Europe/Paris" 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Make release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | sdist: 11 | name: Build source distribution and architecture-independent wheel 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out repository 15 | uses: actions/checkout@v4 16 | - name: Install Python 3.x 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.x 20 | - name: Install build 21 | run: pip install build 22 | - name: Build sdist & wheel 23 | run: python -m build 24 | env: 25 | BUILD_EXTENSION: no 26 | - name: Save sdist & wheel 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: dist-architecture-independent 30 | path: | 31 | dist/*.tar.gz 32 | dist/*.whl 33 | 34 | wheels: 35 | name: Build architecture-specific wheels on ${{ matrix.os }} 36 | runs-on: ${{ matrix.os }} 37 | strategy: 38 | matrix: 39 | os: 40 | - ubuntu-latest 41 | - windows-latest 42 | - macOS-latest 43 | steps: 44 | - name: Check out repository 45 | uses: actions/checkout@v4 46 | - name: Install Python 3.x 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: 3.x 50 | - name: Set up QEMU 51 | if: runner.os == 'Linux' 52 | uses: docker/setup-qemu-action@v3 53 | with: 54 | platforms: all 55 | - name: Build wheels 56 | uses: pypa/cibuildwheel@v2.22.0 57 | env: 58 | BUILD_EXTENSION: yes 59 | - name: Save wheels 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: dist-${{ matrix.os }} 63 | path: wheelhouse/*.whl 64 | 65 | upload: 66 | name: Upload 67 | needs: 68 | - sdist 69 | - wheels 70 | runs-on: ubuntu-latest 71 | # Don't release when running the workflow manually from GitHub's UI. 72 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 73 | permissions: 74 | id-token: write 75 | attestations: write 76 | contents: write 77 | steps: 78 | - name: Download artifacts 79 | uses: actions/download-artifact@v4 80 | with: 81 | pattern: dist-* 82 | merge-multiple: true 83 | path: dist 84 | - name: Attest provenance 85 | uses: actions/attest-build-provenance@v2 86 | with: 87 | subject-path: dist/* 88 | - name: Upload to PyPI 89 | uses: pypa/gh-action-pypi-publish@release/v1 90 | - name: Create GitHub release 91 | env: 92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 93 | run: gh release -R python-websockets/websockets create ${{ github.ref_name }} --notes "See https://websockets.readthedocs.io/en/stable/project/changelog.html for details." 94 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | WEBSOCKETS_TESTS_TIMEOUT_FACTOR: 10 13 | 14 | jobs: 15 | coverage: 16 | name: Run test coverage checks 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out repository 20 | uses: actions/checkout@v4 21 | - name: Install Python 3.x 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.x" 25 | - name: Install tox 26 | run: pip install tox 27 | - name: Run tests with coverage 28 | run: tox -e coverage 29 | - name: Run tests with per-module coverage 30 | run: tox -e maxi_cov 31 | 32 | quality: 33 | name: Run code quality checks 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Check out repository 37 | uses: actions/checkout@v4 38 | - name: Install Python 3.x 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: "3.x" 42 | - name: Install tox 43 | run: pip install tox 44 | - name: Check code formatting & style 45 | run: tox -e ruff 46 | - name: Check types statically 47 | run: tox -e mypy 48 | 49 | matrix: 50 | name: Run tests on Python ${{ matrix.python }} 51 | needs: 52 | - coverage 53 | - quality 54 | runs-on: ubuntu-latest 55 | strategy: 56 | matrix: 57 | python: 58 | - "3.9" 59 | - "3.10" 60 | - "3.11" 61 | - "3.12" 62 | - "3.13" 63 | - "pypy-3.10" 64 | is_main: 65 | - ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 66 | exclude: 67 | - python: "pypy-3.10" 68 | is_main: false 69 | steps: 70 | - name: Check out repository 71 | uses: actions/checkout@v4 72 | - name: Install Python ${{ matrix.python }} 73 | uses: actions/setup-python@v5 74 | with: 75 | python-version: ${{ matrix.python }} 76 | allow-prereleases: true 77 | - name: Install tox 78 | run: pip install tox 79 | - name: Run tests 80 | run: tox -e py 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.so 3 | .coverage 4 | .direnv/ 5 | .envrc 6 | .idea/ 7 | .mypy_cache/ 8 | .tox/ 9 | .vscode/ 10 | build/ 11 | compliance/reports/ 12 | dist/ 13 | docs/_build/ 14 | experiments/compression/corpus/ 15 | htmlcov/ 16 | src/websockets.egg-info/ 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-lts-latest 5 | tools: 6 | python: latest 7 | jobs: 8 | post_checkout: 9 | - git fetch --unshallow 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Aymeric Augustin and contributors 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | * Neither the name of the copyright holder nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include src/websockets/py.typed 3 | include src/websockets/speedups.c # required when BUILD_EXTENSION=no 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default style types tests coverage maxi_cov build clean 2 | 3 | export PYTHONASYNCIODEBUG=1 4 | export PYTHONPATH=src 5 | export PYTHONWARNINGS=default 6 | 7 | build: 8 | python setup.py build_ext --inplace 9 | 10 | style: 11 | ruff format compliance src tests 12 | ruff check --fix compliance src tests 13 | 14 | types: 15 | mypy --strict src 16 | 17 | tests: 18 | python -m unittest 19 | 20 | coverage: 21 | coverage run --source src/websockets,tests -m unittest 22 | coverage html 23 | coverage report --show-missing --fail-under=100 24 | 25 | maxi_cov: 26 | python tests/maxi_cov.py 27 | coverage html 28 | coverage report --show-missing --fail-under=100 29 | 30 | clean: 31 | find src -name '*.so' -delete 32 | find . -name '*.pyc' -delete 33 | find . -name __pycache__ -delete 34 | rm -rf .coverage .mypy_cache build compliance/reports dist docs/_build htmlcov MANIFEST src/websockets.egg-info 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Policy 4 | 5 | Only the latest version receives security updates. 6 | 7 | ## Contact information 8 | 9 | Please report security vulnerabilities to the 10 | [Tidelift security team](https://tidelift.com/security). 11 | 12 | Tidelift will coordinate the fix and disclosure. 13 | -------------------------------------------------------------------------------- /compliance/README.rst: -------------------------------------------------------------------------------- 1 | Autobahn Testsuite 2 | ================== 3 | 4 | General information and installation instructions are available at 5 | https://github.com/crossbario/autobahn-testsuite. 6 | 7 | Running the test suite 8 | ---------------------- 9 | 10 | All commands below must be run from the root directory of the repository. 11 | 12 | To get acceptable performance, compile the C extension first: 13 | 14 | .. code-block:: console 15 | 16 | $ python setup.py build_ext --inplace 17 | 18 | Run each command in a different shell. Testing takes several minutes to complete 19 | — wstest is the bottleneck. When clients finish, stop servers with Ctrl-C. 20 | 21 | You can exclude slow tests by modifying the configuration files as follows:: 22 | 23 | "exclude-cases": ["9.*", "12.*", "13.*"] 24 | 25 | The test server and client applications shouldn't display any exceptions. 26 | 27 | To test the servers: 28 | 29 | .. code-block:: console 30 | 31 | $ PYTHONPATH=src python compliance/asyncio/server.py 32 | $ PYTHONPATH=src python compliance/sync/server.py 33 | 34 | $ docker run --interactive --tty --rm \ 35 | --volume "${PWD}/compliance/config:/config" \ 36 | --volume "${PWD}/compliance/reports:/reports" \ 37 | --name fuzzingclient \ 38 | crossbario/autobahn-testsuite \ 39 | wstest --mode fuzzingclient --spec /config/fuzzingclient.json 40 | 41 | $ open compliance/reports/servers/index.html 42 | 43 | To test the clients: 44 | 45 | .. code-block:: console 46 | $ docker run --interactive --tty --rm \ 47 | --volume "${PWD}/compliance/config:/config" \ 48 | --volume "${PWD}/compliance/reports:/reports" \ 49 | --publish 9001:9001 \ 50 | --name fuzzingserver \ 51 | crossbario/autobahn-testsuite \ 52 | wstest --mode fuzzingserver --spec /config/fuzzingserver.json 53 | 54 | $ PYTHONPATH=src python compliance/asyncio/client.py 55 | $ PYTHONPATH=src python compliance/sync/client.py 56 | 57 | $ open compliance/reports/clients/index.html 58 | 59 | Conformance notes 60 | ----------------- 61 | 62 | Some test cases are more strict than the RFC. Given the implementation of the 63 | library and the test client and server applications, websockets passes with a 64 | "Non-Strict" result in these cases. 65 | 66 | In 3.2, 3.3, 4.1.3, 4.1.4, 4.2.3, 4.2.4, and 5.15 websockets notices the 67 | protocol error and closes the connection at the library level before the 68 | application gets a chance to echo the previous frame. 69 | 70 | In 6.4.1, 6.4.2, 6.4.3, and 6.4.4, even though it uses an incremental decoder, 71 | websockets doesn't notice the invalid utf-8 fast enough to get a "Strict" pass. 72 | These tests are more strict than the RFC. 73 | 74 | Test case 7.1.5 fails because websockets treats closing the connection in the 75 | middle of a fragmented message as a protocol error. As a consequence, it sends 76 | a close frame with code 1002. The test suite expects a close frame with code 77 | 1000, echoing the close code that it sent. This isn't required. RFC 6455 states 78 | that "the endpoint typically echos the status code it received", which leaves 79 | the possibility to send a close frame with a different status code. 80 | -------------------------------------------------------------------------------- /compliance/asyncio/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | 5 | from websockets.asyncio.client import connect 6 | from websockets.exceptions import WebSocketException 7 | 8 | 9 | logging.basicConfig(level=logging.WARNING) 10 | 11 | SERVER = "ws://localhost:9001" 12 | 13 | AGENT = "websockets.asyncio" 14 | 15 | 16 | async def get_case_count(): 17 | async with connect(f"{SERVER}/getCaseCount") as ws: 18 | return json.loads(await ws.recv()) 19 | 20 | 21 | async def run_case(case): 22 | async with connect( 23 | f"{SERVER}/runCase?case={case}&agent={AGENT}", 24 | max_size=2**25, 25 | ) as ws: 26 | try: 27 | async for msg in ws: 28 | await ws.send(msg) 29 | except WebSocketException: 30 | pass 31 | 32 | 33 | async def update_reports(): 34 | async with connect( 35 | f"{SERVER}/updateReports?agent={AGENT}", 36 | open_timeout=60, 37 | ): 38 | pass 39 | 40 | 41 | async def main(): 42 | cases = await get_case_count() 43 | for case in range(1, cases + 1): 44 | print(f"Running test case {case:03d} / {cases}... ", end="\t") 45 | try: 46 | await run_case(case) 47 | except WebSocketException as exc: 48 | print(f"ERROR: {type(exc).__name__}: {exc}") 49 | except Exception as exc: 50 | print(f"FAIL: {type(exc).__name__}: {exc}") 51 | else: 52 | print("OK") 53 | print(f"Ran {cases} test cases") 54 | await update_reports() 55 | print("Updated reports") 56 | 57 | 58 | if __name__ == "__main__": 59 | asyncio.run(main()) 60 | -------------------------------------------------------------------------------- /compliance/asyncio/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from websockets.asyncio.server import serve 5 | from websockets.exceptions import WebSocketException 6 | 7 | 8 | logging.basicConfig(level=logging.WARNING) 9 | 10 | HOST, PORT = "0.0.0.0", 9002 11 | 12 | 13 | async def echo(ws): 14 | try: 15 | async for msg in ws: 16 | await ws.send(msg) 17 | except WebSocketException: 18 | pass 19 | 20 | 21 | async def main(): 22 | async with serve( 23 | echo, 24 | HOST, 25 | PORT, 26 | server_header="websockets.sync", 27 | max_size=2**25, 28 | ) as server: 29 | try: 30 | await server.serve_forever() 31 | except KeyboardInterrupt: 32 | pass 33 | 34 | 35 | if __name__ == "__main__": 36 | asyncio.run(main()) 37 | -------------------------------------------------------------------------------- /compliance/config/fuzzingclient.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "servers": [{ 4 | "url": "ws://host.docker.internal:9002" 5 | }, { 6 | "url": "ws://host.docker.internal:9003" 7 | }], 8 | "outdir": "/reports/servers", 9 | "cases": ["*"], 10 | "exclude-cases": [] 11 | } 12 | -------------------------------------------------------------------------------- /compliance/config/fuzzingserver.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "url": "ws://localhost:9001", 4 | "outdir": "/reports/clients", 5 | "cases": ["*"], 6 | "exclude-cases": [] 7 | } 8 | -------------------------------------------------------------------------------- /compliance/sync/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from websockets.exceptions import WebSocketException 5 | from websockets.sync.client import connect 6 | 7 | 8 | logging.basicConfig(level=logging.WARNING) 9 | 10 | SERVER = "ws://localhost:9001" 11 | 12 | AGENT = "websockets.sync" 13 | 14 | 15 | def get_case_count(): 16 | with connect(f"{SERVER}/getCaseCount") as ws: 17 | return json.loads(ws.recv()) 18 | 19 | 20 | def run_case(case): 21 | with connect( 22 | f"{SERVER}/runCase?case={case}&agent={AGENT}", 23 | max_size=2**25, 24 | ) as ws: 25 | try: 26 | for msg in ws: 27 | ws.send(msg) 28 | except WebSocketException: 29 | pass 30 | 31 | 32 | def update_reports(): 33 | with connect( 34 | f"{SERVER}/updateReports?agent={AGENT}", 35 | open_timeout=60, 36 | ): 37 | pass 38 | 39 | 40 | def main(): 41 | cases = get_case_count() 42 | for case in range(1, cases + 1): 43 | print(f"Running test case {case:03d} / {cases}... ", end="\t") 44 | try: 45 | run_case(case) 46 | except WebSocketException as exc: 47 | print(f"ERROR: {type(exc).__name__}: {exc}") 48 | except Exception as exc: 49 | print(f"FAIL: {type(exc).__name__}: {exc}") 50 | else: 51 | print("OK") 52 | print(f"Ran {cases} test cases") 53 | update_reports() 54 | print("Updated reports") 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /compliance/sync/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from websockets.exceptions import WebSocketException 4 | from websockets.sync.server import serve 5 | 6 | 7 | logging.basicConfig(level=logging.WARNING) 8 | 9 | HOST, PORT = "0.0.0.0", 9003 10 | 11 | 12 | def echo(ws): 13 | try: 14 | for msg in ws: 15 | ws.send(msg) 16 | except WebSocketException: 17 | pass 18 | 19 | 20 | def main(): 21 | with serve( 22 | echo, 23 | HOST, 24 | PORT, 25 | server_header="websockets.asyncio", 26 | max_size=2**25, 27 | ) as server: 28 | try: 29 | server.serve_forever() 30 | except KeyboardInterrupt: 31 | pass 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | livehtml: 23 | sphinx-autobuild --watch "$(SOURCEDIR)/../src" "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- 1 | ../../logo/favicon.ico -------------------------------------------------------------------------------- /docs/_static/tidelift.png: -------------------------------------------------------------------------------- 1 | ../../logo/tidelift.png -------------------------------------------------------------------------------- /docs/_static/websockets.svg: -------------------------------------------------------------------------------- 1 | ../../logo/vertical.svg -------------------------------------------------------------------------------- /docs/deploy/haproxy.rst: -------------------------------------------------------------------------------- 1 | Deploy behind HAProxy 2 | ===================== 3 | 4 | This guide demonstrates a way to load balance connections across multiple 5 | websockets server processes running on the same machine with HAProxy_. 6 | 7 | We'll run server processes with Supervisor as described in :doc:`this guide 8 | `. 9 | 10 | .. _HAProxy: https://www.haproxy.org/ 11 | 12 | Run server processes 13 | -------------------- 14 | 15 | Save this app to ``app.py``: 16 | 17 | .. literalinclude:: ../../example/deployment/haproxy/app.py 18 | :language: python 19 | 20 | Each server process listens on a different port by extracting an incremental 21 | index from an environment variable set by Supervisor. 22 | 23 | Save this configuration to ``supervisord.conf``: 24 | 25 | .. literalinclude:: ../../example/deployment/haproxy/supervisord.conf 26 | 27 | This configuration runs four instances of the app. 28 | 29 | Install Supervisor and run it: 30 | 31 | .. code-block:: console 32 | 33 | $ supervisord -c supervisord.conf -n 34 | 35 | Configure and run HAProxy 36 | ------------------------- 37 | 38 | Here's a simple HAProxy configuration to load balance connections across four 39 | processes: 40 | 41 | .. literalinclude:: ../../example/deployment/haproxy/haproxy.cfg 42 | 43 | In the backend configuration, we set the load balancing method to 44 | ``leastconn`` in order to balance the number of active connections across 45 | servers. This is best for long running connections. 46 | 47 | Save the configuration to ``haproxy.cfg``, install HAProxy, and run it: 48 | 49 | .. code-block:: console 50 | 51 | $ haproxy -f haproxy.cfg 52 | 53 | You can confirm that HAProxy proxies connections properly: 54 | 55 | .. code-block:: console 56 | 57 | $ websockets ws://localhost:8080/ 58 | Connected to ws://localhost:8080/. 59 | > Hello! 60 | < Hello! 61 | Connection closed: 1000 (OK). 62 | -------------------------------------------------------------------------------- /docs/deploy/nginx.rst: -------------------------------------------------------------------------------- 1 | Deploy behind nginx 2 | =================== 3 | 4 | This guide demonstrates a way to load balance connections across multiple 5 | websockets server processes running on the same machine with nginx_. 6 | 7 | We'll run server processes with Supervisor as described in :doc:`this guide 8 | `. 9 | 10 | .. _nginx: https://nginx.org/ 11 | 12 | Run server processes 13 | -------------------- 14 | 15 | Save this app to ``app.py``: 16 | 17 | .. literalinclude:: ../../example/deployment/nginx/app.py 18 | :language: python 19 | 20 | We'd like nginx to connect to websockets servers via Unix sockets in order to 21 | avoid the overhead of TCP for communicating between processes running in the 22 | same OS. 23 | 24 | We start the app with :func:`~websockets.asyncio.server.unix_serve`. Each server 25 | process listens on a different socket thanks to an environment variable set by 26 | Supervisor to a different value. 27 | 28 | Save this configuration to ``supervisord.conf``: 29 | 30 | .. literalinclude:: ../../example/deployment/nginx/supervisord.conf 31 | 32 | This configuration runs four instances of the app. 33 | 34 | Install Supervisor and run it: 35 | 36 | .. code-block:: console 37 | 38 | $ supervisord -c supervisord.conf -n 39 | 40 | Configure and run nginx 41 | ----------------------- 42 | 43 | Here's a simple nginx configuration to load balance connections across four 44 | processes: 45 | 46 | .. literalinclude:: ../../example/deployment/nginx/nginx.conf 47 | 48 | We set ``daemon off`` so we can run nginx in the foreground for testing. 49 | 50 | Then we combine the `WebSocket proxying`_ and `load balancing`_ guides: 51 | 52 | * The WebSocket protocol requires HTTP/1.1. We must set the HTTP protocol 53 | version to 1.1, else nginx defaults to HTTP/1.0 for proxying. 54 | 55 | * The WebSocket handshake involves the ``Connection`` and ``Upgrade`` HTTP 56 | headers. We must pass them to the upstream explicitly, else nginx drops 57 | them because they're hop-by-hop headers. 58 | 59 | We deviate from the `WebSocket proxying`_ guide because its example adds a 60 | ``Connection: Upgrade`` header to every upstream request, even if the 61 | original request didn't contain that header. 62 | 63 | * In the upstream configuration, we set the load balancing method to 64 | ``least_conn`` in order to balance the number of active connections across 65 | servers. This is best for long running connections. 66 | 67 | .. _WebSocket proxying: http://nginx.org/en/docs/http/websocket.html 68 | .. _load balancing: http://nginx.org/en/docs/http/load_balancing.html 69 | 70 | Save the configuration to ``nginx.conf``, install nginx, and run it: 71 | 72 | .. code-block:: console 73 | 74 | $ nginx -c nginx.conf -p . 75 | 76 | You can confirm that nginx proxies connections properly: 77 | 78 | .. code-block:: console 79 | 80 | $ websockets ws://localhost:8080/ 81 | Connected to ws://localhost:8080/. 82 | > Hello! 83 | < Hello! 84 | Connection closed: 1000 (OK). 85 | -------------------------------------------------------------------------------- /docs/faq/asyncio.rst: -------------------------------------------------------------------------------- 1 | Using asyncio 2 | ============= 3 | 4 | .. currentmodule:: websockets.asyncio.connection 5 | 6 | .. admonition:: This FAQ is written for the new :mod:`asyncio` implementation. 7 | :class: tip 8 | 9 | Answers are also valid for the legacy :mod:`asyncio` implementation. 10 | 11 | How do I run two coroutines in parallel? 12 | ---------------------------------------- 13 | 14 | You must start two tasks, which the event loop will run concurrently. You can 15 | achieve this with :func:`asyncio.gather` or :func:`asyncio.create_task`. 16 | 17 | Keep track of the tasks and make sure that they terminate or that you cancel 18 | them when the connection terminates. 19 | 20 | Why does my program never receive any messages? 21 | ----------------------------------------------- 22 | 23 | Your program runs a coroutine that never yields control to the event loop. The 24 | coroutine that receives messages never gets a chance to run. 25 | 26 | Putting an ``await`` statement in a ``for`` or a ``while`` loop isn't enough 27 | to yield control. Awaiting a coroutine may yield control, but there's no 28 | guarantee that it will. 29 | 30 | For example, :meth:`~Connection.send` only yields control when send buffers are 31 | full, which never happens in most practical cases. 32 | 33 | If you run a loop that contains only synchronous operations and a 34 | :meth:`~Connection.send` call, you must yield control explicitly with 35 | :func:`asyncio.sleep`:: 36 | 37 | async def producer(websocket): 38 | message = generate_next_message() 39 | await websocket.send(message) 40 | await asyncio.sleep(0) # yield control to the event loop 41 | 42 | :func:`asyncio.sleep` always suspends the current task, allowing other tasks 43 | to run. This behavior is documented precisely because it isn't expected from 44 | every coroutine. 45 | 46 | See `issue 867`_. 47 | 48 | .. _issue 867: https://github.com/python-websockets/websockets/issues/867 49 | 50 | Why am I having problems with threads? 51 | -------------------------------------- 52 | 53 | If you choose websockets' :mod:`asyncio` implementation, then you shouldn't use 54 | threads. Indeed, choosing :mod:`asyncio` to handle concurrency is mutually 55 | exclusive with :mod:`threading`. 56 | 57 | If you believe that you need to run websockets in a thread and some logic in 58 | another thread, you should run that logic in a :class:`~asyncio.Task` instead. 59 | 60 | If it has to run in another thread because it would block the event loop, 61 | :func:`~asyncio.to_thread` or :meth:`~asyncio.loop.run_in_executor` is the way 62 | to go. 63 | 64 | Please review the advice about :ref:`asyncio-multithreading` in the Python 65 | documentation. 66 | 67 | Why does my simple program misbehave mysteriously? 68 | -------------------------------------------------- 69 | 70 | You are using :func:`time.sleep` instead of :func:`asyncio.sleep`, which 71 | blocks the event loop and prevents asyncio from operating normally. 72 | 73 | This may lead to messages getting send but not received, to connection timeouts, 74 | and to unexpected results of shotgun debugging e.g. adding an unnecessary call 75 | to a coroutine makes the program functional. 76 | -------------------------------------------------------------------------------- /docs/faq/index.rst: -------------------------------------------------------------------------------- 1 | Frequently asked questions 2 | ========================== 3 | 4 | .. currentmodule:: websockets 5 | 6 | .. admonition:: Many questions asked in websockets' issue tracker are really 7 | about :mod:`asyncio`. 8 | :class: seealso 9 | 10 | If you're new to ``asyncio``, you will certainly encounter issues that are 11 | related to asynchronous programming in general rather than to websockets in 12 | particular. 13 | 14 | Fortunately, Python's official documentation provides advice to `develop 15 | with asyncio`_. Check it out: it's invaluable! 16 | 17 | .. _develop with asyncio: https://docs.python.org/3/library/asyncio-dev.html 18 | 19 | .. toctree:: 20 | 21 | server 22 | client 23 | common 24 | asyncio 25 | misc 26 | -------------------------------------------------------------------------------- /docs/faq/misc.rst: -------------------------------------------------------------------------------- 1 | Miscellaneous 2 | ============= 3 | 4 | .. currentmodule:: websockets 5 | 6 | .. Remove this question when dropping Python < 3.13, which provides natively 7 | .. a good error message in this case. 8 | 9 | Why do I get the error: ``module 'websockets' has no attribute '...'``? 10 | ....................................................................... 11 | 12 | Often, this is because you created a script called ``websockets.py`` in your 13 | current working directory. Then ``import websockets`` imports this module 14 | instead of the websockets library. 15 | 16 | Why is websockets slower than another library in my benchmark? 17 | .............................................................. 18 | 19 | Not all libraries are as feature-complete as websockets. For a fair benchmark, 20 | you should disable features that the other library doesn't provide. Typically, 21 | you must disable: 22 | 23 | * Compression: set ``compression=None`` 24 | * Keepalive: set ``ping_interval=None`` 25 | * Limits: set ``max_size=None`` 26 | * UTF-8 decoding: send ``bytes`` rather than ``str`` 27 | 28 | Then, please consider whether websockets is the bottleneck of the performance 29 | of your application. Usually, in real-world applications, CPU time spent in 30 | websockets is negligible compared to time spent in the application logic. 31 | 32 | Are there ``onopen``, ``onmessage``, ``onerror``, and ``onclose`` callbacks? 33 | ............................................................................ 34 | 35 | No, there aren't. 36 | 37 | websockets provides high-level, coroutine-based APIs. Compared to callbacks, 38 | coroutines make it easier to manage control flow in concurrent code. 39 | 40 | If you prefer callback-based APIs, you should use another library. 41 | -------------------------------------------------------------------------------- /docs/howto/autoreload.rst: -------------------------------------------------------------------------------- 1 | Reload on code changes 2 | ====================== 3 | 4 | When developing a websockets server, you are likely to run it locally to test 5 | changes. Unfortunately, whenever you want to try a new version of the code, you 6 | must stop the server and restart it, which slows down your development process. 7 | 8 | Web frameworks such as Django or Flask provide a development server that reloads 9 | the application automatically when you make code changes. There is no equivalent 10 | functionality in websockets because it's designed only for production. 11 | 12 | However, you can achieve the same result easily with a third-party library and a 13 | shell command. 14 | 15 | Install watchdog_ with the ``watchmedo`` shell utility: 16 | 17 | .. code-block:: console 18 | 19 | $ pip install 'watchdog[watchmedo]' 20 | 21 | .. _watchdog: https://pypi.org/project/watchdog/ 22 | 23 | Run your server with ``watchmedo auto-restart``: 24 | 25 | .. code-block:: console 26 | 27 | $ watchmedo auto-restart --pattern "*.py" --recursive --signal SIGTERM \ 28 | python app.py 29 | 30 | This example assumes that the server is defined in a script called ``app.py`` 31 | and exits cleanly when receiving the ``SIGTERM`` signal. Adapt as necessary. 32 | -------------------------------------------------------------------------------- /docs/howto/debugging.rst: -------------------------------------------------------------------------------- 1 | Enable debug logs 2 | ================== 3 | 4 | websockets logs events with the :mod:`logging` module from the standard library. 5 | 6 | It emits logs in the ``"websockets.server"`` and ``"websockets.client"`` 7 | loggers. 8 | 9 | You can enable logs at the ``DEBUG`` level to see exactly what websockets does. 10 | 11 | If logging isn't configured in your application:: 12 | 13 | import logging 14 | 15 | logging.basicConfig( 16 | format="%(asctime)s %(message)s", 17 | level=logging.DEBUG, 18 | ) 19 | 20 | If logging is already configured:: 21 | 22 | import logging 23 | 24 | logger = logging.getLogger("websockets") 25 | logger.setLevel(logging.DEBUG) 26 | logger.addHandler(logging.StreamHandler()) 27 | 28 | Refer to the :doc:`logging guide <../topics/logging>` for more information about 29 | logging in websockets. 30 | 31 | You may also enable asyncio's `debug mode`_ to get warnings about classic 32 | pitfalls. 33 | 34 | .. _debug mode: https://docs.python.org/3/library/asyncio-dev.html#asyncio-debug-mode 35 | -------------------------------------------------------------------------------- /docs/howto/encryption.rst: -------------------------------------------------------------------------------- 1 | Encrypt connections 2 | ==================== 3 | 4 | .. currentmodule:: websockets 5 | 6 | You should always secure WebSocket connections with TLS_ (Transport Layer 7 | Security). 8 | 9 | .. admonition:: TLS vs. SSL 10 | :class: tip 11 | 12 | TLS is sometimes referred to as SSL (Secure Sockets Layer). SSL was an 13 | earlier encryption protocol; the name stuck. 14 | 15 | The ``wss`` protocol is to ``ws`` what ``https`` is to ``http``. 16 | 17 | Secure WebSocket connections require certificates just like HTTPS. 18 | 19 | .. _TLS: https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security 20 | 21 | .. admonition:: Configure the TLS context securely 22 | :class: attention 23 | 24 | The examples below demonstrate the ``ssl`` argument with a TLS certificate 25 | shared between the client and the server. This is a simplistic setup. 26 | 27 | Please review the advice and security considerations in the documentation of 28 | the :mod:`ssl` module to configure the TLS context appropriately. 29 | 30 | Servers 31 | ------- 32 | 33 | In a typical :doc:`deployment <../deploy/index>`, the server is behind a reverse 34 | proxy that terminates TLS. The client connects to the reverse proxy with TLS and 35 | the reverse proxy connects to the server without TLS. 36 | 37 | In that case, you don't need to configure TLS in websockets. 38 | 39 | If needed in your setup, you can terminate TLS in the server. 40 | 41 | In the example below, :func:`~asyncio.server.serve` is configured to receive 42 | secure connections. Before running this server, download 43 | :download:`localhost.pem <../../example/tls/localhost.pem>` and save it in the 44 | same directory as ``server.py``. 45 | 46 | .. literalinclude:: ../../example/tls/server.py 47 | :caption: server.py 48 | 49 | Receive both plain and TLS connections on the same port isn't supported. 50 | 51 | Clients 52 | ------- 53 | 54 | :func:`~asyncio.client.connect` enables TLS automatically when connecting to a 55 | ``wss://...`` URI. 56 | 57 | This works out of the box when the TLS certificate of the server is valid, 58 | meaning it's signed by a certificate authority that your Python installation 59 | trusts. 60 | 61 | In the example above, since the server uses a self-signed certificate, the 62 | client needs to be configured to trust the certificate. Here's how to do so. 63 | 64 | .. literalinclude:: ../../example/tls/client.py 65 | :caption: client.py 66 | -------------------------------------------------------------------------------- /docs/howto/extensions.rst: -------------------------------------------------------------------------------- 1 | Write an extension 2 | ================== 3 | 4 | .. currentmodule:: websockets 5 | 6 | During the opening handshake, WebSocket clients and servers negotiate which 7 | extensions_ will be used and with which parameters. 8 | 9 | .. _extensions: https://datatracker.ietf.org/doc/html/rfc6455.html#section-9 10 | 11 | Then, each frame is processed before being sent and after being received 12 | according to the extensions that were negotiated. 13 | 14 | Writing an extension requires implementing at least two classes, an extension 15 | factory and an extension. They inherit from base classes provided by websockets. 16 | 17 | Extension factory 18 | ----------------- 19 | 20 | An extension factory negotiates parameters and instantiates the extension. 21 | 22 | Clients and servers require separate extension factories with distinct APIs. 23 | Base classes are :class:`~extensions.ClientExtensionFactory` and 24 | :class:`~extensions.ServerExtensionFactory`. 25 | 26 | Extension factories are the public API of an extension. Extensions are enabled 27 | with the ``extensions`` parameter of :func:`~asyncio.client.connect` or 28 | :func:`~asyncio.server.serve`. 29 | 30 | Extension 31 | --------- 32 | 33 | An extension decodes incoming frames and encodes outgoing frames. 34 | 35 | If the extension is symmetrical, clients and servers can use the same class. The 36 | base class is :class:`~extensions.Extension`. 37 | 38 | Since extensions are initialized by extension factories, they don't need to be 39 | part of the public API of an extension. 40 | -------------------------------------------------------------------------------- /docs/howto/index.rst: -------------------------------------------------------------------------------- 1 | How-to guides 2 | ============= 3 | 4 | Set up your development environment comfortably. 5 | 6 | .. toctree:: 7 | 8 | autoreload 9 | debugging 10 | 11 | Configure websockets securely in production. 12 | 13 | .. toctree:: 14 | 15 | encryption 16 | 17 | These guides will help you design and build your application. 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | patterns 23 | django 24 | 25 | Upgrading from the legacy :mod:`asyncio` implementation to the new one? 26 | Read this. 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | 31 | upgrade 32 | 33 | If you're integrating the Sans-I/O layer of websockets into a library, rather 34 | than building an application with websockets, follow this guide. 35 | 36 | .. toctree:: 37 | :maxdepth: 2 38 | 39 | sansio 40 | 41 | The WebSocket protocol makes provisions for extending or specializing its 42 | features, which websockets supports fully. 43 | 44 | .. toctree:: 45 | 46 | extensions 47 | -------------------------------------------------------------------------------- /docs/intro/index.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | .. currentmodule:: websockets 5 | 6 | Requirements 7 | ------------ 8 | 9 | websockets requires Python ≥ 3.9. 10 | 11 | .. admonition:: Use the most recent Python release 12 | :class: tip 13 | 14 | For each minor version (3.x), only the latest bugfix or security release 15 | (3.x.y) is officially supported. 16 | 17 | It doesn't have any dependencies. 18 | 19 | .. _install: 20 | 21 | Installation 22 | ------------ 23 | 24 | Install websockets with: 25 | 26 | .. code-block:: console 27 | 28 | $ pip install websockets 29 | 30 | Wheels are available for all platforms. 31 | 32 | Tutorial 33 | -------- 34 | 35 | Learn how to build an real-time web application with websockets. 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | 40 | tutorial1 41 | tutorial2 42 | tutorial3 43 | 44 | In a hurry? 45 | ----------- 46 | 47 | These examples will get you started quickly with websockets. 48 | 49 | .. toctree:: 50 | :maxdepth: 2 51 | 52 | examples 53 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/project/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thanks for taking the time to contribute to websockets! 5 | 6 | Code of Conduct 7 | --------------- 8 | 9 | This project and everyone participating in it is governed by the `Code of 10 | Conduct`_. By participating, you are expected to uphold this code. Please 11 | report inappropriate behavior to aymeric DOT augustin AT fractalideas DOT com. 12 | 13 | .. _Code of Conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md 14 | 15 | *(If I'm the person with the inappropriate behavior, please accept my 16 | apologies. I know I can mess up. I can't expect you to tell me, but if you 17 | choose to do so, I'll do my best to handle criticism constructively. 18 | -- Aymeric)* 19 | 20 | Contributing 21 | ------------ 22 | 23 | Bug reports, patches and suggestions are welcome! 24 | 25 | Please open an issue_ or send a `pull request`_. 26 | 27 | Feedback about the documentation is especially valuable, as the primary author 28 | feels more confident about writing code than writing docs :-) 29 | 30 | If you're wondering why things are done in a certain way, the :doc:`design 31 | document <../topics/design>` provides lots of details about the internals of 32 | websockets. 33 | 34 | .. _issue: https://github.com/python-websockets/websockets/issues/new 35 | .. _pull request: https://github.com/python-websockets/websockets/compare/ 36 | 37 | Packaging 38 | --------- 39 | 40 | Some distributions package websockets so that it can be installed with the 41 | system package manager rather than with pip, possibly in a virtualenv. 42 | 43 | If you're packaging websockets for a distribution, you must use `releases 44 | published on PyPI`_ as input. You may check `SLSA attestations on GitHub`_. 45 | 46 | .. _releases published on PyPI: https://pypi.org/project/websockets/#files 47 | .. _SLSA attestations on GitHub: https://github.com/python-websockets/websockets/attestations 48 | 49 | You mustn't rely on the git repository as input. Specifically, you mustn't 50 | attempt to run the main test suite. It isn't treated as a deliverable of the 51 | project. It doesn't do what you think it does. It's designed for the needs of 52 | developers, not packagers. 53 | 54 | On a typical build farm for a distribution, tests that exercise timeouts will 55 | fail randomly. Indeed, the test suite is optimized for running very fast, with a 56 | tolerable level of flakiness, on a high-end laptop without noisy neighbors. This 57 | isn't your context. 58 | -------------------------------------------------------------------------------- /docs/project/index.rst: -------------------------------------------------------------------------------- 1 | About websockets 2 | ================ 3 | 4 | This is about websockets-the-project rather than websockets-the-software. 5 | 6 | .. toctree:: 7 | :titlesonly: 8 | 9 | changelog 10 | contributing 11 | sponsoring 12 | For enterprise 13 | support 14 | license 15 | -------------------------------------------------------------------------------- /docs/project/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. include:: ../../LICENSE 5 | -------------------------------------------------------------------------------- /docs/project/sponsoring.rst: -------------------------------------------------------------------------------- 1 | Sponsoring 2 | ========== 3 | 4 | You may sponsor the development of websockets through: 5 | 6 | * `GitHub Sponsors`_ 7 | * `Open Collective`_ 8 | * :doc:`Tidelift ` 9 | 10 | .. _GitHub Sponsors: https://github.com/sponsors/python-websockets 11 | .. _Open Collective: https://opencollective.com/websockets 12 | -------------------------------------------------------------------------------- /docs/project/support.rst: -------------------------------------------------------------------------------- 1 | Getting support 2 | =============== 3 | 4 | .. admonition:: There are no free support channels. 5 | :class: tip 6 | 7 | websockets is an open-source project. It's primarily maintained by one 8 | person as a hobby. 9 | 10 | For this reason, the focus is on flawless code and self-service 11 | documentation, not support. 12 | 13 | Enterprise 14 | ---------- 15 | 16 | websockets is maintained with high standards, making it suitable for enterprise 17 | use cases. Additional guarantees are available via :doc:`Tidelift `. 18 | If you're using it in a professional setting, consider subscribing. 19 | 20 | Questions 21 | --------- 22 | 23 | GitHub issues aren't a good medium for handling questions. There are better 24 | places to ask questions, for example Stack Overflow. 25 | 26 | If you want to ask a question anyway, please make sure that: 27 | 28 | - it's a question about websockets and not about :mod:`asyncio`; 29 | - it isn't answered in the documentation; 30 | - it wasn't asked already. 31 | 32 | A good question can be written as a suggestion to improve the documentation. 33 | 34 | Cryptocurrency users 35 | -------------------- 36 | 37 | websockets appears to be quite popular for interfacing with Bitcoin or other 38 | cryptocurrency trackers. I'm strongly opposed to Bitcoin's carbon footprint. 39 | 40 | I'm aware of efforts to build proof-of-stake models. I'll care once the total 41 | energy consumption of all cryptocurrencies drops to a non-bullshit level. 42 | 43 | You already negated all of humanity's efforts to develop renewable energy. 44 | Please stop heating the planet where my children will have to live. 45 | 46 | Since websockets is released under an open-source license, you can use it for 47 | any purpose you like. However, I won't spend any of my time to help you. 48 | 49 | I will summarily close issues related to cryptocurrency in any way. 50 | -------------------------------------------------------------------------------- /docs/reference/asyncio/client.rst: -------------------------------------------------------------------------------- 1 | Client (:mod:`asyncio`) 2 | ======================= 3 | 4 | .. automodule:: websockets.asyncio.client 5 | 6 | Opening a connection 7 | -------------------- 8 | 9 | .. autofunction:: connect 10 | :async: 11 | 12 | .. autofunction:: unix_connect 13 | :async: 14 | 15 | .. autofunction:: process_exception 16 | 17 | Using a connection 18 | ------------------ 19 | 20 | .. autoclass:: ClientConnection 21 | 22 | .. automethod:: __aiter__ 23 | 24 | .. automethod:: recv 25 | 26 | .. automethod:: recv_streaming 27 | 28 | .. automethod:: send 29 | 30 | .. automethod:: close 31 | 32 | .. automethod:: wait_closed 33 | 34 | .. automethod:: ping 35 | 36 | .. automethod:: pong 37 | 38 | WebSocket connection objects also provide these attributes: 39 | 40 | .. autoattribute:: id 41 | 42 | .. autoattribute:: logger 43 | 44 | .. autoproperty:: local_address 45 | 46 | .. autoproperty:: remote_address 47 | 48 | .. autoattribute:: latency 49 | 50 | .. autoproperty:: state 51 | 52 | The following attributes are available after the opening handshake, 53 | once the WebSocket connection is open: 54 | 55 | .. autoattribute:: request 56 | 57 | .. autoattribute:: response 58 | 59 | .. autoproperty:: subprotocol 60 | 61 | The following attributes are available after the closing handshake, 62 | once the WebSocket connection is closed: 63 | 64 | .. autoproperty:: close_code 65 | 66 | .. autoproperty:: close_reason 67 | -------------------------------------------------------------------------------- /docs/reference/asyncio/common.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Both sides (:mod:`asyncio`) 4 | =========================== 5 | 6 | .. automodule:: websockets.asyncio.connection 7 | 8 | .. autoclass:: Connection 9 | 10 | .. automethod:: __aiter__ 11 | 12 | .. automethod:: recv 13 | 14 | .. automethod:: recv_streaming 15 | 16 | .. automethod:: send 17 | 18 | .. automethod:: close 19 | 20 | .. automethod:: wait_closed 21 | 22 | .. automethod:: ping 23 | 24 | .. automethod:: pong 25 | 26 | WebSocket connection objects also provide these attributes: 27 | 28 | .. autoattribute:: id 29 | 30 | .. autoattribute:: logger 31 | 32 | .. autoproperty:: local_address 33 | 34 | .. autoproperty:: remote_address 35 | 36 | .. autoattribute:: latency 37 | 38 | .. autoproperty:: state 39 | 40 | The following attributes are available after the opening handshake, 41 | once the WebSocket connection is open: 42 | 43 | .. autoattribute:: request 44 | 45 | .. autoattribute:: response 46 | 47 | .. autoproperty:: subprotocol 48 | 49 | The following attributes are available after the closing handshake, 50 | once the WebSocket connection is closed: 51 | 52 | .. autoproperty:: close_code 53 | 54 | .. autoproperty:: close_reason 55 | -------------------------------------------------------------------------------- /docs/reference/asyncio/server.rst: -------------------------------------------------------------------------------- 1 | Server (:mod:`asyncio`) 2 | ======================= 3 | 4 | .. automodule:: websockets.asyncio.server 5 | 6 | Creating a server 7 | ----------------- 8 | 9 | .. autofunction:: serve 10 | :async: 11 | 12 | .. autofunction:: unix_serve 13 | :async: 14 | 15 | Routing connections 16 | ------------------- 17 | 18 | .. automodule:: websockets.asyncio.router 19 | 20 | .. autofunction:: route 21 | :async: 22 | 23 | .. autofunction:: unix_route 24 | :async: 25 | 26 | .. autoclass:: Router 27 | 28 | .. currentmodule:: websockets.asyncio.server 29 | 30 | Running a server 31 | ---------------- 32 | 33 | .. autoclass:: Server 34 | 35 | .. autoattribute:: connections 36 | 37 | .. automethod:: close 38 | 39 | .. automethod:: wait_closed 40 | 41 | .. automethod:: get_loop 42 | 43 | .. automethod:: is_serving 44 | 45 | .. automethod:: start_serving 46 | 47 | .. automethod:: serve_forever 48 | 49 | .. autoattribute:: sockets 50 | 51 | Using a connection 52 | ------------------ 53 | 54 | .. autoclass:: ServerConnection 55 | 56 | .. automethod:: __aiter__ 57 | 58 | .. automethod:: recv 59 | 60 | .. automethod:: recv_streaming 61 | 62 | .. automethod:: send 63 | 64 | .. automethod:: close 65 | 66 | .. automethod:: wait_closed 67 | 68 | .. automethod:: ping 69 | 70 | .. automethod:: pong 71 | 72 | .. automethod:: respond 73 | 74 | WebSocket connection objects also provide these attributes: 75 | 76 | .. autoattribute:: id 77 | 78 | .. autoattribute:: logger 79 | 80 | .. autoproperty:: local_address 81 | 82 | .. autoproperty:: remote_address 83 | 84 | .. autoattribute:: latency 85 | 86 | .. autoproperty:: state 87 | 88 | The following attributes are available after the opening handshake, 89 | once the WebSocket connection is open: 90 | 91 | .. autoattribute:: request 92 | 93 | .. autoattribute:: response 94 | 95 | .. autoproperty:: subprotocol 96 | 97 | The following attributes are available after the closing handshake, 98 | once the WebSocket connection is closed: 99 | 100 | .. autoproperty:: close_code 101 | 102 | .. autoproperty:: close_reason 103 | 104 | Broadcast 105 | --------- 106 | 107 | .. autofunction:: broadcast 108 | 109 | HTTP Basic Authentication 110 | ------------------------- 111 | 112 | websockets supports HTTP Basic Authentication according to 113 | :rfc:`7235` and :rfc:`7617`. 114 | 115 | .. autofunction:: basic_auth 116 | -------------------------------------------------------------------------------- /docs/reference/datastructures.rst: -------------------------------------------------------------------------------- 1 | Data structures 2 | =============== 3 | 4 | WebSocket events 5 | ---------------- 6 | 7 | .. automodule:: websockets.frames 8 | 9 | .. autoclass:: Frame 10 | 11 | .. autoclass:: Opcode 12 | 13 | .. autoattribute:: CONT 14 | .. autoattribute:: TEXT 15 | .. autoattribute:: BINARY 16 | .. autoattribute:: CLOSE 17 | .. autoattribute:: PING 18 | .. autoattribute:: PONG 19 | 20 | .. autoclass:: Close 21 | 22 | .. autoclass:: CloseCode 23 | 24 | .. autoattribute:: NORMAL_CLOSURE 25 | .. autoattribute:: GOING_AWAY 26 | .. autoattribute:: PROTOCOL_ERROR 27 | .. autoattribute:: UNSUPPORTED_DATA 28 | .. autoattribute:: NO_STATUS_RCVD 29 | .. autoattribute:: ABNORMAL_CLOSURE 30 | .. autoattribute:: INVALID_DATA 31 | .. autoattribute:: POLICY_VIOLATION 32 | .. autoattribute:: MESSAGE_TOO_BIG 33 | .. autoattribute:: MANDATORY_EXTENSION 34 | .. autoattribute:: INTERNAL_ERROR 35 | .. autoattribute:: SERVICE_RESTART 36 | .. autoattribute:: TRY_AGAIN_LATER 37 | .. autoattribute:: BAD_GATEWAY 38 | .. autoattribute:: TLS_HANDSHAKE 39 | 40 | HTTP events 41 | ----------- 42 | 43 | .. automodule:: websockets.http11 44 | 45 | .. autoclass:: Request 46 | 47 | .. autoclass:: Response 48 | 49 | .. automodule:: websockets.datastructures 50 | 51 | .. autoclass:: Headers 52 | 53 | .. automethod:: get_all 54 | 55 | .. automethod:: raw_items 56 | 57 | .. autoexception:: MultipleValuesError 58 | 59 | URIs 60 | ---- 61 | 62 | .. automodule:: websockets.uri 63 | 64 | .. autofunction:: parse_uri 65 | 66 | .. autoclass:: WebSocketURI 67 | -------------------------------------------------------------------------------- /docs/reference/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | .. automodule:: websockets.exceptions 5 | 6 | .. autoexception:: WebSocketException 7 | 8 | Connection closed 9 | ----------------- 10 | 11 | :meth:`~websockets.asyncio.connection.Connection.recv`, 12 | :meth:`~websockets.asyncio.connection.Connection.send`, and similar methods 13 | raise the exceptions below when the connection is closed. This is the expected 14 | way to detect disconnections. 15 | 16 | .. autoexception:: ConnectionClosed 17 | 18 | .. autoexception:: ConnectionClosedOK 19 | 20 | .. autoexception:: ConnectionClosedError 21 | 22 | Connection failed 23 | ----------------- 24 | 25 | These exceptions are raised by :func:`~websockets.asyncio.client.connect` when 26 | the opening handshake fails and the connection cannot be established. They are 27 | also reported by :func:`~websockets.asyncio.server.serve` in logs. 28 | 29 | .. autoexception:: InvalidURI 30 | 31 | .. autoexception:: InvalidProxy 32 | 33 | .. autoexception:: InvalidHandshake 34 | 35 | .. autoexception:: SecurityError 36 | 37 | .. autoexception:: ProxyError 38 | 39 | .. autoexception:: InvalidProxyMessage 40 | 41 | .. autoexception:: InvalidProxyStatus 42 | 43 | .. autoexception:: InvalidMessage 44 | 45 | .. autoexception:: InvalidStatus 46 | 47 | .. autoexception:: InvalidHeader 48 | 49 | .. autoexception:: InvalidHeaderFormat 50 | 51 | .. autoexception:: InvalidHeaderValue 52 | 53 | .. autoexception:: InvalidOrigin 54 | 55 | .. autoexception:: InvalidUpgrade 56 | 57 | .. autoexception:: NegotiationError 58 | 59 | .. autoexception:: DuplicateParameter 60 | 61 | .. autoexception:: InvalidParameterName 62 | 63 | .. autoexception:: InvalidParameterValue 64 | 65 | Sans-I/O exceptions 66 | ------------------- 67 | 68 | These exceptions are only raised by the Sans-I/O implementation. They are 69 | translated to :exc:`ConnectionClosedError` in the other implementations. 70 | 71 | .. autoexception:: ProtocolError 72 | 73 | .. autoexception:: PayloadTooBig 74 | 75 | .. autoexception:: InvalidState 76 | 77 | Miscellaneous exceptions 78 | ------------------------ 79 | 80 | .. autoexception:: ConcurrencyError 81 | 82 | Legacy exceptions 83 | ----------------- 84 | 85 | These exceptions are only used by the legacy :mod:`asyncio` implementation. 86 | 87 | .. autoexception:: InvalidStatusCode 88 | 89 | .. autoexception:: AbortHandshake 90 | 91 | .. autoexception:: RedirectHandshake 92 | -------------------------------------------------------------------------------- /docs/reference/extensions.rst: -------------------------------------------------------------------------------- 1 | Extensions 2 | ========== 3 | 4 | .. currentmodule:: websockets.extensions 5 | 6 | The WebSocket protocol supports extensions_. 7 | 8 | At the time of writing, there's only one `registered extension`_ with a public 9 | specification, WebSocket Per-Message Deflate. 10 | 11 | .. _extensions: https://datatracker.ietf.org/doc/html/rfc6455.html#section-9 12 | .. _registered extension: https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name 13 | 14 | Per-Message Deflate 15 | ------------------- 16 | 17 | .. automodule:: websockets.extensions.permessage_deflate 18 | 19 | :mod:`websockets.extensions.permessage_deflate` implements WebSocket Per-Message 20 | Deflate. 21 | 22 | This extension is specified in :rfc:`7692`. 23 | 24 | Refer to the :doc:`topic guide on compression <../topics/compression>` to learn 25 | more about tuning compression settings. 26 | 27 | .. autoclass:: ServerPerMessageDeflateFactory 28 | 29 | .. autoclass:: ClientPerMessageDeflateFactory 30 | 31 | Base classes 32 | ------------ 33 | 34 | .. automodule:: websockets.extensions 35 | 36 | :mod:`websockets.extensions` defines base classes for implementing extensions. 37 | 38 | Refer to the :doc:`how-to guide on extensions <../howto/extensions>` to learn 39 | more about writing an extension. 40 | 41 | .. autoclass:: Extension 42 | 43 | .. autoattribute:: name 44 | 45 | .. automethod:: decode 46 | 47 | .. automethod:: encode 48 | 49 | .. autoclass:: ServerExtensionFactory 50 | 51 | .. automethod:: process_request_params 52 | 53 | .. autoclass:: ClientExtensionFactory 54 | 55 | .. autoattribute:: name 56 | 57 | .. automethod:: get_request_params 58 | 59 | .. automethod:: process_response_params 60 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. currentmodule:: websockets 5 | 6 | Features 7 | -------- 8 | 9 | Check which implementations support which features and known limitations. 10 | 11 | .. toctree:: 12 | :titlesonly: 13 | 14 | features 15 | 16 | :mod:`asyncio` 17 | -------------- 18 | 19 | It's ideal for servers that handle many clients concurrently. 20 | 21 | This is the default implementation. 22 | 23 | .. toctree:: 24 | :titlesonly: 25 | 26 | asyncio/server 27 | asyncio/client 28 | 29 | :mod:`threading` 30 | ---------------- 31 | 32 | This alternative implementation can be a good choice for clients. 33 | 34 | .. toctree:: 35 | :titlesonly: 36 | 37 | sync/server 38 | sync/client 39 | 40 | `Sans-I/O`_ 41 | ----------- 42 | 43 | This layer is designed for integrating in third-party libraries, typically 44 | application servers. 45 | 46 | .. _Sans-I/O: https://sans-io.readthedocs.io/ 47 | 48 | .. toctree:: 49 | :titlesonly: 50 | 51 | sansio/server 52 | sansio/client 53 | 54 | Legacy 55 | ------ 56 | 57 | This is the historical implementation. It is deprecated. It will be removed by 58 | 2030. 59 | 60 | .. toctree:: 61 | :titlesonly: 62 | 63 | legacy/server 64 | legacy/client 65 | 66 | Extensions 67 | ---------- 68 | 69 | The Per-Message Deflate extension is built-in. You may also define custom 70 | extensions. 71 | 72 | .. toctree:: 73 | :titlesonly: 74 | 75 | extensions 76 | 77 | Shared 78 | ------ 79 | 80 | These low-level APIs are shared by all implementations. 81 | 82 | .. toctree:: 83 | :titlesonly: 84 | 85 | datastructures 86 | exceptions 87 | types 88 | variables 89 | 90 | API stability 91 | ------------- 92 | 93 | Public APIs documented in this API reference are subject to the 94 | :ref:`backwards-compatibility policy `. 95 | 96 | Anything that isn't listed in the API reference is a private API. There's no 97 | guarantees of behavior or backwards-compatibility for private APIs. 98 | 99 | Convenience imports 100 | ------------------- 101 | 102 | For convenience, some public APIs can be imported directly from the 103 | ``websockets`` package. 104 | -------------------------------------------------------------------------------- /docs/reference/legacy/client.rst: -------------------------------------------------------------------------------- 1 | Client (legacy) 2 | =============== 3 | 4 | .. admonition:: The legacy :mod:`asyncio` implementation is deprecated. 5 | :class: caution 6 | 7 | The :doc:`upgrade guide <../../howto/upgrade>` provides complete instructions 8 | to migrate your application. 9 | 10 | .. automodule:: websockets.legacy.client 11 | 12 | Opening a connection 13 | -------------------- 14 | 15 | .. autofunction:: connect(uri, *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) 16 | :async: 17 | 18 | .. autofunction:: unix_connect(path, uri="ws://localhost/", *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) 19 | :async: 20 | 21 | Using a connection 22 | ------------------ 23 | 24 | .. autoclass:: WebSocketClientProtocol(*, logger=None, origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) 25 | 26 | .. automethod:: recv 27 | 28 | .. automethod:: send 29 | 30 | .. automethod:: close 31 | 32 | .. automethod:: wait_closed 33 | 34 | .. automethod:: ping 35 | 36 | .. automethod:: pong 37 | 38 | WebSocket connection objects also provide these attributes: 39 | 40 | .. autoattribute:: id 41 | 42 | .. autoattribute:: logger 43 | 44 | .. autoproperty:: local_address 45 | 46 | .. autoproperty:: remote_address 47 | 48 | .. autoproperty:: open 49 | 50 | .. autoproperty:: closed 51 | 52 | .. autoattribute:: latency 53 | 54 | The following attributes are available after the opening handshake, 55 | once the WebSocket connection is open: 56 | 57 | .. autoattribute:: path 58 | 59 | .. autoattribute:: request_headers 60 | 61 | .. autoattribute:: response_headers 62 | 63 | .. autoattribute:: subprotocol 64 | 65 | The following attributes are available after the closing handshake, 66 | once the WebSocket connection is closed: 67 | 68 | .. autoproperty:: close_code 69 | 70 | .. autoproperty:: close_reason 71 | -------------------------------------------------------------------------------- /docs/reference/legacy/common.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Both sides (legacy) 4 | =================== 5 | 6 | .. admonition:: The legacy :mod:`asyncio` implementation is deprecated. 7 | :class: caution 8 | 9 | The :doc:`upgrade guide <../../howto/upgrade>` provides complete instructions 10 | to migrate your application. 11 | 12 | .. automodule:: websockets.legacy.protocol 13 | 14 | .. autoclass:: WebSocketCommonProtocol(*, logger=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) 15 | 16 | .. automethod:: recv 17 | 18 | .. automethod:: send 19 | 20 | .. automethod:: close 21 | 22 | .. automethod:: wait_closed 23 | 24 | .. automethod:: ping 25 | 26 | .. automethod:: pong 27 | 28 | WebSocket connection objects also provide these attributes: 29 | 30 | .. autoattribute:: id 31 | 32 | .. autoattribute:: logger 33 | 34 | .. autoproperty:: local_address 35 | 36 | .. autoproperty:: remote_address 37 | 38 | .. autoproperty:: open 39 | 40 | .. autoproperty:: closed 41 | 42 | .. autoattribute:: latency 43 | 44 | The following attributes are available after the opening handshake, 45 | once the WebSocket connection is open: 46 | 47 | .. autoattribute:: path 48 | 49 | .. autoattribute:: request_headers 50 | 51 | .. autoattribute:: response_headers 52 | 53 | .. autoattribute:: subprotocol 54 | 55 | The following attributes are available after the closing handshake, 56 | once the WebSocket connection is closed: 57 | 58 | .. autoproperty:: close_code 59 | 60 | .. autoproperty:: close_reason 61 | -------------------------------------------------------------------------------- /docs/reference/sansio/client.rst: -------------------------------------------------------------------------------- 1 | Client (`Sans-I/O`_) 2 | ==================== 3 | 4 | .. _Sans-I/O: https://sans-io.readthedocs.io/ 5 | 6 | .. currentmodule:: websockets.client 7 | 8 | .. autoclass:: ClientProtocol 9 | 10 | .. automethod:: receive_data 11 | 12 | .. automethod:: receive_eof 13 | 14 | .. automethod:: connect 15 | 16 | .. automethod:: send_request 17 | 18 | .. automethod:: send_continuation 19 | 20 | .. automethod:: send_text 21 | 22 | .. automethod:: send_binary 23 | 24 | .. automethod:: send_close 25 | 26 | .. automethod:: send_ping 27 | 28 | .. automethod:: send_pong 29 | 30 | .. automethod:: fail 31 | 32 | .. automethod:: events_received 33 | 34 | .. automethod:: data_to_send 35 | 36 | .. automethod:: close_expected 37 | 38 | WebSocket protocol objects also provide these attributes: 39 | 40 | .. autoattribute:: id 41 | 42 | .. autoattribute:: logger 43 | 44 | .. autoproperty:: state 45 | 46 | The following attributes are available after the opening handshake, 47 | once the WebSocket connection is open: 48 | 49 | .. autoattribute:: handshake_exc 50 | 51 | The following attributes are available after the closing handshake, 52 | once the WebSocket connection is closed: 53 | 54 | .. autoproperty:: close_code 55 | 56 | .. autoproperty:: close_reason 57 | 58 | .. autoproperty:: close_exc 59 | -------------------------------------------------------------------------------- /docs/reference/sansio/common.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Both sides (`Sans-I/O`_) 4 | ========================= 5 | 6 | .. _Sans-I/O: https://sans-io.readthedocs.io/ 7 | 8 | .. automodule:: websockets.protocol 9 | 10 | .. autoclass:: Protocol 11 | 12 | .. automethod:: receive_data 13 | 14 | .. automethod:: receive_eof 15 | 16 | .. automethod:: send_continuation 17 | 18 | .. automethod:: send_text 19 | 20 | .. automethod:: send_binary 21 | 22 | .. automethod:: send_close 23 | 24 | .. automethod:: send_ping 25 | 26 | .. automethod:: send_pong 27 | 28 | .. automethod:: fail 29 | 30 | .. automethod:: events_received 31 | 32 | .. automethod:: data_to_send 33 | 34 | .. automethod:: close_expected 35 | 36 | .. autoattribute:: id 37 | 38 | .. autoattribute:: logger 39 | 40 | .. autoproperty:: state 41 | 42 | .. autoproperty:: close_code 43 | 44 | .. autoproperty:: close_reason 45 | 46 | .. autoproperty:: close_exc 47 | 48 | .. autoclass:: Side 49 | 50 | .. autoattribute:: SERVER 51 | 52 | .. autoattribute:: CLIENT 53 | 54 | .. autoclass:: State 55 | 56 | .. autoattribute:: CONNECTING 57 | 58 | .. autoattribute:: OPEN 59 | 60 | .. autoattribute:: CLOSING 61 | 62 | .. autoattribute:: CLOSED 63 | 64 | .. autodata:: SEND_EOF 65 | -------------------------------------------------------------------------------- /docs/reference/sansio/server.rst: -------------------------------------------------------------------------------- 1 | Server (`Sans-I/O`_) 2 | ==================== 3 | 4 | .. _Sans-I/O: https://sans-io.readthedocs.io/ 5 | 6 | .. currentmodule:: websockets.server 7 | 8 | .. autoclass:: ServerProtocol 9 | 10 | .. automethod:: receive_data 11 | 12 | .. automethod:: receive_eof 13 | 14 | .. automethod:: accept 15 | 16 | .. automethod:: select_subprotocol 17 | 18 | .. automethod:: reject 19 | 20 | .. automethod:: send_response 21 | 22 | .. automethod:: send_continuation 23 | 24 | .. automethod:: send_text 25 | 26 | .. automethod:: send_binary 27 | 28 | .. automethod:: send_close 29 | 30 | .. automethod:: send_ping 31 | 32 | .. automethod:: send_pong 33 | 34 | .. automethod:: fail 35 | 36 | .. automethod:: events_received 37 | 38 | .. automethod:: data_to_send 39 | 40 | .. automethod:: close_expected 41 | 42 | WebSocket protocol objects also provide these attributes: 43 | 44 | .. autoattribute:: id 45 | 46 | .. autoattribute:: logger 47 | 48 | .. autoproperty:: state 49 | 50 | The following attributes are available after the opening handshake, 51 | once the WebSocket connection is open: 52 | 53 | .. autoattribute:: handshake_exc 54 | 55 | The following attributes are available after the closing handshake, 56 | once the WebSocket connection is closed: 57 | 58 | .. autoproperty:: close_code 59 | 60 | .. autoproperty:: close_reason 61 | 62 | .. autoproperty:: close_exc 63 | -------------------------------------------------------------------------------- /docs/reference/sync/client.rst: -------------------------------------------------------------------------------- 1 | Client (:mod:`threading`) 2 | ========================= 3 | 4 | .. automodule:: websockets.sync.client 5 | 6 | Opening a connection 7 | -------------------- 8 | 9 | .. autofunction:: connect 10 | 11 | .. autofunction:: unix_connect 12 | 13 | Using a connection 14 | ------------------ 15 | 16 | .. autoclass:: ClientConnection 17 | 18 | .. automethod:: __iter__ 19 | 20 | .. automethod:: recv 21 | 22 | .. automethod:: recv_streaming 23 | 24 | .. automethod:: send 25 | 26 | .. automethod:: close 27 | 28 | .. automethod:: ping 29 | 30 | .. automethod:: pong 31 | 32 | WebSocket connection objects also provide these attributes: 33 | 34 | .. autoattribute:: id 35 | 36 | .. autoattribute:: logger 37 | 38 | .. autoproperty:: local_address 39 | 40 | .. autoproperty:: remote_address 41 | 42 | .. autoproperty:: latency 43 | 44 | .. autoproperty:: state 45 | 46 | The following attributes are available after the opening handshake, 47 | once the WebSocket connection is open: 48 | 49 | .. autoattribute:: request 50 | 51 | .. autoattribute:: response 52 | 53 | .. autoproperty:: subprotocol 54 | 55 | The following attributes are available after the closing handshake, 56 | once the WebSocket connection is closed: 57 | 58 | .. autoproperty:: close_code 59 | 60 | .. autoproperty:: close_reason 61 | -------------------------------------------------------------------------------- /docs/reference/sync/common.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Both sides (:mod:`threading`) 4 | ============================= 5 | 6 | .. automodule:: websockets.sync.connection 7 | 8 | .. autoclass:: Connection 9 | 10 | .. automethod:: __iter__ 11 | 12 | .. automethod:: recv 13 | 14 | .. automethod:: recv_streaming 15 | 16 | .. automethod:: send 17 | 18 | .. automethod:: close 19 | 20 | .. automethod:: ping 21 | 22 | .. automethod:: pong 23 | 24 | WebSocket connection objects also provide these attributes: 25 | 26 | .. autoattribute:: id 27 | 28 | .. autoattribute:: logger 29 | 30 | .. autoproperty:: local_address 31 | 32 | .. autoproperty:: remote_address 33 | 34 | .. autoattribute:: latency 35 | 36 | .. autoproperty:: state 37 | 38 | The following attributes are available after the opening handshake, 39 | once the WebSocket connection is open: 40 | 41 | .. autoattribute:: request 42 | 43 | .. autoattribute:: response 44 | 45 | .. autoproperty:: subprotocol 46 | 47 | The following attributes are available after the closing handshake, 48 | once the WebSocket connection is closed: 49 | 50 | .. autoproperty:: close_code 51 | 52 | .. autoproperty:: close_reason 53 | -------------------------------------------------------------------------------- /docs/reference/sync/server.rst: -------------------------------------------------------------------------------- 1 | Server (:mod:`threading`) 2 | ========================= 3 | 4 | .. automodule:: websockets.sync.server 5 | 6 | Creating a server 7 | ----------------- 8 | 9 | .. autofunction:: serve 10 | 11 | .. autofunction:: unix_serve 12 | 13 | Routing connections 14 | ------------------- 15 | 16 | .. automodule:: websockets.sync.router 17 | 18 | .. autofunction:: route 19 | 20 | .. autofunction:: unix_route 21 | 22 | .. autoclass:: Router 23 | 24 | .. currentmodule:: websockets.sync.server 25 | 26 | Running a server 27 | ---------------- 28 | 29 | .. autoclass:: Server 30 | 31 | .. automethod:: serve_forever 32 | 33 | .. automethod:: shutdown 34 | 35 | .. automethod:: fileno 36 | 37 | Using a connection 38 | ------------------ 39 | 40 | .. autoclass:: ServerConnection 41 | 42 | .. automethod:: __iter__ 43 | 44 | .. automethod:: recv 45 | 46 | .. automethod:: recv_streaming 47 | 48 | .. automethod:: send 49 | 50 | .. automethod:: close 51 | 52 | .. automethod:: ping 53 | 54 | .. automethod:: pong 55 | 56 | .. automethod:: respond 57 | 58 | WebSocket connection objects also provide these attributes: 59 | 60 | .. autoattribute:: id 61 | 62 | .. autoattribute:: logger 63 | 64 | .. autoproperty:: local_address 65 | 66 | .. autoproperty:: remote_address 67 | 68 | .. autoproperty:: latency 69 | 70 | .. autoproperty:: state 71 | 72 | The following attributes are available after the opening handshake, 73 | once the WebSocket connection is open: 74 | 75 | .. autoattribute:: request 76 | 77 | .. autoattribute:: response 78 | 79 | .. autoproperty:: subprotocol 80 | 81 | The following attributes are available after the closing handshake, 82 | once the WebSocket connection is closed: 83 | 84 | .. autoproperty:: close_code 85 | 86 | .. autoproperty:: close_reason 87 | 88 | HTTP Basic Authentication 89 | ------------------------- 90 | 91 | websockets supports HTTP Basic Authentication according to 92 | :rfc:`7235` and :rfc:`7617`. 93 | 94 | .. autofunction:: basic_auth 95 | -------------------------------------------------------------------------------- /docs/reference/types.rst: -------------------------------------------------------------------------------- 1 | Types 2 | ===== 3 | 4 | .. automodule:: websockets.typing 5 | 6 | .. autodata:: Data 7 | 8 | .. autodata:: LoggerLike 9 | 10 | .. autodata:: StatusLike 11 | 12 | .. autodata:: Origin 13 | 14 | .. autodata:: Subprotocol 15 | 16 | .. autodata:: ExtensionName 17 | 18 | .. autodata:: ExtensionParameter 19 | 20 | .. autodata:: websockets.protocol.Event 21 | 22 | .. autodata:: websockets.datastructures.HeadersLike 23 | 24 | .. autodata:: websockets.datastructures.SupportsKeysAndGetItem 25 | -------------------------------------------------------------------------------- /docs/reference/variables.rst: -------------------------------------------------------------------------------- 1 | Environment variables 2 | ===================== 3 | 4 | .. currentmodule:: websockets 5 | 6 | Logging 7 | ------- 8 | 9 | .. envvar:: WEBSOCKETS_MAX_LOG_SIZE 10 | 11 | How much of each frame to show in debug logs. 12 | 13 | The default value is ``75``. 14 | 15 | See the :doc:`logging guide <../topics/logging>` for details. 16 | 17 | Security 18 | -------- 19 | 20 | .. envvar:: WEBSOCKETS_SERVER 21 | 22 | Server header sent by websockets. 23 | 24 | The default value uses the format ``"Python/x.y.z websockets/X.Y"``. 25 | 26 | .. envvar:: WEBSOCKETS_USER_AGENT 27 | 28 | User-Agent header sent by websockets. 29 | 30 | The default value uses the format ``"Python/x.y.z websockets/X.Y"``. 31 | 32 | .. envvar:: WEBSOCKETS_MAX_LINE_LENGTH 33 | 34 | Maximum length of the request or status line in the opening handshake. 35 | 36 | The default value is ``8192`` bytes. 37 | 38 | .. envvar:: WEBSOCKETS_MAX_NUM_HEADERS 39 | 40 | Maximum number of HTTP headers in the opening handshake. 41 | 42 | The default value is ``128`` bytes. 43 | 44 | .. envvar:: WEBSOCKETS_MAX_BODY_SIZE 45 | 46 | Maximum size of the body of an HTTP response in the opening handshake. 47 | 48 | The default value is ``1_048_576`` bytes (1 MiB). 49 | 50 | See the :doc:`security guide <../topics/security>` for details. 51 | 52 | Reconnection 53 | ------------ 54 | 55 | Reconnection attempts are spaced out with truncated exponential backoff. 56 | 57 | .. envvar:: WEBSOCKETS_BACKOFF_INITIAL_DELAY 58 | 59 | The first attempt is delayed by a random amount of time between ``0`` and 60 | ``WEBSOCKETS_BACKOFF_INITIAL_DELAY`` seconds. 61 | 62 | The default value is ``5.0`` seconds. 63 | 64 | .. envvar:: WEBSOCKETS_BACKOFF_MIN_DELAY 65 | 66 | The second attempt is delayed by ``WEBSOCKETS_BACKOFF_MIN_DELAY`` seconds. 67 | 68 | The default value is ``3.1`` seconds. 69 | 70 | .. envvar:: WEBSOCKETS_BACKOFF_FACTOR 71 | 72 | After the second attempt, the delay is multiplied by 73 | ``WEBSOCKETS_BACKOFF_FACTOR`` between each attempt. 74 | 75 | The default value is ``1.618``. 76 | 77 | .. envvar:: WEBSOCKETS_BACKOFF_MAX_DELAY 78 | 79 | The delay between attempts is capped at ``WEBSOCKETS_BACKOFF_MAX_DELAY`` 80 | seconds. 81 | 82 | The default value is ``90.0`` seconds. 83 | 84 | Redirects 85 | --------- 86 | 87 | .. envvar:: WEBSOCKETS_MAX_REDIRECTS 88 | 89 | Maximum number of redirects that :func:`~asyncio.client.connect` follows. 90 | 91 | The default value is ``10``. 92 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | furo 2 | sphinx 3 | sphinx-autobuild 4 | sphinx-copybutton 5 | sphinx-inline-tabs 6 | sphinxcontrib-spelling 7 | sphinxcontrib-trio 8 | sphinxext-opengraph 9 | werkzeug 10 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | augustin 2 | auth 3 | autoscaler 4 | aymeric 5 | backend 6 | backoff 7 | backpressure 8 | balancer 9 | balancers 10 | bottlenecked 11 | bufferbloat 12 | bugfix 13 | buildpack 14 | bytestring 15 | bytestrings 16 | changelog 17 | coroutine 18 | coroutines 19 | cryptocurrencies 20 | cryptocurrency 21 | css 22 | ctrl 23 | deserialize 24 | dev 25 | django 26 | Dockerfile 27 | dyno 28 | formatter 29 | fractalideas 30 | github 31 | gunicorn 32 | healthz 33 | html 34 | hypercorn 35 | iframe 36 | io 37 | IPv 38 | istio 39 | iterable 40 | js 41 | keepalive 42 | KiB 43 | koyeb 44 | kubernetes 45 | lifecycle 46 | linkerd 47 | liveness 48 | lookups 49 | MiB 50 | middleware 51 | mutex 52 | mypy 53 | nginx 54 | PaaS 55 | Paketo 56 | permessage 57 | pid 58 | procfile 59 | proxying 60 | py 61 | pythonic 62 | reconnection 63 | redis 64 | redistributions 65 | retransmit 66 | retryable 67 | runtime 68 | scalable 69 | stateful 70 | subclasses 71 | subclassing 72 | submodule 73 | subpackages 74 | subprotocol 75 | subprotocols 76 | supervisord 77 | tidelift 78 | tls 79 | tox 80 | txt 81 | unregister 82 | uple 83 | uvicorn 84 | uvloop 85 | virtualenv 86 | websocket 87 | WebSocket 88 | websockets 89 | ws 90 | wsgi 91 | www 92 | -------------------------------------------------------------------------------- /docs/topics/index.rst: -------------------------------------------------------------------------------- 1 | Topic guides 2 | ============ 3 | 4 | These documents discuss how websockets is designed and how to make the best of 5 | its features when building applications. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | authentication 11 | broadcast 12 | logging 13 | proxies 14 | routing 15 | 16 | These guides describe how to optimize the configuration of websockets 17 | applications for performance and reliability. 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | compression 23 | keepalive 24 | memory 25 | security 26 | performance 27 | -------------------------------------------------------------------------------- /docs/topics/lifecycle.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/docs/topics/lifecycle.graffle -------------------------------------------------------------------------------- /docs/topics/performance.rst: -------------------------------------------------------------------------------- 1 | Performance 2 | =========== 3 | 4 | .. currentmodule:: websockets 5 | 6 | Here are tips to optimize performance. 7 | 8 | uvloop 9 | ------ 10 | 11 | You can make a websockets application faster by running it with uvloop_. 12 | 13 | (This advice isn't specific to websockets. It applies to any :mod:`asyncio` 14 | application.) 15 | 16 | .. _uvloop: https://github.com/MagicStack/uvloop 17 | 18 | broadcast 19 | --------- 20 | 21 | :func:`~asyncio.server.broadcast` is the most efficient way to send a message to 22 | many clients. 23 | -------------------------------------------------------------------------------- /docs/topics/protocol.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/docs/topics/protocol.graffle -------------------------------------------------------------------------------- /docs/topics/routing.rst: -------------------------------------------------------------------------------- 1 | Routing 2 | ======= 3 | 4 | .. currentmodule:: websockets 5 | 6 | Many WebSocket servers provide just one endpoint. That's why 7 | :func:`~asyncio.server.serve` accepts a single connection handler as its first 8 | argument. 9 | 10 | This may come as a surprise to you if you're used to HTTP servers. In a standard 11 | HTTP application, each request gets dispatched to a handler based on the request 12 | path. Clients know which path to use for which operation. 13 | 14 | In a WebSocket application, clients open a persistent connection then they send 15 | all messages over that unique connection. When different messages correspond to 16 | different operations, they must be dispatched based on the message content. 17 | 18 | Simple routing 19 | -------------- 20 | 21 | If you need different handlers for different clients or different use cases, you 22 | may route each connection to the right handler based on the request path. 23 | 24 | Since WebSocket servers typically provide fewer routes than HTTP servers, you 25 | can keep it simple:: 26 | 27 | async def handler(websocket): 28 | match websocket.request.path: 29 | case "/blue": 30 | await blue_handler(websocket) 31 | case "/green": 32 | await green_handler(websocket) 33 | case _: 34 | # No handler for this path. Close the connection. 35 | return 36 | 37 | You may also route connections based on the first message received from the 38 | client, as demonstrated in the :doc:`tutorial <../intro/tutorial2>`:: 39 | 40 | import json 41 | 42 | async def handler(websocket): 43 | message = await websocket.recv() 44 | settings = json.loads(message) 45 | match settings["color"]: 46 | case "blue": 47 | await blue_handler(websocket) 48 | case "green": 49 | await green_handler(websocket) 50 | case _: 51 | # No handler for this message. Close the connection. 52 | return 53 | 54 | When you need to authenticate the connection before routing it, this pattern is 55 | more convenient. 56 | 57 | Complex routing 58 | --------------- 59 | 60 | If you have outgrow these simple patterns, websockets provides full-fledged 61 | routing based on the request path with :func:`~asyncio.router.route`. 62 | 63 | This feature builds upon Flask_'s router. To use it, you must install the 64 | third-party library `werkzeug`_: 65 | 66 | .. code-block:: console 67 | 68 | $ pip install werkzeug 69 | 70 | .. _Flask: https://flask.palletsprojects.com/ 71 | .. _werkzeug: https://werkzeug.palletsprojects.com/ 72 | 73 | :func:`~asyncio.router.route` expects a :class:`werkzeug.routing.Map` as its 74 | first argument to declare which URL patterns map to which handlers. Review the 75 | documentation of :mod:`werkzeug.routing` to learn about its functionality. 76 | 77 | To give you a sense of what's possible, here's the URL map of the example in 78 | `experiments/routing.py`_: 79 | 80 | .. _experiments/routing.py: https://github.com/python-websockets/websockets/blob/main/experiments/routing.py 81 | 82 | .. literalinclude:: ../../experiments/routing.py 83 | :start-at: url_map = Map( 84 | :end-at: await server.serve_forever() 85 | -------------------------------------------------------------------------------- /docs/topics/security.rst: -------------------------------------------------------------------------------- 1 | Security 2 | ======== 3 | 4 | .. currentmodule:: websockets 5 | 6 | Encryption 7 | ---------- 8 | 9 | In production, you should always secure WebSocket connections with TLS. 10 | 11 | Secure WebSocket connections provide confidentiality and integrity, as well as 12 | better reliability because they reduce the risk of interference by bad proxies. 13 | 14 | WebSocket servers are usually deployed behind a reverse proxy that terminates 15 | TLS. Else, you can :doc:`configure TLS <../howto/encryption>` for the server. 16 | 17 | Memory usage 18 | ------------ 19 | 20 | .. warning:: 21 | 22 | An attacker who can open an arbitrary number of connections will be able 23 | to perform a denial of service by memory exhaustion. If you're concerned 24 | by denial of service attacks, you must reject suspicious connections 25 | before they reach websockets, typically in a reverse proxy. 26 | 27 | With the default settings, opening a connection uses 70 KiB of memory. 28 | 29 | Sending some highly compressed messages could use up to 128 MiB of memory with 30 | an amplification factor of 1000 between network traffic and memory usage. 31 | 32 | Configuring a server to :doc:`optimize memory usage ` will improve 33 | security in addition to improving performance. 34 | 35 | HTTP limits 36 | ----------- 37 | 38 | In the opening handshake, websockets applies limits to the amount of data that 39 | it accepts in order to minimize exposure to denial of service attacks. 40 | 41 | The request or status line is limited to 8192 bytes. Each header line, including 42 | the name and value, is limited to 8192 bytes too. No more than 128 HTTP headers 43 | are allowed. When the HTTP response includes a body, it is limited to 1 MiB. 44 | 45 | You may change these limits by setting the :envvar:`WEBSOCKETS_MAX_LINE_LENGTH`, 46 | :envvar:`WEBSOCKETS_MAX_NUM_HEADERS`, and :envvar:`WEBSOCKETS_MAX_BODY_SIZE` 47 | environment variables respectively. 48 | 49 | Identification 50 | -------------- 51 | 52 | By default, websockets identifies itself with a ``Server`` or ``User-Agent`` 53 | header in the format ``"Python/x.y.z websockets/X.Y"``. 54 | 55 | You can set the ``server_header`` argument of :func:`~asyncio.server.serve` or 56 | the ``user_agent_header`` argument of :func:`~asyncio.client.connect` to 57 | configure another value. Setting them to :obj:`None` removes the header. 58 | 59 | Alternatively, you can set the :envvar:`WEBSOCKETS_SERVER` and 60 | :envvar:`WEBSOCKETS_USER_AGENT` environment variables respectively. Setting them 61 | to an empty string removes the header. 62 | 63 | If both the argument and the environment variable are set, the argument takes 64 | precedence. 65 | -------------------------------------------------------------------------------- /example/asyncio/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Client example using the asyncio API.""" 4 | 5 | import asyncio 6 | 7 | from websockets.asyncio.client import connect 8 | 9 | 10 | async def hello(): 11 | async with connect("ws://localhost:8765") as websocket: 12 | name = input("What's your name? ") 13 | 14 | await websocket.send(name) 15 | print(f">>> {name}") 16 | 17 | greeting = await websocket.recv() 18 | print(f"<<< {greeting}") 19 | 20 | 21 | if __name__ == "__main__": 22 | asyncio.run(hello()) 23 | -------------------------------------------------------------------------------- /example/asyncio/echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Echo server using the asyncio API.""" 4 | 5 | import asyncio 6 | from websockets.asyncio.server import serve 7 | 8 | 9 | async def echo(websocket): 10 | async for message in websocket: 11 | await websocket.send(message) 12 | 13 | 14 | async def main(): 15 | async with serve(echo, "localhost", 8765) as server: 16 | await server.serve_forever() 17 | 18 | 19 | if __name__ == "__main__": 20 | asyncio.run(main()) 21 | -------------------------------------------------------------------------------- /example/asyncio/hello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Client using the asyncio API.""" 4 | 5 | import asyncio 6 | from websockets.asyncio.client import connect 7 | 8 | 9 | async def hello(): 10 | async with connect("ws://localhost:8765") as websocket: 11 | await websocket.send("Hello world!") 12 | message = await websocket.recv() 13 | print(message) 14 | 15 | 16 | if __name__ == "__main__": 17 | asyncio.run(hello()) 18 | -------------------------------------------------------------------------------- /example/asyncio/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Server example using the asyncio API.""" 4 | 5 | import asyncio 6 | from websockets.asyncio.server import serve 7 | 8 | 9 | async def hello(websocket): 10 | name = await websocket.recv() 11 | print(f"<<< {name}") 12 | 13 | greeting = f"Hello {name}!" 14 | 15 | await websocket.send(greeting) 16 | print(f">>> {greeting}") 17 | 18 | 19 | async def main(): 20 | async with serve(hello, "localhost", 8765) as server: 21 | await server.serve_forever() 22 | 23 | 24 | if __name__ == "__main__": 25 | asyncio.run(main()) 26 | -------------------------------------------------------------------------------- /example/deployment/fly/Procfile: -------------------------------------------------------------------------------- 1 | web: python app.py 2 | -------------------------------------------------------------------------------- /example/deployment/fly/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import http 5 | import signal 6 | 7 | from websockets.asyncio.server import serve 8 | 9 | 10 | async def echo(websocket): 11 | async for message in websocket: 12 | await websocket.send(message) 13 | 14 | 15 | def health_check(connection, request): 16 | if request.path == "/healthz": 17 | return connection.respond(http.HTTPStatus.OK, "OK\n") 18 | 19 | 20 | async def main(): 21 | async with serve(echo, "", 8080, process_request=health_check) as server: 22 | loop = asyncio.get_running_loop() 23 | loop.add_signal_handler(signal.SIGTERM, server.close) 24 | await server.wait_closed() 25 | 26 | 27 | if __name__ == "__main__": 28 | asyncio.run(main()) 29 | -------------------------------------------------------------------------------- /example/deployment/fly/fly.toml: -------------------------------------------------------------------------------- 1 | app = "websockets-echo" 2 | kill_signal = "SIGTERM" 3 | 4 | [build] 5 | builder = "paketobuildpacks/builder:base" 6 | 7 | [[services]] 8 | internal_port = 8080 9 | protocol = "tcp" 10 | 11 | [[services.http_checks]] 12 | path = "/healthz" 13 | 14 | [[services.ports]] 15 | handlers = ["tls", "http"] 16 | port = 443 17 | -------------------------------------------------------------------------------- /example/deployment/fly/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | -------------------------------------------------------------------------------- /example/deployment/haproxy/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import os 5 | import signal 6 | 7 | from websockets.asyncio.server import serve 8 | 9 | 10 | async def echo(websocket): 11 | async for message in websocket: 12 | await websocket.send(message) 13 | 14 | 15 | async def main(): 16 | port = 8000 + int(os.environ["SUPERVISOR_PROCESS_NAME"][-2:]) 17 | async with serve(echo, "localhost", port) as server: 18 | loop = asyncio.get_running_loop() 19 | loop.add_signal_handler(signal.SIGTERM, server.close) 20 | await server.wait_closed() 21 | 22 | 23 | if __name__ == "__main__": 24 | asyncio.run(main()) 25 | -------------------------------------------------------------------------------- /example/deployment/haproxy/haproxy.cfg: -------------------------------------------------------------------------------- 1 | defaults 2 | mode http 3 | timeout connect 10s 4 | timeout client 30s 5 | timeout server 30s 6 | 7 | frontend websocket 8 | bind localhost:8080 9 | default_backend websocket 10 | 11 | backend websocket 12 | balance leastconn 13 | server websockets-test_00 localhost:8000 14 | server websockets-test_01 localhost:8001 15 | server websockets-test_02 localhost:8002 16 | server websockets-test_03 localhost:8003 17 | 18 | -------------------------------------------------------------------------------- /example/deployment/haproxy/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | 3 | [program:websockets-test] 4 | command = python app.py 5 | process_name = %(program_name)s_%(process_num)02d 6 | numprocs = 4 7 | autorestart = true 8 | -------------------------------------------------------------------------------- /example/deployment/heroku/Procfile: -------------------------------------------------------------------------------- 1 | web: python app.py 2 | -------------------------------------------------------------------------------- /example/deployment/heroku/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import signal 5 | import os 6 | 7 | from websockets.asyncio.server import serve 8 | 9 | 10 | async def echo(websocket): 11 | async for message in websocket: 12 | await websocket.send(message) 13 | 14 | 15 | async def main(): 16 | port = int(os.environ["PORT"]) 17 | async with serve(echo, "localhost", port) as server: 18 | loop = asyncio.get_running_loop() 19 | loop.add_signal_handler(signal.SIGTERM, server.close) 20 | await server.wait_closed() 21 | 22 | 23 | if __name__ == "__main__": 24 | asyncio.run(main()) 25 | -------------------------------------------------------------------------------- /example/deployment/heroku/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | -------------------------------------------------------------------------------- /example/deployment/koyeb/Procfile: -------------------------------------------------------------------------------- 1 | web: python app.py 2 | -------------------------------------------------------------------------------- /example/deployment/koyeb/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import http 5 | import os 6 | import signal 7 | 8 | from websockets.asyncio.server import serve 9 | 10 | 11 | async def echo(websocket): 12 | async for message in websocket: 13 | await websocket.send(message) 14 | 15 | 16 | def health_check(connection, request): 17 | if request.path == "/healthz": 18 | return connection.respond(http.HTTPStatus.OK, "OK\n") 19 | 20 | 21 | async def main(): 22 | port = int(os.environ["PORT"]) 23 | async with serve(echo, "", port, process_request=health_check) as server: 24 | loop = asyncio.get_running_loop() 25 | loop.add_signal_handler(signal.SIGTERM, server.close) 26 | await server.wait_closed() 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /example/deployment/koyeb/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | -------------------------------------------------------------------------------- /example/deployment/kubernetes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-alpine 2 | 3 | RUN pip install websockets 4 | 5 | COPY app.py . 6 | 7 | CMD ["python", "app.py"] 8 | -------------------------------------------------------------------------------- /example/deployment/kubernetes/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import http 5 | import signal 6 | import sys 7 | import time 8 | 9 | from websockets.asyncio.server import serve 10 | 11 | 12 | async def slow_echo(websocket): 13 | async for message in websocket: 14 | # Block the event loop! This allows saturating a single asyncio 15 | # process without opening an impractical number of connections. 16 | time.sleep(0.1) # 100ms 17 | await websocket.send(message) 18 | 19 | 20 | def health_check(connection, request): 21 | if request.path == "/healthz": 22 | return connection.respond(http.HTTPStatus.OK, "OK\n") 23 | if request.path == "/inemuri": 24 | loop = asyncio.get_running_loop() 25 | loop.call_later(1, time.sleep, 10) 26 | return connection.respond(http.HTTPStatus.OK, "Sleeping for 10s\n") 27 | if request.path == "/seppuku": 28 | loop = asyncio.get_running_loop() 29 | loop.call_later(1, sys.exit, 69) 30 | return connection.respond(http.HTTPStatus.OK, "Terminating\n") 31 | 32 | 33 | async def main(): 34 | async with serve(slow_echo, "", 80, process_request=health_check) as server: 35 | loop = asyncio.get_running_loop() 36 | loop.add_signal_handler(signal.SIGTERM, server.close) 37 | await server.wait_closed() 38 | 39 | 40 | if __name__ == "__main__": 41 | asyncio.run(main()) 42 | -------------------------------------------------------------------------------- /example/deployment/kubernetes/benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import sys 5 | 6 | from websockets.asyncio.client import connect 7 | 8 | 9 | URI = "ws://localhost:32080" 10 | 11 | 12 | async def run(client_id, messages): 13 | async with connect(URI) as websocket: 14 | for message_id in range(messages): 15 | await websocket.send(f"{client_id}:{message_id}") 16 | await websocket.recv() 17 | 18 | 19 | async def benchmark(clients, messages): 20 | await asyncio.wait([ 21 | asyncio.create_task(run(client_id, messages)) 22 | for client_id in range(clients) 23 | ]) 24 | 25 | 26 | if __name__ == "__main__": 27 | clients, messages = int(sys.argv[1]), int(sys.argv[2]) 28 | asyncio.run(benchmark(clients, messages)) 29 | -------------------------------------------------------------------------------- /example/deployment/kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: websockets-test 5 | spec: 6 | type: NodePort 7 | ports: 8 | - port: 80 9 | nodePort: 32080 10 | selector: 11 | app: websockets-test 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: websockets-test 17 | spec: 18 | selector: 19 | matchLabels: 20 | app: websockets-test 21 | template: 22 | metadata: 23 | labels: 24 | app: websockets-test 25 | spec: 26 | containers: 27 | - name: websockets-test 28 | image: websockets-test:1.0 29 | livenessProbe: 30 | httpGet: 31 | path: /healthz 32 | port: 80 33 | periodSeconds: 1 34 | ports: 35 | - containerPort: 80 36 | -------------------------------------------------------------------------------- /example/deployment/nginx/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import os 5 | import signal 6 | 7 | from websockets.asyncio.server import unix_serve 8 | 9 | 10 | async def echo(websocket): 11 | async for message in websocket: 12 | await websocket.send(message) 13 | 14 | 15 | async def main(): 16 | path = f"{os.environ['SUPERVISOR_PROCESS_NAME']}.sock" 17 | async with unix_serve(echo, path) as server: 18 | loop = asyncio.get_running_loop() 19 | loop.add_signal_handler(signal.SIGTERM, server.close) 20 | await server.wait_closed() 21 | 22 | 23 | if __name__ == "__main__": 24 | asyncio.run(main()) 25 | -------------------------------------------------------------------------------- /example/deployment/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | 3 | events { 4 | } 5 | 6 | http { 7 | server { 8 | listen localhost:8080; 9 | 10 | location / { 11 | proxy_http_version 1.1; 12 | proxy_pass http://websocket; 13 | proxy_set_header Connection $http_connection; 14 | proxy_set_header Upgrade $http_upgrade; 15 | } 16 | } 17 | 18 | upstream websocket { 19 | least_conn; 20 | server unix:websockets-test_00.sock; 21 | server unix:websockets-test_01.sock; 22 | server unix:websockets-test_02.sock; 23 | server unix:websockets-test_03.sock; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/deployment/nginx/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | 3 | [program:websockets-test] 4 | command = python app.py 5 | process_name = %(program_name)s_%(process_num)02d 6 | numprocs = 4 7 | autorestart = true 8 | -------------------------------------------------------------------------------- /example/deployment/render/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import http 5 | import signal 6 | 7 | from websockets.asyncio.server import serve 8 | 9 | 10 | async def echo(websocket): 11 | async for message in websocket: 12 | await websocket.send(message) 13 | 14 | 15 | def health_check(connection, request): 16 | if request.path == "/healthz": 17 | return connection.respond(http.HTTPStatus.OK, "OK\n") 18 | 19 | 20 | async def main(): 21 | async with serve(echo, "", 8080, process_request=health_check) as server: 22 | loop = asyncio.get_running_loop() 23 | loop.add_signal_handler(signal.SIGTERM, server.close) 24 | await server.wait_closed() 25 | 26 | 27 | if __name__ == "__main__": 28 | asyncio.run(main()) 29 | -------------------------------------------------------------------------------- /example/deployment/render/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | -------------------------------------------------------------------------------- /example/deployment/supervisor/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import signal 5 | 6 | from websockets.asyncio.server import serve 7 | 8 | 9 | async def echo(websocket): 10 | async for message in websocket: 11 | await websocket.send(message) 12 | 13 | 14 | async def main(): 15 | async with serve(echo, "", 8080, reuse_port=True) as server: 16 | loop = asyncio.get_running_loop() 17 | loop.add_signal_handler(signal.SIGTERM, server.close) 18 | await server.wait_closed() 19 | 20 | 21 | if __name__ == "__main__": 22 | asyncio.run(main()) 23 | -------------------------------------------------------------------------------- /example/deployment/supervisor/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | 3 | [program:websockets-test] 4 | command = python app.py 5 | process_name = %(program_name)s_%(process_num)02d 6 | numprocs = 4 7 | autorestart = true 8 | -------------------------------------------------------------------------------- /example/django/authentication.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | 5 | import django 6 | 7 | django.setup() 8 | 9 | from sesame.utils import get_user 10 | from websockets.asyncio.server import serve 11 | from websockets.frames import CloseCode 12 | 13 | 14 | async def handler(websocket): 15 | sesame = await websocket.recv() 16 | user = await asyncio.to_thread(get_user, sesame) 17 | if user is None: 18 | await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") 19 | return 20 | 21 | await websocket.send(f"Hello {user}!") 22 | 23 | 24 | async def main(): 25 | async with serve(handler, "localhost", 8888) as server: 26 | await server.serve_forever() 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /example/django/notifications.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import json 5 | 6 | import aioredis 7 | import django 8 | 9 | django.setup() 10 | 11 | from django.contrib.contenttypes.models import ContentType 12 | from sesame.utils import get_user 13 | from websockets.asyncio.server import broadcast, serve 14 | from websockets.frames import CloseCode 15 | 16 | 17 | CONNECTIONS = {} 18 | 19 | 20 | def get_content_types(user): 21 | """Return the set of IDs of content types visible by user.""" 22 | # This does only three database queries because Django caches 23 | # all permissions on the first call to user.has_perm(...). 24 | return { 25 | ct.id 26 | for ct in ContentType.objects.all() 27 | if user.has_perm(f"{ct.app_label}.view_{ct.model}") 28 | or user.has_perm(f"{ct.app_label}.change_{ct.model}") 29 | } 30 | 31 | 32 | async def handler(websocket): 33 | """Authenticate user and register connection in CONNECTIONS.""" 34 | sesame = await websocket.recv() 35 | user = await asyncio.to_thread(get_user, sesame) 36 | if user is None: 37 | await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") 38 | return 39 | 40 | ct_ids = await asyncio.to_thread(get_content_types, user) 41 | CONNECTIONS[websocket] = {"content_type_ids": ct_ids} 42 | try: 43 | await websocket.wait_closed() 44 | finally: 45 | del CONNECTIONS[websocket] 46 | 47 | 48 | async def process_events(): 49 | """Listen to events in Redis and process them.""" 50 | redis = aioredis.from_url("redis://127.0.0.1:6379/1") 51 | pubsub = redis.pubsub() 52 | await pubsub.subscribe("events") 53 | async for message in pubsub.listen(): 54 | if message["type"] != "message": 55 | continue 56 | payload = message["data"].decode() 57 | # Broadcast event to all users who have permissions to see it. 58 | event = json.loads(payload) 59 | recipients = ( 60 | websocket 61 | for websocket, connection in CONNECTIONS.items() 62 | if event["content_type_id"] in connection["content_type_ids"] 63 | ) 64 | broadcast(recipients, payload) 65 | 66 | 67 | async def main(): 68 | async with serve(handler, "localhost", 8888): 69 | await process_events() # runs forever 70 | 71 | 72 | if __name__ == "__main__": 73 | asyncio.run(main()) 74 | -------------------------------------------------------------------------------- /example/django/signals.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib.admin.models import LogEntry 4 | from django.db.models.signals import post_save 5 | from django.dispatch import receiver 6 | 7 | from django_redis import get_redis_connection 8 | 9 | 10 | @receiver(post_save, sender=LogEntry) 11 | def publish_event(instance, **kwargs): 12 | event = { 13 | "model": instance.content_type.name, 14 | "object": instance.object_repr, 15 | "message": instance.get_change_message(), 16 | "timestamp": instance.action_time.isoformat(), 17 | "user": str(instance.user), 18 | "content_type_id": instance.content_type_id, 19 | "object_id": instance.object_id, 20 | } 21 | connection = get_redis_connection("default") 22 | payload = json.dumps(event) 23 | connection.publish("events", payload) 24 | -------------------------------------------------------------------------------- /example/faq/health_check_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | from http import HTTPStatus 5 | from websockets.asyncio.server import serve 6 | 7 | def health_check(connection, request): 8 | if request.path == "/healthz": 9 | return connection.respond(HTTPStatus.OK, "OK\n") 10 | 11 | async def echo(websocket): 12 | async for message in websocket: 13 | await websocket.send(message) 14 | 15 | async def main(): 16 | async with serve(echo, "localhost", 8765, process_request=health_check) as server: 17 | await server.serve_forever() 18 | 19 | asyncio.run(main()) 20 | -------------------------------------------------------------------------------- /example/faq/shutdown_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import signal 5 | 6 | from websockets.asyncio.client import connect 7 | 8 | async def client(): 9 | async with connect("ws://localhost:8765") as websocket: 10 | # Close the connection when receiving SIGTERM. 11 | loop = asyncio.get_running_loop() 12 | loop.add_signal_handler(signal.SIGTERM, loop.create_task, websocket.close()) 13 | 14 | # Process messages received on the connection. 15 | async for message in websocket: 16 | ... 17 | 18 | asyncio.run(client()) 19 | -------------------------------------------------------------------------------- /example/faq/shutdown_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import signal 5 | 6 | from websockets.asyncio.server import serve 7 | 8 | async def handler(websocket): 9 | async for message in websocket: 10 | ... 11 | 12 | async def server(): 13 | async with serve(handler, "localhost", 8765) as server: 14 | # Close the server when receiving SIGTERM. 15 | loop = asyncio.get_running_loop() 16 | loop.add_signal_handler(signal.SIGTERM, server.close) 17 | await server.wait_closed() 18 | 19 | asyncio.run(server()) 20 | -------------------------------------------------------------------------------- /example/legacy/basic_auth_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # WS client example with HTTP Basic Authentication 4 | 5 | import asyncio 6 | 7 | from websockets.legacy.client import connect 8 | 9 | async def hello(): 10 | uri = "ws://mary:p@ssw0rd@localhost:8765" 11 | async with connect(uri) as websocket: 12 | greeting = await websocket.recv() 13 | print(greeting) 14 | 15 | asyncio.run(hello()) 16 | -------------------------------------------------------------------------------- /example/legacy/basic_auth_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Server example with HTTP Basic Authentication over TLS 4 | 5 | import asyncio 6 | 7 | from websockets.legacy.auth import basic_auth_protocol_factory 8 | from websockets.legacy.server import serve 9 | 10 | async def hello(websocket): 11 | greeting = f"Hello {websocket.username}!" 12 | await websocket.send(greeting) 13 | 14 | async def main(): 15 | async with serve( 16 | hello, "localhost", 8765, 17 | create_protocol=basic_auth_protocol_factory( 18 | realm="example", credentials=("mary", "p@ssw0rd") 19 | ), 20 | ): 21 | await asyncio.get_running_loop().create_future() # run forever 22 | 23 | asyncio.run(main()) 24 | -------------------------------------------------------------------------------- /example/legacy/unix_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # WS client example connecting to a Unix socket 4 | 5 | import asyncio 6 | import os.path 7 | 8 | from websockets.legacy.client import unix_connect 9 | 10 | async def hello(): 11 | socket_path = os.path.join(os.path.dirname(__file__), "socket") 12 | async with unix_connect(socket_path) as websocket: 13 | name = input("What's your name? ") 14 | await websocket.send(name) 15 | print(f">>> {name}") 16 | 17 | greeting = await websocket.recv() 18 | print(f"<<< {greeting}") 19 | 20 | asyncio.run(hello()) 21 | -------------------------------------------------------------------------------- /example/legacy/unix_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # WS server example listening on a Unix socket 4 | 5 | import asyncio 6 | import os.path 7 | 8 | from websockets.legacy.server import unix_serve 9 | 10 | async def hello(websocket): 11 | name = await websocket.recv() 12 | print(f"<<< {name}") 13 | 14 | greeting = f"Hello {name}!" 15 | 16 | await websocket.send(greeting) 17 | print(f">>> {greeting}") 18 | 19 | async def main(): 20 | socket_path = os.path.join(os.path.dirname(__file__), "socket") 21 | async with unix_serve(hello, socket_path): 22 | await asyncio.get_running_loop().create_future() # run forever 23 | 24 | asyncio.run(main()) 25 | -------------------------------------------------------------------------------- /example/quick/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from websockets.sync.client import connect 4 | 5 | def hello(): 6 | uri = "ws://localhost:8765" 7 | with connect(uri) as websocket: 8 | name = input("What's your name? ") 9 | 10 | websocket.send(name) 11 | print(f">>> {name}") 12 | 13 | greeting = websocket.recv() 14 | print(f"<<< {greeting}") 15 | 16 | if __name__ == "__main__": 17 | hello() 18 | -------------------------------------------------------------------------------- /example/quick/counter.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Courier New", sans-serif; 3 | text-align: center; 4 | } 5 | .buttons { 6 | font-size: 4em; 7 | display: flex; 8 | justify-content: center; 9 | } 10 | .button, .value { 11 | line-height: 1; 12 | padding: 2rem; 13 | margin: 2rem; 14 | border: medium solid; 15 | min-height: 1em; 16 | min-width: 1em; 17 | } 18 | .button { 19 | cursor: pointer; 20 | user-select: none; 21 | } 22 | .minus { 23 | color: red; 24 | } 25 | .plus { 26 | color: green; 27 | } 28 | .value { 29 | min-width: 2em; 30 | } 31 | .state { 32 | font-size: 2em; 33 | } 34 | -------------------------------------------------------------------------------- /example/quick/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket demo 5 | 6 | 7 | 8 |
9 |
-
10 |
?
11 |
+
12 |
13 |
14 | ? online 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/quick/counter.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", () => { 2 | const websocket = new WebSocket("ws://localhost:6789/"); 3 | 4 | document.querySelector(".minus").addEventListener("click", () => { 5 | websocket.send(JSON.stringify({ action: "minus" })); 6 | }); 7 | 8 | document.querySelector(".plus").addEventListener("click", () => { 9 | websocket.send(JSON.stringify({ action: "plus" })); 10 | }); 11 | 12 | websocket.onmessage = ({ data }) => { 13 | const event = JSON.parse(data); 14 | switch (event.type) { 15 | case "value": 16 | document.querySelector(".value").textContent = event.value; 17 | break; 18 | case "users": 19 | const users = `${event.count} user${event.count == 1 ? "" : "s"}`; 20 | document.querySelector(".users").textContent = users; 21 | break; 22 | default: 23 | console.error("unsupported event", event); 24 | } 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /example/quick/counter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import json 5 | import logging 6 | 7 | from websockets.asyncio.server import broadcast, serve 8 | 9 | logging.basicConfig() 10 | 11 | USERS = set() 12 | 13 | VALUE = 0 14 | 15 | def users_event(): 16 | return json.dumps({"type": "users", "count": len(USERS)}) 17 | 18 | def value_event(): 19 | return json.dumps({"type": "value", "value": VALUE}) 20 | 21 | async def counter(websocket): 22 | global USERS, VALUE 23 | try: 24 | # Register user 25 | USERS.add(websocket) 26 | broadcast(USERS, users_event()) 27 | # Send current state to user 28 | await websocket.send(value_event()) 29 | # Manage state changes 30 | async for message in websocket: 31 | event = json.loads(message) 32 | if event["action"] == "minus": 33 | VALUE -= 1 34 | broadcast(USERS, value_event()) 35 | elif event["action"] == "plus": 36 | VALUE += 1 37 | broadcast(USERS, value_event()) 38 | else: 39 | logging.error("unsupported event: %s", event) 40 | finally: 41 | # Unregister user 42 | USERS.remove(websocket) 43 | broadcast(USERS, users_event()) 44 | 45 | async def main(): 46 | async with serve(counter, "localhost", 6789) as server: 47 | await server.serve_forever() 48 | 49 | if __name__ == "__main__": 50 | asyncio.run(main()) 51 | -------------------------------------------------------------------------------- /example/quick/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | 5 | from websockets.asyncio.server import serve 6 | 7 | async def hello(websocket): 8 | name = await websocket.recv() 9 | print(f"<<< {name}") 10 | 11 | greeting = f"Hello {name}!" 12 | 13 | await websocket.send(greeting) 14 | print(f">>> {greeting}") 15 | 16 | async def main(): 17 | async with serve(hello, "localhost", 8765) as server: 18 | await server.serve_forever() 19 | 20 | if __name__ == "__main__": 21 | asyncio.run(main()) 22 | -------------------------------------------------------------------------------- /example/quick/show_time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket demo 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/quick/show_time.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", () => { 2 | const messages = document.createElement("ul"); 3 | document.body.appendChild(messages); 4 | 5 | const websocket = new WebSocket("ws://localhost:5678/"); 6 | websocket.onmessage = ({ data }) => { 7 | const message = document.createElement("li"); 8 | const content = document.createTextNode(data); 9 | message.appendChild(content); 10 | messages.appendChild(message); 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /example/quick/show_time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import datetime 5 | import random 6 | 7 | from websockets.asyncio.server import serve 8 | 9 | async def show_time(websocket): 10 | while True: 11 | message = datetime.datetime.now(tz=datetime.timezone.utc).isoformat() 12 | await websocket.send(message) 13 | await asyncio.sleep(random.random() * 2 + 1) 14 | 15 | async def main(): 16 | async with serve(show_time, "localhost", 5678) as server: 17 | await server.serve_forever() 18 | 19 | if __name__ == "__main__": 20 | asyncio.run(main()) 21 | -------------------------------------------------------------------------------- /example/quick/sync_time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import datetime 5 | import random 6 | 7 | from websockets.asyncio.server import broadcast, serve 8 | 9 | async def noop(websocket): 10 | await websocket.wait_closed() 11 | 12 | async def show_time(server): 13 | while True: 14 | message = datetime.datetime.now(tz=datetime.timezone.utc).isoformat() 15 | broadcast(server.connections, message) 16 | await asyncio.sleep(random.random() * 2 + 1) 17 | 18 | async def main(): 19 | async with serve(noop, "localhost", 5678) as server: 20 | await show_time(server) 21 | 22 | if __name__ == "__main__": 23 | asyncio.run(main()) 24 | -------------------------------------------------------------------------------- /example/ruff.toml: -------------------------------------------------------------------------------- 1 | [lint.isort] 2 | no-sections = true 3 | -------------------------------------------------------------------------------- /example/sync/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Client example using the threading API.""" 4 | 5 | from websockets.sync.client import connect 6 | 7 | 8 | def hello(): 9 | with connect("ws://localhost:8765") as websocket: 10 | name = input("What's your name? ") 11 | 12 | websocket.send(name) 13 | print(f">>> {name}") 14 | 15 | greeting = websocket.recv() 16 | print(f"<<< {greeting}") 17 | 18 | 19 | if __name__ == "__main__": 20 | hello() 21 | -------------------------------------------------------------------------------- /example/sync/echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Echo server using the threading API.""" 4 | 5 | from websockets.sync.server import serve 6 | 7 | 8 | def echo(websocket): 9 | for message in websocket: 10 | websocket.send(message) 11 | 12 | 13 | def main(): 14 | with serve(echo, "localhost", 8765) as server: 15 | server.serve_forever() 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /example/sync/hello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Client using the threading API.""" 4 | 5 | from websockets.sync.client import connect 6 | 7 | 8 | def hello(): 9 | with connect("ws://localhost:8765") as websocket: 10 | websocket.send("Hello world!") 11 | message = websocket.recv() 12 | print(message) 13 | 14 | 15 | if __name__ == "__main__": 16 | hello() 17 | -------------------------------------------------------------------------------- /example/sync/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Server example using the threading API.""" 4 | 5 | from websockets.sync.server import serve 6 | 7 | 8 | def hello(websocket): 9 | name = websocket.recv() 10 | print(f"<<< {name}") 11 | 12 | greeting = f"Hello {name}!" 13 | 14 | websocket.send(greeting) 15 | print(f">>> {greeting}") 16 | 17 | 18 | def main(): 19 | with serve(hello, "localhost", 8765) as server: 20 | server.serve_forever() 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /example/tls/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pathlib 4 | import ssl 5 | 6 | from websockets.sync.client import connect 7 | 8 | ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 9 | localhost_pem = pathlib.Path(__file__).with_name("localhost.pem") 10 | ssl_context.load_verify_locations(localhost_pem) 11 | 12 | def hello(): 13 | uri = "wss://localhost:8765" 14 | with connect(uri, ssl=ssl_context) as websocket: 15 | name = input("What's your name? ") 16 | 17 | websocket.send(name) 18 | print(f">>> {name}") 19 | 20 | greeting = websocket.recv() 21 | print(f"<<< {greeting}") 22 | 23 | if __name__ == "__main__": 24 | hello() 25 | -------------------------------------------------------------------------------- /example/tls/localhost.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDG8iDak4UBpurI 3 | TWjSfqJ0YVG/S56nhswehupCaIzu0xQ8wqPSs36h5t1jMexJPZfvwyvFjcV+hYpj 4 | LMM0wMJPx9oBQEe0bsmlC66e8aF0UpSQw1aVfYoxA9BejgEyrFNE7cRbQNYFEb/5 5 | 3HfqZKdEQA2fgQSlZ0RTRmLrD+l72iO5o2xl5bttXpqYZB2XOkyO79j/xWdu9zFE 6 | sgZJ5ysWbqoRAGgnxjdYYr9DARd8bIE/hN3SW7mDt5v4LqCIhGn1VmrwtT3d5AuG 7 | QPz4YEbm0t6GOlmFjIMYH5Y7pALRVfoJKRj6DGNIR1JicL+wqLV66kcVnj8WKbla 8 | 20i7fR7NAgMBAAECggEAG5yvgqbG5xvLqlFUIyMAWTbIqcxNEONcoUAIc38fUGZr 9 | gKNjKXNQOBha0dG0AdZSqCxmftzWdGEEfA9SaJf4YCpUz6ekTB60Tfv5GIZg6kwr 10 | 4ou6ELWD4Jmu6fC7qdTRGdgGUMQG8F0uT/eRjS67KHXbbi/x/SMAEK7MO+PRfCbj 11 | +JGzS9Ym9mUweINPotgjHdDGwwd039VWYS+9A+QuNK27p3zq4hrWRb4wshSC8fKy 12 | oLoe4OQt81aowpX9k6mAU6N8vOmP8/EcQHYC+yFIIDZB2EmDP07R1LUEH3KJnzo7 13 | plCK1/kYPhX0a05cEdTpXdKa74AlvSRkS11sGqfUAQKBgQDj1SRv0AUGsHSA0LWx 14 | a0NT1ZLEXCG0uqgdgh0sTqIeirQsPROw3ky4lH5MbjkfReArFkhHu3M6KoywEPxE 15 | wanSRh/t1qcNjNNZUvFoUzAKVpb33RLkJppOTVEWPt+wtyDlfz1ZAXzMV66tACrx 16 | H2a3v0ZWUz6J+x/dESH5TTNL4QKBgQDfirmknp408pwBE+bulngKy0QvU09En8H0 17 | uvqr8q4jCXqJ1tXon4wsHg2yF4Fa37SCpSmvONIDwJvVWkkYLyBHKOns/fWCkW3n 18 | hIcYx0q2jgcoOLU0uoaM9ArRXhIxoWqV/KGkQzN+3xXC1/MxZ5OhyxBxfPCPIYIN 19 | YN3M1t/QbQKBgDImhsC+D30rdlmsl3IYZFed2ZKznQ/FTqBANd+8517FtWdPgnga 20 | VtUCitKUKKrDnNafLwXrMzAIkbNn6b/QyWrp2Lln2JnY9+TfpxgJx7de3BhvZ2sl 21 | PC4kQsccy+yAQxOBcKWY+Dmay251bP5qpRepWPhDlq6UwqzMyqev4KzBAoGAWDMi 22 | IEO9ZGK9DufNXCHeZ1PgKVQTmJ34JxmHQkTUVFqvEKfFaq1Y3ydUfAouLa7KSCnm 23 | ko42vuhGFB41bOdbMvh/o9RoBAZheNGfhDVN002ioUoOpSlbYU4A3q7hOtfXeCpf 24 | lLI3JT3cFi6ic8HMTDAU4tJLEA5GhATOPr4hPNkCgYB8jTYGcLvoeFaLEveg0kS2 25 | cz6ZXGLJx5m1AOQy5g9FwGaW+10lr8TF2k3AldwoiwX0R6sHAf/945aGU83ms5v9 26 | PB9/x66AYtSRUos9MwB4y1ur4g6FiXZUBgTJUqzz2nehPCyGjYhh49WucjszqcjX 27 | chS1bKZOY+1knWq8xj5Qyg== 28 | -----END PRIVATE KEY----- 29 | -----BEGIN CERTIFICATE----- 30 | MIIDTTCCAjWgAwIBAgIJAOjte6l+03jvMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV 31 | BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp 32 | bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MDUwNTE2NTkyOVoYDzIwNjAwNTA0 33 | MTY1OTI5WjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM 34 | EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI 35 | hvcNAQEBBQADggEPADCCAQoCggEBAMbyINqThQGm6shNaNJ+onRhUb9LnqeGzB6G 36 | 6kJojO7TFDzCo9KzfqHm3WMx7Ek9l+/DK8WNxX6FimMswzTAwk/H2gFAR7RuyaUL 37 | rp7xoXRSlJDDVpV9ijED0F6OATKsU0TtxFtA1gURv/ncd+pkp0RADZ+BBKVnRFNG 38 | YusP6XvaI7mjbGXlu21emphkHZc6TI7v2P/FZ273MUSyBknnKxZuqhEAaCfGN1hi 39 | v0MBF3xsgT+E3dJbuYO3m/guoIiEafVWavC1Pd3kC4ZA/PhgRubS3oY6WYWMgxgf 40 | ljukAtFV+gkpGPoMY0hHUmJwv7CotXrqRxWePxYpuVrbSLt9Hs0CAwEAAaMwMC4w 41 | LAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G 42 | CSqGSIb3DQEBCwUAA4IBAQC9TsTxTEvqHPUS6sfvF77eG0D6HLOONVN91J+L7LiX 43 | v3bFeS1xbUS6/wIxZi5EnAt/te5vaHk/5Q1UvznQP4j2gNoM6lH/DRkSARvRitVc 44 | H0qN4Xp2Yk1R9VEx4ZgArcyMpI+GhE4vJRx1LE/hsuAzw7BAdsTt9zicscNg2fxO 45 | 3ao/eBcdaC6n9aFYdE6CADMpB1lCX2oWNVdj6IavQLu7VMc+WJ3RKncwC9th+5OP 46 | ISPvkVZWf25rR2STmvvb0qEm3CZjk4Xd7N+gxbKKUvzEgPjrLSWzKKJAWHjCLugI 47 | /kQqhpjWVlTbtKzWz5bViqCjSbrIPpU2MgG9AUV9y3iV 48 | -----END CERTIFICATE----- 49 | -------------------------------------------------------------------------------- /example/tls/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import pathlib 5 | import ssl 6 | 7 | from websockets.asyncio.server import serve 8 | 9 | async def hello(websocket): 10 | name = await websocket.recv() 11 | print(f"<<< {name}") 12 | 13 | greeting = f"Hello {name}!" 14 | 15 | await websocket.send(greeting) 16 | print(f">>> {greeting}") 17 | 18 | ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 19 | localhost_pem = pathlib.Path(__file__).with_name("localhost.pem") 20 | ssl_context.load_cert_chain(localhost_pem) 21 | 22 | async def main(): 23 | async with serve(hello, "localhost", 8765, ssl=ssl_context) as server: 24 | await server.serve_forever() 25 | 26 | if __name__ == "__main__": 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /example/tutorial/start/connect4.css: -------------------------------------------------------------------------------- 1 | /* General layout */ 2 | 3 | body { 4 | background-color: white; 5 | display: flex; 6 | flex-direction: column-reverse; 7 | justify-content: center; 8 | align-items: center; 9 | margin: 0; 10 | min-height: 100vh; 11 | } 12 | 13 | /* Action buttons */ 14 | 15 | .actions { 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: space-evenly; 19 | align-items: flex-end; 20 | width: 720px; 21 | height: 100px; 22 | } 23 | 24 | .action { 25 | color: darkgray; 26 | font-family: "Helvetica Neue", sans-serif; 27 | font-size: 20px; 28 | line-height: 20px; 29 | font-weight: 300; 30 | text-align: center; 31 | text-decoration: none; 32 | text-transform: uppercase; 33 | padding: 20px; 34 | width: 120px; 35 | } 36 | 37 | .action:hover { 38 | background-color: darkgray; 39 | color: white; 40 | font-weight: 700; 41 | } 42 | 43 | .action[href=""] { 44 | display: none; 45 | } 46 | 47 | /* Connect Four board */ 48 | 49 | .board { 50 | background-color: blue; 51 | display: flex; 52 | flex-direction: row; 53 | padding: 0 10px; 54 | position: relative; 55 | } 56 | 57 | .board::before, 58 | .board::after { 59 | background-color: blue; 60 | content: ""; 61 | height: 720px; 62 | width: 20px; 63 | position: absolute; 64 | } 65 | 66 | .board::before { 67 | left: -20px; 68 | } 69 | 70 | .board::after { 71 | right: -20px; 72 | } 73 | 74 | .column { 75 | display: flex; 76 | flex-direction: column-reverse; 77 | padding: 10px; 78 | } 79 | 80 | .cell { 81 | border-radius: 50%; 82 | width: 80px; 83 | height: 80px; 84 | margin: 10px 0; 85 | } 86 | 87 | .empty { 88 | background-color: white; 89 | } 90 | 91 | .column:hover .empty { 92 | background-color: lightgray; 93 | } 94 | 95 | .column:hover .empty ~ .empty { 96 | background-color: white; 97 | } 98 | 99 | .red { 100 | background-color: red; 101 | } 102 | 103 | .yellow { 104 | background-color: yellow; 105 | } 106 | -------------------------------------------------------------------------------- /example/tutorial/start/connect4.js: -------------------------------------------------------------------------------- 1 | const PLAYER1 = "red"; 2 | 3 | const PLAYER2 = "yellow"; 4 | 5 | function createBoard(board) { 6 | // Inject stylesheet. 7 | const linkElement = document.createElement("link"); 8 | linkElement.href = import.meta.url.replace(".js", ".css"); 9 | linkElement.rel = "stylesheet"; 10 | document.head.append(linkElement); 11 | // Generate board. 12 | for (let column = 0; column < 7; column++) { 13 | const columnElement = document.createElement("div"); 14 | columnElement.className = "column"; 15 | columnElement.dataset.column = column; 16 | for (let row = 0; row < 6; row++) { 17 | const cellElement = document.createElement("div"); 18 | cellElement.className = "cell empty"; 19 | cellElement.dataset.column = column; 20 | columnElement.append(cellElement); 21 | } 22 | board.append(columnElement); 23 | } 24 | } 25 | 26 | function playMove(board, player, column, row) { 27 | // Check values of arguments. 28 | if (player !== PLAYER1 && player !== PLAYER2) { 29 | throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`); 30 | } 31 | const columnElement = board.querySelectorAll(".column")[column]; 32 | if (columnElement === undefined) { 33 | throw new RangeError("column must be between 0 and 6."); 34 | } 35 | const cellElement = columnElement.querySelectorAll(".cell")[row]; 36 | if (cellElement === undefined) { 37 | throw new RangeError("row must be between 0 and 5."); 38 | } 39 | // Place checker in cell. 40 | if (!cellElement.classList.replace("empty", player)) { 41 | throw new Error("cell must be empty."); 42 | } 43 | } 44 | 45 | export { PLAYER1, PLAYER2, createBoard, playMove }; 46 | -------------------------------------------------------------------------------- /example/tutorial/start/connect4.py: -------------------------------------------------------------------------------- 1 | __all__ = ["PLAYER1", "PLAYER2", "Connect4"] 2 | 3 | PLAYER1, PLAYER2 = "red", "yellow" 4 | 5 | 6 | class Connect4: 7 | """ 8 | A Connect Four game. 9 | 10 | Play moves with :meth:`play`. 11 | 12 | Get past moves with :attr:`moves`. 13 | 14 | Check for a victory with :attr:`winner`. 15 | 16 | """ 17 | 18 | def __init__(self): 19 | self.moves = [] 20 | self.top = [0 for _ in range(7)] 21 | self.winner = None 22 | 23 | @property 24 | def last_player(self): 25 | """ 26 | Player who played the last move. 27 | 28 | """ 29 | return PLAYER1 if len(self.moves) % 2 else PLAYER2 30 | 31 | @property 32 | def last_player_won(self): 33 | """ 34 | Whether the last move is winning. 35 | 36 | """ 37 | b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2]) 38 | return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9]) 39 | 40 | def play(self, player, column): 41 | """ 42 | Play a move in a column. 43 | 44 | Returns the row where the checker lands. 45 | 46 | Raises :exc:`ValueError` if the move is illegal. 47 | 48 | """ 49 | if player == self.last_player: 50 | raise ValueError("It isn't your turn.") 51 | 52 | row = self.top[column] 53 | if row == 6: 54 | raise ValueError("This slot is full.") 55 | 56 | self.moves.append((player, column, row)) 57 | self.top[column] += 1 58 | 59 | if self.winner is None and self.last_player_won: 60 | self.winner = self.last_player 61 | 62 | return row 63 | -------------------------------------------------------------------------------- /example/tutorial/start/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/example/tutorial/start/favicon.ico -------------------------------------------------------------------------------- /example/tutorial/step1/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import itertools 5 | import json 6 | 7 | from websockets.asyncio.server import serve 8 | 9 | from connect4 import PLAYER1, PLAYER2, Connect4 10 | 11 | 12 | async def handler(websocket): 13 | # Initialize a Connect Four game. 14 | game = Connect4() 15 | 16 | # Players take alternate turns, using the same browser. 17 | turns = itertools.cycle([PLAYER1, PLAYER2]) 18 | player = next(turns) 19 | 20 | async for message in websocket: 21 | # Parse a "play" event from the UI. 22 | event = json.loads(message) 23 | assert event["type"] == "play" 24 | column = event["column"] 25 | 26 | try: 27 | # Play the move. 28 | row = game.play(player, column) 29 | except ValueError as exc: 30 | # Send an "error" event if the move was illegal. 31 | event = { 32 | "type": "error", 33 | "message": str(exc), 34 | } 35 | await websocket.send(json.dumps(event)) 36 | continue 37 | 38 | # Send a "play" event to update the UI. 39 | event = { 40 | "type": "play", 41 | "player": player, 42 | "column": column, 43 | "row": row, 44 | } 45 | await websocket.send(json.dumps(event)) 46 | 47 | # If move is winning, send a "win" event. 48 | if game.winner is not None: 49 | event = { 50 | "type": "win", 51 | "player": game.winner, 52 | } 53 | await websocket.send(json.dumps(event)) 54 | 55 | # Alternate turns. 56 | player = next(turns) 57 | 58 | 59 | async def main(): 60 | async with serve(handler, "", 8001) as server: 61 | await server.serve_forever() 62 | 63 | 64 | if __name__ == "__main__": 65 | asyncio.run(main()) 66 | -------------------------------------------------------------------------------- /example/tutorial/step1/connect4.css: -------------------------------------------------------------------------------- 1 | ../start/connect4.css -------------------------------------------------------------------------------- /example/tutorial/step1/connect4.js: -------------------------------------------------------------------------------- 1 | ../start/connect4.js -------------------------------------------------------------------------------- /example/tutorial/step1/connect4.py: -------------------------------------------------------------------------------- 1 | ../start/connect4.py -------------------------------------------------------------------------------- /example/tutorial/step1/favicon.ico: -------------------------------------------------------------------------------- 1 | ../../../logo/favicon.ico -------------------------------------------------------------------------------- /example/tutorial/step1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Connect Four 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/tutorial/step1/main.js: -------------------------------------------------------------------------------- 1 | import { createBoard, playMove } from "./connect4.js"; 2 | 3 | function showMessage(message) { 4 | window.setTimeout(() => window.alert(message), 50); 5 | } 6 | 7 | function receiveMoves(board, websocket) { 8 | websocket.addEventListener("message", ({ data }) => { 9 | const event = JSON.parse(data); 10 | switch (event.type) { 11 | case "play": 12 | // Update the UI with the move. 13 | playMove(board, event.player, event.column, event.row); 14 | break; 15 | case "win": 16 | showMessage(`Player ${event.player} wins!`); 17 | // No further messages are expected; close the WebSocket connection. 18 | websocket.close(1000); 19 | break; 20 | case "error": 21 | showMessage(event.message); 22 | break; 23 | default: 24 | throw new Error(`Unsupported event type: ${event.type}.`); 25 | } 26 | }); 27 | } 28 | 29 | function sendMoves(board, websocket) { 30 | // When clicking a column, send a "play" event for a move in that column. 31 | board.addEventListener("click", ({ target }) => { 32 | const column = target.dataset.column; 33 | // Ignore clicks outside a column. 34 | if (column === undefined) { 35 | return; 36 | } 37 | const event = { 38 | type: "play", 39 | column: parseInt(column, 10), 40 | }; 41 | websocket.send(JSON.stringify(event)); 42 | }); 43 | } 44 | 45 | window.addEventListener("DOMContentLoaded", () => { 46 | // Initialize the UI. 47 | const board = document.querySelector(".board"); 48 | createBoard(board); 49 | // Open the WebSocket connection and register event handlers. 50 | const websocket = new WebSocket("ws://localhost:8001/"); 51 | receiveMoves(board, websocket); 52 | sendMoves(board, websocket); 53 | }); 54 | -------------------------------------------------------------------------------- /example/tutorial/step2/connect4.css: -------------------------------------------------------------------------------- 1 | ../start/connect4.css -------------------------------------------------------------------------------- /example/tutorial/step2/connect4.js: -------------------------------------------------------------------------------- 1 | ../start/connect4.js -------------------------------------------------------------------------------- /example/tutorial/step2/connect4.py: -------------------------------------------------------------------------------- 1 | ../start/connect4.py -------------------------------------------------------------------------------- /example/tutorial/step2/favicon.ico: -------------------------------------------------------------------------------- 1 | ../../../logo/favicon.ico -------------------------------------------------------------------------------- /example/tutorial/step2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Connect Four 5 | 6 | 7 |
8 | New 9 | Join 10 | Watch 11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/tutorial/step2/main.js: -------------------------------------------------------------------------------- 1 | import { createBoard, playMove } from "./connect4.js"; 2 | 3 | function initGame(websocket) { 4 | websocket.addEventListener("open", () => { 5 | // Send an "init" event according to who is connecting. 6 | const params = new URLSearchParams(window.location.search); 7 | let event = { type: "init" }; 8 | if (params.has("join")) { 9 | // Second player joins an existing game. 10 | event.join = params.get("join"); 11 | } else if (params.has("watch")) { 12 | // Spectator watches an existing game. 13 | event.watch = params.get("watch"); 14 | } else { 15 | // First player starts a new game. 16 | } 17 | websocket.send(JSON.stringify(event)); 18 | }); 19 | } 20 | 21 | function showMessage(message) { 22 | window.setTimeout(() => window.alert(message), 50); 23 | } 24 | 25 | function receiveMoves(board, websocket) { 26 | websocket.addEventListener("message", ({ data }) => { 27 | const event = JSON.parse(data); 28 | switch (event.type) { 29 | case "init": 30 | // Create links for inviting the second player and spectators. 31 | document.querySelector(".join").href = "?join=" + event.join; 32 | document.querySelector(".watch").href = "?watch=" + event.watch; 33 | break; 34 | case "play": 35 | // Update the UI with the move. 36 | playMove(board, event.player, event.column, event.row); 37 | break; 38 | case "win": 39 | showMessage(`Player ${event.player} wins!`); 40 | // No further messages are expected; close the WebSocket connection. 41 | websocket.close(1000); 42 | break; 43 | case "error": 44 | showMessage(event.message); 45 | break; 46 | default: 47 | throw new Error(`Unsupported event type: ${event.type}.`); 48 | } 49 | }); 50 | } 51 | 52 | function sendMoves(board, websocket) { 53 | // Don't send moves for a spectator watching a game. 54 | const params = new URLSearchParams(window.location.search); 55 | if (params.has("watch")) { 56 | return; 57 | } 58 | 59 | // When clicking a column, send a "play" event for a move in that column. 60 | board.addEventListener("click", ({ target }) => { 61 | const column = target.dataset.column; 62 | // Ignore clicks outside a column. 63 | if (column === undefined) { 64 | return; 65 | } 66 | const event = { 67 | type: "play", 68 | column: parseInt(column, 10), 69 | }; 70 | websocket.send(JSON.stringify(event)); 71 | }); 72 | } 73 | 74 | window.addEventListener("DOMContentLoaded", () => { 75 | // Initialize the UI. 76 | const board = document.querySelector(".board"); 77 | createBoard(board); 78 | // Open the WebSocket connection and register event handlers. 79 | const websocket = new WebSocket("ws://localhost:8001/"); 80 | initGame(websocket); 81 | receiveMoves(board, websocket); 82 | sendMoves(board, websocket); 83 | }); 84 | -------------------------------------------------------------------------------- /example/tutorial/step3/Procfile: -------------------------------------------------------------------------------- 1 | web: python app.py 2 | -------------------------------------------------------------------------------- /example/tutorial/step3/connect4.css: -------------------------------------------------------------------------------- 1 | ../start/connect4.css -------------------------------------------------------------------------------- /example/tutorial/step3/connect4.js: -------------------------------------------------------------------------------- 1 | ../start/connect4.js -------------------------------------------------------------------------------- /example/tutorial/step3/connect4.py: -------------------------------------------------------------------------------- 1 | ../start/connect4.py -------------------------------------------------------------------------------- /example/tutorial/step3/favicon.ico: -------------------------------------------------------------------------------- 1 | ../../../logo/favicon.ico -------------------------------------------------------------------------------- /example/tutorial/step3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Connect Four 5 | 6 | 7 |
8 | New 9 | Join 10 | Watch 11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/tutorial/step3/main.js: -------------------------------------------------------------------------------- 1 | import { createBoard, playMove } from "./connect4.js"; 2 | 3 | function getWebSocketServer() { 4 | if (window.location.host === "python-websockets.github.io") { 5 | return "wss://websockets-tutorial.koyeb.app/"; 6 | } else if (window.location.host === "localhost:8000") { 7 | return "ws://localhost:8001/"; 8 | } else { 9 | throw new Error(`Unsupported host: ${window.location.host}`); 10 | } 11 | } 12 | 13 | function initGame(websocket) { 14 | websocket.addEventListener("open", () => { 15 | // Send an "init" event according to who is connecting. 16 | const params = new URLSearchParams(window.location.search); 17 | let event = { type: "init" }; 18 | if (params.has("join")) { 19 | // Second player joins an existing game. 20 | event.join = params.get("join"); 21 | } else if (params.has("watch")) { 22 | // Spectator watches an existing game. 23 | event.watch = params.get("watch"); 24 | } else { 25 | // First player starts a new game. 26 | } 27 | websocket.send(JSON.stringify(event)); 28 | }); 29 | } 30 | 31 | function showMessage(message) { 32 | window.setTimeout(() => window.alert(message), 50); 33 | } 34 | 35 | function receiveMoves(board, websocket) { 36 | websocket.addEventListener("message", ({ data }) => { 37 | const event = JSON.parse(data); 38 | switch (event.type) { 39 | case "init": 40 | // Create links for inviting the second player and spectators. 41 | document.querySelector(".join").href = "?join=" + event.join; 42 | document.querySelector(".watch").href = "?watch=" + event.watch; 43 | break; 44 | case "play": 45 | // Update the UI with the move. 46 | playMove(board, event.player, event.column, event.row); 47 | break; 48 | case "win": 49 | showMessage(`Player ${event.player} wins!`); 50 | // No further messages are expected; close the WebSocket connection. 51 | websocket.close(1000); 52 | break; 53 | case "error": 54 | showMessage(event.message); 55 | break; 56 | default: 57 | throw new Error(`Unsupported event type: ${event.type}.`); 58 | } 59 | }); 60 | } 61 | 62 | function sendMoves(board, websocket) { 63 | // Don't send moves for a spectator watching a game. 64 | const params = new URLSearchParams(window.location.search); 65 | if (params.has("watch")) { 66 | return; 67 | } 68 | 69 | // When clicking a column, send a "play" event for a move in that column. 70 | board.addEventListener("click", ({ target }) => { 71 | const column = target.dataset.column; 72 | // Ignore clicks outside a column. 73 | if (column === undefined) { 74 | return; 75 | } 76 | const event = { 77 | type: "play", 78 | column: parseInt(column, 10), 79 | }; 80 | websocket.send(JSON.stringify(event)); 81 | }); 82 | } 83 | 84 | window.addEventListener("DOMContentLoaded", () => { 85 | // Initialize the UI. 86 | const board = document.querySelector(".board"); 87 | createBoard(board); 88 | // Open the WebSocket connection and register event handlers. 89 | const websocket = new WebSocket(getWebSocketServer()); 90 | initGame(websocket); 91 | receiveMoves(board, websocket); 92 | sendMoves(board, websocket); 93 | }); 94 | -------------------------------------------------------------------------------- /example/tutorial/step3/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | -------------------------------------------------------------------------------- /experiments/authentication/cookie.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cookie | WebSocket Authentication 5 | 6 | 7 | 8 |

[??] Cookie

9 |

[OK] Cookie

10 |

[KO] Cookie

11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /experiments/authentication/cookie.js: -------------------------------------------------------------------------------- 1 | // send token to iframe 2 | window.addEventListener("DOMContentLoaded", () => { 3 | const iframe = document.querySelector("iframe"); 4 | iframe.addEventListener("load", () => { 5 | iframe.contentWindow.postMessage(token, "http://localhost:8003"); 6 | }); 7 | }); 8 | 9 | // once iframe has set cookie, open WebSocket connection 10 | window.addEventListener("message", ({ origin }) => { 11 | if (origin !== "http://localhost:8003") { 12 | return; 13 | } 14 | 15 | const websocket = new WebSocket("ws://localhost:8003/"); 16 | 17 | websocket.onmessage = ({ data }) => { 18 | // event.data is expected to be "Hello !" 19 | websocket.send(`Goodbye ${data.slice(6, -1)}.`); 20 | }; 21 | 22 | runTest(websocket); 23 | }); 24 | -------------------------------------------------------------------------------- /experiments/authentication/cookie_iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cookie iframe | WebSocket Authentication 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /experiments/authentication/cookie_iframe.js: -------------------------------------------------------------------------------- 1 | // receive token from the parent window, set cookie and notify parent 2 | window.addEventListener("message", ({ origin, data }) => { 3 | if (origin !== "http://localhost:8000") { 4 | return; 5 | } 6 | 7 | document.cookie = `token=${data}; SameSite=Strict`; 8 | window.parent.postMessage("", "http://localhost:8000"); 9 | }); 10 | -------------------------------------------------------------------------------- /experiments/authentication/favicon.ico: -------------------------------------------------------------------------------- 1 | ../../logo/favicon.ico -------------------------------------------------------------------------------- /experiments/authentication/first_message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | First message | WebSocket Authentication 5 | 6 | 7 | 8 |

[??] First message

9 |

[OK] First message

10 |

[KO] First message

11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /experiments/authentication/first_message.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", () => { 2 | const websocket = new WebSocket("ws://localhost:8001/"); 3 | websocket.onopen = () => websocket.send(token); 4 | 5 | websocket.onmessage = ({ data }) => { 6 | // event.data is expected to be "Hello !" 7 | websocket.send(`Goodbye ${data.slice(6, -1)}.`); 8 | }; 9 | 10 | runTest(websocket); 11 | }); 12 | -------------------------------------------------------------------------------- /experiments/authentication/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Authentication 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /experiments/authentication/query_param.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Query parameter | WebSocket Authentication 5 | 6 | 7 | 8 |

[??] Query parameter

9 |

[OK] Query parameter

10 |

[KO] Query parameter

11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /experiments/authentication/query_param.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", () => { 2 | const uri = `ws://localhost:8002/?token=${token}`; 3 | const websocket = new WebSocket(uri); 4 | 5 | websocket.onmessage = ({ data }) => { 6 | // event.data is expected to be "Hello !" 7 | websocket.send(`Goodbye ${data.slice(6, -1)}.`); 8 | }; 9 | 10 | runTest(websocket); 11 | }); 12 | -------------------------------------------------------------------------------- /experiments/authentication/script.js: -------------------------------------------------------------------------------- 1 | var token = window.parent.token, 2 | user = window.parent.user; 3 | 4 | function getExpectedEvents() { 5 | return [ 6 | { 7 | type: "open", 8 | }, 9 | { 10 | type: "message", 11 | data: `Hello ${user}!`, 12 | }, 13 | { 14 | type: "close", 15 | code: 1000, 16 | reason: "", 17 | wasClean: true, 18 | }, 19 | ]; 20 | } 21 | 22 | function isEqual(expected, actual) { 23 | // good enough for our purposes here! 24 | return JSON.stringify(expected) === JSON.stringify(actual); 25 | } 26 | 27 | function testStep(expected, actual) { 28 | if (isEqual(expected, actual)) { 29 | document.body.className = "ok"; 30 | } else if (isEqual(expected.slice(0, actual.length), actual)) { 31 | document.body.className = "test"; 32 | } else { 33 | document.body.className = "ko"; 34 | } 35 | } 36 | 37 | function runTest(websocket) { 38 | const expected = getExpectedEvents(); 39 | var actual = []; 40 | websocket.addEventListener("open", ({ type }) => { 41 | actual.push({ type }); 42 | testStep(expected, actual); 43 | }); 44 | websocket.addEventListener("message", ({ type, data }) => { 45 | actual.push({ type, data }); 46 | testStep(expected, actual); 47 | }); 48 | websocket.addEventListener("close", ({ type, code, reason, wasClean }) => { 49 | actual.push({ type, code, reason, wasClean }); 50 | testStep(expected, actual); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /experiments/authentication/style.css: -------------------------------------------------------------------------------- 1 | /* page layout */ 2 | 3 | body { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | margin: 0; 9 | height: 100vh; 10 | } 11 | div.title, iframe { 12 | width: 100vw; 13 | height: 20vh; 14 | border: none; 15 | } 16 | div.title { 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: center; 20 | align-items: center; 21 | } 22 | h1, p { 23 | margin: 0; 24 | width: 24em; 25 | } 26 | 27 | /* text style */ 28 | 29 | h1, input, p { 30 | font-family: monospace; 31 | font-size: 3em; 32 | } 33 | input { 34 | color: #333; 35 | border: 3px solid #999; 36 | padding: 1em; 37 | } 38 | input:focus { 39 | border-color: #333; 40 | outline: none; 41 | } 42 | input::placeholder { 43 | color: #999; 44 | opacity: 1; 45 | } 46 | 47 | /* test results */ 48 | 49 | body.test { 50 | background-color: #666; 51 | color: #fff; 52 | } 53 | body.ok { 54 | background-color: #090; 55 | color: #fff; 56 | } 57 | body.ko { 58 | background-color: #900; 59 | color: #fff; 60 | } 61 | body > p { 62 | display: none; 63 | } 64 | body > p.title, 65 | body.test > p.test, 66 | body.ok > p.ok, 67 | body.ko > p.ko { 68 | display: block; 69 | } 70 | -------------------------------------------------------------------------------- /experiments/authentication/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Authentication 5 | 6 | 7 | 8 |

WebSocket Authentication

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /experiments/authentication/test.js: -------------------------------------------------------------------------------- 1 | var token = document.body.dataset.token; 2 | 3 | const params = new URLSearchParams(window.location.search); 4 | var user = params.get("user"); 5 | -------------------------------------------------------------------------------- /experiments/authentication/user_info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | User information | WebSocket Authentication 5 | 6 | 7 | 8 |

[??] User information

9 |

[OK] User information

10 |

[KO] User information

11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /experiments/authentication/user_info.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", () => { 2 | const uri = `ws://${user}:${token}@localhost:8004/`; 3 | const websocket = new WebSocket(uri); 4 | 5 | websocket.onmessage = ({ data }) => { 6 | // event.data is expected to be "Hello !" 7 | websocket.send(`Goodbye ${data.slice(6, -1)}.`); 8 | }; 9 | 10 | runTest(websocket); 11 | }); 12 | -------------------------------------------------------------------------------- /experiments/broadcast/clients.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import statistics 5 | import sys 6 | import time 7 | 8 | from websockets.asyncio.client import connect 9 | 10 | 11 | LATENCIES = {} 12 | 13 | 14 | async def log_latency(interval): 15 | while True: 16 | await asyncio.sleep(interval) 17 | p = statistics.quantiles(LATENCIES.values(), n=100) 18 | print(f"clients = {len(LATENCIES)}") 19 | print( 20 | f"p50 = {p[49] / 1e6:.1f}ms, " 21 | f"p95 = {p[94] / 1e6:.1f}ms, " 22 | f"p99 = {p[98] / 1e6:.1f}ms" 23 | ) 24 | print() 25 | 26 | 27 | async def client(): 28 | try: 29 | async with connect( 30 | "ws://localhost:8765", 31 | ping_timeout=None, 32 | ) as websocket: 33 | async for msg in websocket: 34 | client_time = time.time_ns() 35 | server_time = int(msg[:19].decode()) 36 | LATENCIES[websocket] = client_time - server_time 37 | except Exception as exc: 38 | print(exc) 39 | 40 | 41 | async def main(count, interval): 42 | asyncio.create_task(log_latency(interval)) 43 | clients = [] 44 | for _ in range(count): 45 | clients.append(asyncio.create_task(client())) 46 | await asyncio.sleep(0.001) # 1ms between each connection 47 | await asyncio.wait(clients) 48 | 49 | 50 | if __name__ == "__main__": 51 | try: 52 | count = int(sys.argv[1]) 53 | interval = float(sys.argv[2]) 54 | except Exception as exc: 55 | print(f"Usage: {sys.argv[0]} count interval") 56 | print(" Connect clients e.g. 1000") 57 | print(" Report latency every seconds e.g. 1") 58 | print() 59 | print(exc) 60 | else: 61 | asyncio.run(main(count, interval)) 62 | -------------------------------------------------------------------------------- /experiments/compression/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import statistics 5 | import tracemalloc 6 | 7 | from websockets.asyncio.client import connect 8 | from websockets.extensions.permessage_deflate import ClientPerMessageDeflateFactory 9 | 10 | 11 | CLIENTS = 20 12 | INTERVAL = 1 / 10 # seconds 13 | 14 | WB, ML = 12, 5 15 | 16 | MEM_SIZE = [] 17 | 18 | 19 | async def client(num): 20 | # Space out connections to make them sequential. 21 | await asyncio.sleep(num * INTERVAL) 22 | 23 | tracemalloc.start() 24 | 25 | async with connect( 26 | "ws://localhost:8765", 27 | extensions=[ 28 | ClientPerMessageDeflateFactory( 29 | server_max_window_bits=WB, 30 | client_max_window_bits=WB, 31 | compress_settings={"memLevel": ML}, 32 | ) 33 | ], 34 | ) as ws: 35 | await ws.send("hello") 36 | await ws.recv() 37 | 38 | await ws.send(b"hello") 39 | await ws.recv() 40 | 41 | MEM_SIZE.append(tracemalloc.get_traced_memory()[0]) 42 | tracemalloc.stop() 43 | 44 | # Hold connection open until the end of the test. 45 | await asyncio.sleep((CLIENTS + 1 - num) * INTERVAL) 46 | 47 | 48 | async def clients(): 49 | # Start one more client than necessary because we will ignore 50 | # non-representative results from the first connection. 51 | await asyncio.gather(*[client(num) for num in range(CLIENTS + 1)]) 52 | 53 | 54 | asyncio.run(clients()) 55 | 56 | 57 | # First connection incurs non-representative setup costs. 58 | del MEM_SIZE[0] 59 | 60 | print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB") 61 | print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB") 62 | -------------------------------------------------------------------------------- /experiments/compression/corpus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import getpass 4 | import json 5 | import pathlib 6 | import subprocess 7 | import sys 8 | import time 9 | 10 | 11 | def github_commits(): 12 | OAUTH_TOKEN = getpass.getpass("OAuth Token? ") 13 | COMMIT_API = ( 14 | f'curl -H "Authorization: token {OAUTH_TOKEN}" ' 15 | f"https://api.github.com/repos/python-websockets/websockets/git/commits/:sha" 16 | ) 17 | 18 | commits = [] 19 | 20 | head = subprocess.check_output( 21 | "git rev-parse origin/main", 22 | shell=True, 23 | text=True, 24 | ).strip() 25 | todo = [head] 26 | seen = set() 27 | 28 | while todo: 29 | sha = todo.pop(0) 30 | commit = subprocess.check_output(COMMIT_API.replace(":sha", sha), shell=True) 31 | commits.append(commit) 32 | seen.add(sha) 33 | for parent in json.loads(commit)["parents"]: 34 | sha = parent["sha"] 35 | if sha not in seen and sha not in todo: 36 | todo.append(sha) 37 | time.sleep(1) # rate throttling 38 | 39 | return commits 40 | 41 | 42 | def main(corpus): 43 | data = github_commits() 44 | for num, content in enumerate(reversed(data)): 45 | (corpus / f"{num:04d}.json").write_bytes(content) 46 | 47 | 48 | if __name__ == "__main__": 49 | if len(sys.argv) < 2: 50 | print(f"Usage: {sys.argv[0]} ") 51 | sys.exit(2) 52 | main(pathlib.Path(sys.argv[1])) 53 | -------------------------------------------------------------------------------- /experiments/compression/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import os 5 | import signal 6 | import statistics 7 | import tracemalloc 8 | 9 | from websockets.asyncio.server import serve 10 | from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory 11 | 12 | 13 | CLIENTS = 20 14 | INTERVAL = 1 / 10 # seconds 15 | 16 | WB, ML = 12, 5 17 | 18 | MEM_SIZE = [] 19 | 20 | 21 | async def handler(ws): 22 | msg = await ws.recv() 23 | await ws.send(msg) 24 | 25 | msg = await ws.recv() 26 | await ws.send(msg) 27 | 28 | MEM_SIZE.append(tracemalloc.get_traced_memory()[0]) 29 | tracemalloc.stop() 30 | 31 | tracemalloc.start() 32 | 33 | # Hold connection open until the end of the test. 34 | await asyncio.sleep(CLIENTS * INTERVAL) 35 | 36 | 37 | async def server(): 38 | async with serve( 39 | handler, 40 | "localhost", 41 | 8765, 42 | extensions=[ 43 | ServerPerMessageDeflateFactory( 44 | server_max_window_bits=WB, 45 | client_max_window_bits=WB, 46 | compress_settings={"memLevel": ML}, 47 | ) 48 | ], 49 | ) as server: 50 | print("Stop the server with:") 51 | print(f"kill -TERM {os.getpid()}") 52 | print() 53 | loop = asyncio.get_running_loop() 54 | loop.add_signal_handler(signal.SIGTERM, server.close) 55 | 56 | tracemalloc.start() 57 | await server.wait_closed() 58 | 59 | 60 | asyncio.run(server()) 61 | 62 | 63 | # First connection incurs non-representative setup costs. 64 | del MEM_SIZE[0] 65 | 66 | print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB") 67 | print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB") 68 | -------------------------------------------------------------------------------- /experiments/json_log_formatter.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | 5 | class JSONFormatter(logging.Formatter): 6 | """ 7 | Render logs as JSON. 8 | 9 | To add details to a log record, store them in a ``event_data`` 10 | custom attribute. This dict is merged into the event. 11 | 12 | """ 13 | def __init__(self): 14 | pass # override logging.Formatter constructor 15 | 16 | def format(self, record): 17 | event = { 18 | "timestamp": self.getTimestamp(record.created), 19 | "message": record.getMessage(), 20 | "level": record.levelname, 21 | "logger": record.name, 22 | } 23 | event_data = getattr(record, "event_data", None) 24 | if event_data: 25 | event.update(event_data) 26 | if record.exc_info: 27 | event["exc_info"] = self.formatException(record.exc_info) 28 | if record.stack_info: 29 | event["stack_info"] = self.formatStack(record.stack_info) 30 | return json.dumps(event) 31 | 32 | def getTimestamp(self, created): 33 | return datetime.datetime.utcfromtimestamp(created).isoformat() 34 | -------------------------------------------------------------------------------- /experiments/profiling/compression.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Profile the permessage-deflate extension. 5 | 6 | Usage:: 7 | $ pip install line_profiler 8 | $ python experiments/compression/corpus.py experiments/compression/corpus 9 | $ PYTHONPATH=src python -m kernprof \ 10 | --line-by-line \ 11 | --prof-mod src/websockets/extensions/permessage_deflate.py \ 12 | --view \ 13 | experiments/profiling/compression.py experiments/compression/corpus 12 5 6 14 | 15 | """ 16 | 17 | import pathlib 18 | import sys 19 | 20 | from websockets.extensions.permessage_deflate import PerMessageDeflate 21 | from websockets.frames import OP_TEXT, Frame 22 | 23 | 24 | def compress_and_decompress(corpus, max_window_bits, memory_level, level): 25 | extension = PerMessageDeflate( 26 | remote_no_context_takeover=False, 27 | local_no_context_takeover=False, 28 | remote_max_window_bits=max_window_bits, 29 | local_max_window_bits=max_window_bits, 30 | compress_settings={"memLevel": memory_level, "level": level}, 31 | ) 32 | for data in corpus: 33 | frame = Frame(OP_TEXT, data) 34 | frame = extension.encode(frame) 35 | frame = extension.decode(frame) 36 | 37 | 38 | if __name__ == "__main__": 39 | if len(sys.argv) < 2 or not pathlib.Path(sys.argv[1]).is_dir(): 40 | print(f"Usage: {sys.argv[0]} [] []") 41 | corpus = [file.read_bytes() for file in pathlib.Path(sys.argv[1]).iterdir()] 42 | max_window_bits = int(sys.argv[2]) if len(sys.argv) > 2 else 12 43 | memory_level = int(sys.argv[3]) if len(sys.argv) > 3 else 5 44 | level = int(sys.argv[4]) if len(sys.argv) > 4 else 6 45 | compress_and_decompress(corpus, max_window_bits, memory_level, level) 46 | -------------------------------------------------------------------------------- /fuzzing/fuzz_http11_request_parser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import atheris 4 | 5 | 6 | with atheris.instrument_imports(): 7 | from websockets.exceptions import SecurityError 8 | from websockets.http11 import Request 9 | from websockets.streams import StreamReader 10 | 11 | 12 | def test_one_input(data): 13 | reader = StreamReader() 14 | reader.feed_data(data) 15 | reader.feed_eof() 16 | 17 | parser = Request.parse( 18 | reader.read_line, 19 | ) 20 | 21 | try: 22 | next(parser) 23 | except StopIteration as exc: 24 | assert isinstance(exc.value, Request) 25 | return # input accepted 26 | except ( 27 | EOFError, # connection is closed without a full HTTP request 28 | SecurityError, # request exceeds a security limit 29 | ValueError, # request isn't well formatted 30 | ): 31 | return # input rejected with a documented exception 32 | 33 | raise RuntimeError("parsing didn't complete") 34 | 35 | 36 | def main(): 37 | atheris.Setup(sys.argv, test_one_input) 38 | atheris.Fuzz() 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /fuzzing/fuzz_http11_response_parser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import atheris 4 | 5 | 6 | with atheris.instrument_imports(): 7 | from websockets.exceptions import SecurityError 8 | from websockets.http11 import Response 9 | from websockets.streams import StreamReader 10 | 11 | 12 | def test_one_input(data): 13 | reader = StreamReader() 14 | reader.feed_data(data) 15 | reader.feed_eof() 16 | 17 | parser = Response.parse( 18 | reader.read_line, 19 | reader.read_exact, 20 | reader.read_to_eof, 21 | ) 22 | try: 23 | next(parser) 24 | except StopIteration as exc: 25 | assert isinstance(exc.value, Response) 26 | return # input accepted 27 | except ( 28 | EOFError, # connection is closed without a full HTTP response 29 | SecurityError, # response exceeds a security limit 30 | LookupError, # response isn't well formatted 31 | ValueError, # response isn't well formatted 32 | ): 33 | return # input rejected with a documented exception 34 | 35 | raise RuntimeError("parsing didn't complete") 36 | 37 | 38 | def main(): 39 | atheris.Setup(sys.argv, test_one_input) 40 | atheris.Fuzz() 41 | 42 | 43 | if __name__ == "__main__": 44 | main() 45 | -------------------------------------------------------------------------------- /fuzzing/fuzz_websocket_parser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import atheris 4 | 5 | 6 | with atheris.instrument_imports(): 7 | from websockets.exceptions import PayloadTooBig, ProtocolError 8 | from websockets.frames import Frame 9 | from websockets.streams import StreamReader 10 | 11 | 12 | def test_one_input(data): 13 | fdp = atheris.FuzzedDataProvider(data) 14 | mask = fdp.ConsumeBool() 15 | max_size_enabled = fdp.ConsumeBool() 16 | max_size = fdp.ConsumeInt(4) 17 | payload = fdp.ConsumeBytes(atheris.ALL_REMAINING) 18 | 19 | reader = StreamReader() 20 | reader.feed_data(payload) 21 | reader.feed_eof() 22 | 23 | parser = Frame.parse( 24 | reader.read_exact, 25 | mask=mask, 26 | max_size=max_size if max_size_enabled else None, 27 | ) 28 | 29 | try: 30 | next(parser) 31 | except StopIteration as exc: 32 | assert isinstance(exc.value, Frame) 33 | return # input accepted 34 | except ( 35 | EOFError, # connection is closed without a full WebSocket frame 36 | UnicodeDecodeError, # frame contains invalid UTF-8 37 | PayloadTooBig, # frame's payload size exceeds ``max_size`` 38 | ProtocolError, # frame contains incorrect values 39 | ): 40 | return # input rejected with a documented exception 41 | 42 | raise RuntimeError("parsing didn't complete") 43 | 44 | 45 | def main(): 46 | atheris.Setup(sys.argv, test_one_input) 47 | atheris.Fuzz() 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/logo/favicon.ico -------------------------------------------------------------------------------- /logo/github-social-preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub social preview 5 | 31 | 32 | 33 |

Take a screenshot of this DOM node to make a PNG.

34 |

For 2x DPI screens.

35 |

preview @ 2x

36 |

For regular screens.

37 |

preview

38 | 39 | 40 | -------------------------------------------------------------------------------- /logo/github-social-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/logo/github-social-preview.png -------------------------------------------------------------------------------- /logo/icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon 5 | 13 | 14 | 15 |

Take a screenshot of these DOM nodes to2x make a PNG.

16 |

8x8 / 16x16 @ 2x

17 |

16x16 / 32x32 @ 2x

18 |

32x32 / 32x32 @ 2x

19 |

32x32 / 64x64 @ 2x

20 |

64x64 / 128x128 @ 2x

21 |

128x128 / 256x256 @ 2x

22 |

256x256 / 512x512 @ 2x

23 |

512x512 / 1024x1024 @ 2x

24 | 25 | 26 | -------------------------------------------------------------------------------- /logo/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /logo/old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /logo/tidelift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/logo/tidelift.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "websockets" 7 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 8 | requires-python = ">=3.9" 9 | license = { text = "BSD-3-Clause" } 10 | authors = [ 11 | { name = "Aymeric Augustin", email = "aymeric.augustin@m4x.org" }, 12 | ] 13 | keywords = ["WebSocket"] 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Environment :: Web Environment", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: BSD License", 19 | "Operating System :: OS Independent", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | ] 28 | dynamic = ["version", "readme"] 29 | 30 | [project.urls] 31 | Homepage = "https://github.com/python-websockets/websockets" 32 | Changelog = "https://websockets.readthedocs.io/en/stable/project/changelog.html" 33 | Documentation = "https://websockets.readthedocs.io/" 34 | Funding = "https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme" 35 | Tracker = "https://github.com/python-websockets/websockets/issues" 36 | 37 | [project.scripts] 38 | websockets = "websockets.cli:main" 39 | 40 | [tool.cibuildwheel] 41 | enable = ["pypy"] 42 | 43 | # On a macOS runner, build Intel, Universal, and Apple Silicon wheels. 44 | [tool.cibuildwheel.macos] 45 | archs = ["x86_64", "universal2", "arm64"] 46 | 47 | # On an Linux Intel runner with QEMU installed, build Intel and ARM wheels. 48 | [tool.cibuildwheel.linux] 49 | archs = ["auto", "aarch64"] 50 | 51 | [tool.coverage.run] 52 | branch = true 53 | omit = [ 54 | # */websockets matches src/websockets and .tox/**/site-packages/websockets 55 | "*/websockets/__main__.py", 56 | "*/websockets/asyncio/async_timeout.py", 57 | "*/websockets/asyncio/compatibility.py", 58 | "tests/maxi_cov.py", 59 | ] 60 | 61 | [tool.coverage.paths] 62 | source = [ 63 | "src/websockets", 64 | ".tox/*/lib/python*/site-packages/websockets", 65 | ] 66 | 67 | [tool.coverage.report] 68 | exclude_lines = [ 69 | "pragma: no cover", 70 | "except ImportError:", 71 | "if self.debug:", 72 | "if sys.platform == \"win32\":", 73 | "if sys.platform != \"win32\":", 74 | "if TYPE_CHECKING:", 75 | "raise AssertionError", 76 | "self.fail\\(\".*\"\\)", 77 | "@overload", 78 | "@unittest.skip", 79 | ] 80 | partial_branches = [ 81 | "pragma: no branch", 82 | "with self.assertRaises\\(.*\\)", 83 | ] 84 | 85 | [tool.ruff] 86 | target-version = "py312" 87 | 88 | [tool.ruff.lint] 89 | select = [ 90 | "E", # pycodestyle 91 | "F", # Pyflakes 92 | "W", # pycodestyle 93 | "I", # isort 94 | ] 95 | ignore = [ 96 | "F403", 97 | "F405", 98 | ] 99 | 100 | [tool.ruff.lint.isort] 101 | combine-as-imports = true 102 | lines-after-imports = 2 103 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import re 4 | 5 | import setuptools 6 | 7 | 8 | root_dir = pathlib.Path(__file__).parent 9 | 10 | exec((root_dir / "src" / "websockets" / "version.py").read_text(encoding="utf-8")) 11 | 12 | # PyPI disables the "raw" directive. Remove this section of the README. 13 | long_description = re.sub( 14 | r"^\.\. raw:: html.*?^(?=\w)", 15 | "", 16 | (root_dir / "README.rst").read_text(encoding="utf-8"), 17 | flags=re.DOTALL | re.MULTILINE, 18 | ) 19 | 20 | # Set BUILD_EXTENSION to yes or no to force building or not building the 21 | # speedups extension. If unset, the extension is built only if possible. 22 | if os.environ.get("BUILD_EXTENSION") == "no": 23 | ext_modules = [] 24 | else: 25 | ext_modules = [ 26 | setuptools.Extension( 27 | "websockets.speedups", 28 | sources=["src/websockets/speedups.c"], 29 | optional=os.environ.get("BUILD_EXTENSION") != "yes", 30 | ) 31 | ] 32 | 33 | # Static values are declared in pyproject.toml. 34 | setuptools.setup( 35 | version=version, 36 | long_description=long_description, 37 | long_description_content_type="text/x-rst", 38 | ext_modules=ext_modules, 39 | ) 40 | -------------------------------------------------------------------------------- /src/websockets/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import main 2 | 3 | 4 | if __name__ == "__main__": 5 | main() 6 | -------------------------------------------------------------------------------- /src/websockets/asyncio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/src/websockets/asyncio/__init__.py -------------------------------------------------------------------------------- /src/websockets/asyncio/compatibility.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | 6 | __all__ = ["TimeoutError", "aiter", "anext", "asyncio_timeout", "asyncio_timeout_at"] 7 | 8 | 9 | if sys.version_info[:2] >= (3, 11): 10 | TimeoutError = TimeoutError 11 | aiter = aiter 12 | anext = anext 13 | from asyncio import ( 14 | timeout as asyncio_timeout, # noqa: F401 15 | timeout_at as asyncio_timeout_at, # noqa: F401 16 | ) 17 | 18 | else: # Python < 3.11 19 | from asyncio import TimeoutError 20 | 21 | def aiter(async_iterable): 22 | return type(async_iterable).__aiter__(async_iterable) 23 | 24 | async def anext(async_iterator): 25 | return await type(async_iterator).__anext__(async_iterator) 26 | 27 | from .async_timeout import ( 28 | timeout as asyncio_timeout, # noqa: F401 29 | timeout_at as asyncio_timeout_at, # noqa: F401 30 | ) 31 | -------------------------------------------------------------------------------- /src/websockets/auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | 6 | with warnings.catch_warnings(): 7 | # Suppress redundant DeprecationWarning raised by websockets.legacy. 8 | warnings.filterwarnings("ignore", category=DeprecationWarning) 9 | from .legacy.auth import * 10 | from .legacy.auth import __all__ # noqa: F401 11 | 12 | 13 | warnings.warn( # deprecated in 14.0 - 2024-11-09 14 | "websockets.auth, an alias for websockets.legacy.auth, is deprecated; " 15 | "see https://websockets.readthedocs.io/en/stable/howto/upgrade.html " 16 | "for upgrade instructions", 17 | DeprecationWarning, 18 | ) 19 | -------------------------------------------------------------------------------- /src/websockets/connection.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401 6 | 7 | 8 | warnings.warn( # deprecated in 11.0 - 2023-04-02 9 | "websockets.connection was renamed to websockets.protocol " 10 | "and Connection was renamed to Protocol", 11 | DeprecationWarning, 12 | ) 13 | -------------------------------------------------------------------------------- /src/websockets/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | 4 | __all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"] 5 | -------------------------------------------------------------------------------- /src/websockets/http.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | from .datastructures import Headers, MultipleValuesError # noqa: F401 6 | 7 | 8 | with warnings.catch_warnings(): 9 | # Suppress redundant DeprecationWarning raised by websockets.legacy. 10 | warnings.filterwarnings("ignore", category=DeprecationWarning) 11 | from .legacy.http import read_request, read_response # noqa: F401 12 | 13 | 14 | warnings.warn( # deprecated in 9.0 - 2021-09-01 15 | "Headers and MultipleValuesError were moved " 16 | "from websockets.http to websockets.datastructures" 17 | "and read_request and read_response were moved " 18 | "from websockets.http to websockets.legacy.http", 19 | DeprecationWarning, 20 | ) 21 | -------------------------------------------------------------------------------- /src/websockets/legacy/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | 6 | warnings.warn( # deprecated in 14.0 - 2024-11-09 7 | "websockets.legacy is deprecated; " 8 | "see https://websockets.readthedocs.io/en/stable/howto/upgrade.html " 9 | "for upgrade instructions", 10 | DeprecationWarning, 11 | ) 12 | -------------------------------------------------------------------------------- /src/websockets/legacy/exceptions.py: -------------------------------------------------------------------------------- 1 | import http 2 | 3 | from .. import datastructures 4 | from ..exceptions import ( 5 | InvalidHandshake, 6 | # InvalidMessage was incorrectly moved here in versions 14.0 and 14.1. 7 | InvalidMessage, # noqa: F401 8 | ProtocolError as WebSocketProtocolError, # noqa: F401 9 | ) 10 | from ..typing import StatusLike 11 | 12 | 13 | class InvalidStatusCode(InvalidHandshake): 14 | """ 15 | Raised when a handshake response status code is invalid. 16 | 17 | """ 18 | 19 | def __init__(self, status_code: int, headers: datastructures.Headers) -> None: 20 | self.status_code = status_code 21 | self.headers = headers 22 | 23 | def __str__(self) -> str: 24 | return f"server rejected WebSocket connection: HTTP {self.status_code}" 25 | 26 | 27 | class AbortHandshake(InvalidHandshake): 28 | """ 29 | Raised to abort the handshake on purpose and return an HTTP response. 30 | 31 | This exception is an implementation detail. 32 | 33 | The public API is 34 | :meth:`~websockets.legacy.server.WebSocketServerProtocol.process_request`. 35 | 36 | Attributes: 37 | status (~http.HTTPStatus): HTTP status code. 38 | headers (Headers): HTTP response headers. 39 | body (bytes): HTTP response body. 40 | """ 41 | 42 | def __init__( 43 | self, 44 | status: StatusLike, 45 | headers: datastructures.HeadersLike, 46 | body: bytes = b"", 47 | ) -> None: 48 | # If a user passes an int instead of an HTTPStatus, fix it automatically. 49 | self.status = http.HTTPStatus(status) 50 | self.headers = datastructures.Headers(headers) 51 | self.body = body 52 | 53 | def __str__(self) -> str: 54 | return ( 55 | f"HTTP {self.status:d}, {len(self.headers)} headers, {len(self.body)} bytes" 56 | ) 57 | 58 | 59 | class RedirectHandshake(InvalidHandshake): 60 | """ 61 | Raised when a handshake gets redirected. 62 | 63 | This exception is an implementation detail. 64 | 65 | """ 66 | 67 | def __init__(self, uri: str) -> None: 68 | self.uri = uri 69 | 70 | def __str__(self) -> str: 71 | return f"redirect to {self.uri}" 72 | -------------------------------------------------------------------------------- /src/websockets/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/src/websockets/py.typed -------------------------------------------------------------------------------- /src/websockets/speedups.pyi: -------------------------------------------------------------------------------- 1 | def apply_mask(data: bytes, mask: bytes) -> bytes: ... 2 | -------------------------------------------------------------------------------- /src/websockets/sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/src/websockets/sync/__init__.py -------------------------------------------------------------------------------- /src/websockets/sync/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | 5 | 6 | __all__ = ["Deadline"] 7 | 8 | 9 | class Deadline: 10 | """ 11 | Manage timeouts across multiple steps. 12 | 13 | Args: 14 | timeout: Time available in seconds or :obj:`None` if there is no limit. 15 | 16 | """ 17 | 18 | def __init__(self, timeout: float | None) -> None: 19 | self.deadline: float | None 20 | if timeout is None: 21 | self.deadline = None 22 | else: 23 | self.deadline = time.monotonic() + timeout 24 | 25 | def timeout(self, *, raise_if_elapsed: bool = True) -> float | None: 26 | """ 27 | Calculate a timeout from a deadline. 28 | 29 | Args: 30 | raise_if_elapsed: Whether to raise :exc:`TimeoutError` 31 | if the deadline lapsed. 32 | 33 | Raises: 34 | TimeoutError: If the deadline lapsed. 35 | 36 | Returns: 37 | Time left in seconds or :obj:`None` if there is no limit. 38 | 39 | """ 40 | if self.deadline is None: 41 | return None 42 | timeout = self.deadline - time.monotonic() 43 | if raise_if_elapsed and timeout <= 0: 44 | raise TimeoutError("timed out") 45 | return timeout 46 | -------------------------------------------------------------------------------- /src/websockets/typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import http 4 | import logging 5 | from typing import TYPE_CHECKING, Any, NewType, Optional, Sequence, Union 6 | 7 | 8 | __all__ = [ 9 | "Data", 10 | "LoggerLike", 11 | "StatusLike", 12 | "Origin", 13 | "Subprotocol", 14 | "ExtensionName", 15 | "ExtensionParameter", 16 | ] 17 | 18 | 19 | # Public types used in the signature of public APIs 20 | 21 | # Change to str | bytes when dropping Python < 3.10. 22 | Data = Union[str, bytes] 23 | """Types supported in a WebSocket message: 24 | :class:`str` for a Text_ frame, :class:`bytes` for a Binary_. 25 | 26 | .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 27 | .. _Binary : https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 28 | 29 | """ 30 | 31 | 32 | # Change to logging.Logger | ... when dropping Python < 3.10. 33 | if TYPE_CHECKING: 34 | LoggerLike = Union[logging.Logger, logging.LoggerAdapter[Any]] 35 | """Types accepted where a :class:`~logging.Logger` is expected.""" 36 | else: # remove this branch when dropping support for Python < 3.11 37 | LoggerLike = Union[logging.Logger, logging.LoggerAdapter] 38 | """Types accepted where a :class:`~logging.Logger` is expected.""" 39 | 40 | 41 | # Change to http.HTTPStatus | int when dropping Python < 3.10. 42 | StatusLike = Union[http.HTTPStatus, int] 43 | """ 44 | Types accepted where an :class:`~http.HTTPStatus` is expected.""" 45 | 46 | 47 | Origin = NewType("Origin", str) 48 | """Value of a ``Origin`` header.""" 49 | 50 | 51 | Subprotocol = NewType("Subprotocol", str) 52 | """Subprotocol in a ``Sec-WebSocket-Protocol`` header.""" 53 | 54 | 55 | ExtensionName = NewType("ExtensionName", str) 56 | """Name of a WebSocket extension.""" 57 | 58 | # Change to tuple[str, str | None] when dropping Python < 3.10. 59 | ExtensionParameter = tuple[str, Optional[str]] 60 | """Parameter of a WebSocket extension.""" 61 | 62 | 63 | # Private types 64 | 65 | ExtensionHeader = tuple[ExtensionName, Sequence[ExtensionParameter]] 66 | """Extension in a ``Sec-WebSocket-Extensions`` header.""" 67 | 68 | 69 | ConnectionOption = NewType("ConnectionOption", str) 70 | """Connection option in a ``Connection`` header.""" 71 | 72 | 73 | UpgradeProtocol = NewType("UpgradeProtocol", str) 74 | """Upgrade protocol in an ``Upgrade`` header.""" 75 | -------------------------------------------------------------------------------- /src/websockets/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import hashlib 5 | import secrets 6 | import sys 7 | 8 | 9 | __all__ = ["accept_key", "apply_mask"] 10 | 11 | 12 | GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 13 | 14 | 15 | def generate_key() -> str: 16 | """ 17 | Generate a random key for the Sec-WebSocket-Key header. 18 | 19 | """ 20 | key = secrets.token_bytes(16) 21 | return base64.b64encode(key).decode() 22 | 23 | 24 | def accept_key(key: str) -> str: 25 | """ 26 | Compute the value of the Sec-WebSocket-Accept header. 27 | 28 | Args: 29 | key: Value of the Sec-WebSocket-Key header. 30 | 31 | """ 32 | sha1 = hashlib.sha1((key + GUID).encode()).digest() 33 | return base64.b64encode(sha1).decode() 34 | 35 | 36 | def apply_mask(data: bytes, mask: bytes) -> bytes: 37 | """ 38 | Apply masking to the data of a WebSocket message. 39 | 40 | Args: 41 | data: Data to mask. 42 | mask: 4-bytes mask. 43 | 44 | """ 45 | if len(mask) != 4: 46 | raise ValueError("mask must contain 4 bytes") 47 | 48 | data_int = int.from_bytes(data, sys.byteorder) 49 | mask_repeated = mask * (len(data) // 4) + mask[: len(data) % 4] 50 | mask_int = int.from_bytes(mask_repeated, sys.byteorder) 51 | return (data_int ^ mask_int).to_bytes(len(data), sys.byteorder) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | 5 | format = "%(asctime)s %(levelname)s %(name)s %(message)s" 6 | 7 | if bool(os.environ.get("WEBSOCKETS_DEBUG")): # pragma: no cover 8 | # Display every frame sent or received in debug mode. 9 | level = logging.DEBUG 10 | else: 11 | # Hide stack traces of exceptions. 12 | level = logging.CRITICAL 13 | 14 | logging.basicConfig(format=format, level=level) 15 | -------------------------------------------------------------------------------- /tests/asyncio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/tests/asyncio/__init__.py -------------------------------------------------------------------------------- /tests/asyncio/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | import urllib.parse 4 | 5 | 6 | def get_host_port(server): 7 | for sock in server.sockets: 8 | if sock.family == socket.AF_INET: # pragma: no branch 9 | return sock.getsockname() 10 | raise AssertionError("expected at least one IPv4 socket") 11 | 12 | 13 | def get_uri(server, secure=None): 14 | if secure is None: 15 | secure = server.server._ssl_context is not None # hack 16 | protocol = "wss" if secure else "ws" 17 | host, port = get_host_port(server) 18 | return f"{protocol}://{host}:{port}" 19 | 20 | 21 | async def handler(ws): 22 | path = urllib.parse.urlparse(ws.request.path).path 23 | if path == "/": 24 | # The default path is an eval shell. 25 | async for expr in ws: 26 | value = eval(expr) 27 | await ws.send(str(value)) 28 | elif path == "/crash": 29 | raise RuntimeError 30 | elif path == "/no-op": 31 | pass 32 | elif path == "/delay": 33 | delay = float(await ws.recv()) 34 | await ws.close() 35 | await asyncio.sleep(delay) 36 | else: 37 | raise AssertionError(f"unexpected path: {path}") 38 | 39 | 40 | # This shortcut avoids repeating serve(handler, "localhost", 0) for every test. 41 | args = handler, "localhost", 0 42 | 43 | 44 | class EvalShellMixin: 45 | async def assertEval(self, client, expr, value): 46 | await client.send(expr) 47 | self.assertEqual(await client.recv(), value) 48 | -------------------------------------------------------------------------------- /tests/asyncio/utils.py: -------------------------------------------------------------------------------- 1 | async def alist(async_iterable): 2 | items = [] 3 | async for item in async_iterable: 4 | items.append(item) 5 | return items 6 | -------------------------------------------------------------------------------- /tests/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/tests/extensions/__init__.py -------------------------------------------------------------------------------- /tests/extensions/test_base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from websockets.extensions.base import * 4 | from websockets.frames import Frame, Opcode 5 | 6 | 7 | class ExtensionTests(unittest.TestCase): 8 | def test_encode(self): 9 | with self.assertRaises(NotImplementedError): 10 | Extension().encode(Frame(Opcode.TEXT, b"")) 11 | 12 | def test_decode(self): 13 | with self.assertRaises(NotImplementedError): 14 | Extension().decode(Frame(Opcode.TEXT, b"")) 15 | 16 | 17 | class ClientExtensionFactoryTests(unittest.TestCase): 18 | def test_get_request_params(self): 19 | with self.assertRaises(NotImplementedError): 20 | ClientExtensionFactory().get_request_params() 21 | 22 | def test_process_response_params(self): 23 | with self.assertRaises(NotImplementedError): 24 | ClientExtensionFactory().process_response_params([], []) 25 | 26 | 27 | class ServerExtensionFactoryTests(unittest.TestCase): 28 | def test_process_request_params(self): 29 | with self.assertRaises(NotImplementedError): 30 | ServerExtensionFactory().process_request_params([], []) 31 | -------------------------------------------------------------------------------- /tests/extensions/utils.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | 3 | from websockets.exceptions import NegotiationError 4 | 5 | 6 | class OpExtension: 7 | name = "x-op" 8 | 9 | def __init__(self, op=None): 10 | self.op = op 11 | 12 | def decode(self, frame, *, max_size=None): 13 | return frame # pragma: no cover 14 | 15 | def encode(self, frame): 16 | return frame # pragma: no cover 17 | 18 | def __eq__(self, other): 19 | return isinstance(other, OpExtension) and self.op == other.op 20 | 21 | 22 | class ClientOpExtensionFactory: 23 | name = "x-op" 24 | 25 | def __init__(self, op=None): 26 | self.op = op 27 | 28 | def get_request_params(self): 29 | return [("op", self.op)] 30 | 31 | def process_response_params(self, params, accepted_extensions): 32 | if params != [("op", self.op)]: 33 | raise NegotiationError() 34 | return OpExtension(self.op) 35 | 36 | 37 | class ServerOpExtensionFactory: 38 | name = "x-op" 39 | 40 | def __init__(self, op=None): 41 | self.op = op 42 | 43 | def process_request_params(self, params, accepted_extensions): 44 | if params != [("op", self.op)]: 45 | raise NegotiationError() 46 | return [("op", self.op)], OpExtension(self.op) 47 | 48 | 49 | class NoOpExtension: 50 | name = "x-no-op" 51 | 52 | def __repr__(self): 53 | return "NoOpExtension()" 54 | 55 | def decode(self, frame, *, max_size=None): 56 | return frame 57 | 58 | def encode(self, frame): 59 | return frame 60 | 61 | 62 | class ClientNoOpExtensionFactory: 63 | name = "x-no-op" 64 | 65 | def get_request_params(self): 66 | return [] 67 | 68 | def process_response_params(self, params, accepted_extensions): 69 | if params: 70 | raise NegotiationError() 71 | return NoOpExtension() 72 | 73 | 74 | class ServerNoOpExtensionFactory: 75 | name = "x-no-op" 76 | 77 | def __init__(self, params=None): 78 | self.params = params or [] 79 | 80 | def process_request_params(self, params, accepted_extensions): 81 | return self.params, NoOpExtension() 82 | 83 | 84 | class Rsv2Extension: 85 | name = "x-rsv2" 86 | 87 | def decode(self, frame, *, max_size=None): 88 | assert frame.rsv2 89 | return dataclasses.replace(frame, rsv2=False) 90 | 91 | def encode(self, frame): 92 | assert not frame.rsv2 93 | return dataclasses.replace(frame, rsv2=True) 94 | 95 | def __eq__(self, other): 96 | return isinstance(other, Rsv2Extension) 97 | 98 | 99 | class ClientRsv2ExtensionFactory: 100 | name = "x-rsv2" 101 | 102 | def get_request_params(self): 103 | return [] 104 | 105 | def process_response_params(self, params, accepted_extensions): 106 | return Rsv2Extension() 107 | 108 | 109 | class ServerRsv2ExtensionFactory: 110 | name = "x-rsv2" 111 | 112 | def process_request_params(self, params, accepted_extensions): 113 | return [], Rsv2Extension() 114 | -------------------------------------------------------------------------------- /tests/legacy/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | 6 | with warnings.catch_warnings(): 7 | # Suppress DeprecationWarning raised by websockets.legacy. 8 | warnings.filterwarnings("ignore", category=DeprecationWarning) 9 | import websockets.legacy # noqa: F401 10 | -------------------------------------------------------------------------------- /tests/legacy/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from websockets.datastructures import Headers 4 | from websockets.legacy.exceptions import * 5 | 6 | 7 | class ExceptionsTests(unittest.TestCase): 8 | def test_str(self): 9 | for exception, exception_str in [ 10 | ( 11 | InvalidStatusCode(403, Headers()), 12 | "server rejected WebSocket connection: HTTP 403", 13 | ), 14 | ( 15 | AbortHandshake(200, Headers(), b"OK\n"), 16 | "HTTP 200, 0 headers, 3 bytes", 17 | ), 18 | ( 19 | RedirectHandshake("wss://example.com"), 20 | "redirect to wss://example.com", 21 | ), 22 | ]: 23 | with self.subTest(exception=exception): 24 | self.assertEqual(str(exception), exception_str) 25 | -------------------------------------------------------------------------------- /tests/legacy/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import sys 4 | import unittest 5 | 6 | from ..utils import AssertNoLogsMixin 7 | 8 | 9 | class AsyncioTestCase(AssertNoLogsMixin, unittest.TestCase): 10 | """ 11 | Base class for tests that sets up an isolated event loop for each test. 12 | 13 | IsolatedAsyncioTestCase was introduced in Python 3.8 for similar purposes 14 | but isn't a drop-in replacement. 15 | 16 | """ 17 | 18 | def __init_subclass__(cls, **kwargs): 19 | """ 20 | Convert test coroutines to test functions. 21 | 22 | This supports asynchronous tests transparently. 23 | 24 | """ 25 | super().__init_subclass__(**kwargs) 26 | for name in unittest.defaultTestLoader.getTestCaseNames(cls): 27 | test = getattr(cls, name) 28 | if asyncio.iscoroutinefunction(test): 29 | setattr(cls, name, cls.convert_async_to_sync(test)) 30 | 31 | @staticmethod 32 | def convert_async_to_sync(test): 33 | """ 34 | Convert a test coroutine to a test function. 35 | 36 | """ 37 | 38 | @functools.wraps(test) 39 | def test_func(self, *args, **kwargs): 40 | return self.loop.run_until_complete(test(self, *args, **kwargs)) 41 | 42 | return test_func 43 | 44 | def setUp(self): 45 | super().setUp() 46 | self.loop = asyncio.new_event_loop() 47 | asyncio.set_event_loop(self.loop) 48 | 49 | def tearDown(self): 50 | self.loop.close() 51 | super().tearDown() 52 | 53 | def run_loop_once(self): 54 | # Process callbacks scheduled with call_soon by appending a callback 55 | # to stop the event loop then running it until it hits that callback. 56 | self.loop.call_soon(self.loop.stop) 57 | self.loop.run_forever() 58 | 59 | def assertDeprecationWarnings(self, recorded_warnings, expected_warnings): 60 | """ 61 | Check recorded deprecation warnings match a list of expected messages. 62 | 63 | """ 64 | # Work around https://github.com/python/cpython/issues/90476. 65 | if sys.version_info[:2] < (3, 11): # pragma: no cover 66 | recorded_warnings = [ 67 | recorded 68 | for recorded in recorded_warnings 69 | if not ( 70 | type(recorded.message) is ResourceWarning 71 | and str(recorded.message).startswith("unclosed transport") 72 | ) 73 | ] 74 | 75 | for recorded in recorded_warnings: 76 | self.assertIs(type(recorded.message), DeprecationWarning) 77 | self.assertEqual( 78 | {str(recorded.message) for recorded in recorded_warnings}, 79 | set(expected_warnings), 80 | ) 81 | -------------------------------------------------------------------------------- /tests/protocol.py: -------------------------------------------------------------------------------- 1 | from websockets.protocol import Protocol 2 | 3 | 4 | class RecordingProtocol(Protocol): 5 | """ 6 | Protocol subclass that records incoming frames. 7 | 8 | By interfacing with this protocol, you can check easily what the component 9 | being testing sends during a test. 10 | 11 | """ 12 | 13 | def __init__(self, *args, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | self.frames_rcvd = [] 16 | 17 | def get_frames_rcvd(self): 18 | """ 19 | Get incoming frames received up to this point. 20 | 21 | Calling this method clears the list. Each frame is returned only once. 22 | 23 | """ 24 | frames_rcvd, self.frames_rcvd = self.frames_rcvd, [] 25 | return frames_rcvd 26 | 27 | def recv_frame(self, frame): 28 | self.frames_rcvd.append(frame) 29 | super().recv_frame(frame) 30 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | python-socks[asyncio] 2 | mitmproxy 3 | -------------------------------------------------------------------------------- /tests/sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-websockets/websockets/fc7cafea01ebe3ec1738160ac62889fd80653255/tests/sync/__init__.py -------------------------------------------------------------------------------- /tests/sync/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from websockets.sync.utils import * 4 | 5 | from ..utils import MS 6 | 7 | 8 | class DeadlineTests(unittest.TestCase): 9 | def test_timeout_pending(self): 10 | """timeout returns remaining time if deadline is in the future.""" 11 | deadline = Deadline(MS) 12 | timeout = deadline.timeout() 13 | self.assertGreater(timeout, 0) 14 | self.assertLess(timeout, MS) 15 | 16 | def test_timeout_elapsed_exception(self): 17 | """timeout raises TimeoutError if deadline is in the past.""" 18 | deadline = Deadline(-MS) 19 | with self.assertRaises(TimeoutError): 20 | deadline.timeout() 21 | 22 | def test_timeout_elapsed_no_exception(self): 23 | """timeout doesn't raise TimeoutError when raise_if_elapsed is disabled.""" 24 | deadline = Deadline(-MS) 25 | timeout = deadline.timeout(raise_if_elapsed=False) 26 | self.assertGreater(timeout, -2 * MS) 27 | self.assertLess(timeout, -MS) 28 | 29 | def test_no_timeout(self): 30 | """timeout returns None when no deadline is set.""" 31 | deadline = Deadline(None) 32 | timeout = deadline.timeout() 33 | self.assertIsNone(timeout, None) 34 | -------------------------------------------------------------------------------- /tests/sync/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import threading 3 | import time 4 | import unittest 5 | 6 | from ..utils import MS 7 | 8 | 9 | class ThreadTestCase(unittest.TestCase): 10 | @contextlib.contextmanager 11 | def run_in_thread(self, target): 12 | """ 13 | Run ``target`` function without arguments in a thread. 14 | 15 | In order to facilitate writing tests, this helper lets the thread run 16 | for 1ms on entry and joins the thread with a 1ms timeout on exit. 17 | 18 | """ 19 | thread = threading.Thread(target=target) 20 | thread.start() 21 | time.sleep(MS) 22 | try: 23 | yield 24 | finally: 25 | thread.join(MS) 26 | self.assertFalse(thread.is_alive()) 27 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | from .utils import DeprecationTestCase 2 | 3 | 4 | class BackwardsCompatibilityTests(DeprecationTestCase): 5 | def test_headers_class(self): 6 | with self.assertDeprecationWarning( 7 | "websockets.auth, an alias for websockets.legacy.auth, is deprecated; " 8 | "see https://websockets.readthedocs.io/en/stable/howto/upgrade.html " 9 | "for upgrade instructions", 10 | ): 11 | from websockets.auth import ( 12 | BasicAuthWebSocketServerProtocol, # noqa: F401 13 | basic_auth_protocol_factory, # noqa: F401 14 | ) 15 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | from websockets.protocol import Protocol 2 | 3 | from .utils import DeprecationTestCase 4 | 5 | 6 | class BackwardsCompatibilityTests(DeprecationTestCase): 7 | def test_connection_class(self): 8 | """Connection is a deprecated alias for Protocol.""" 9 | with self.assertDeprecationWarning( 10 | "websockets.connection was renamed to websockets.protocol " 11 | "and Connection was renamed to Protocol" 12 | ): 13 | from websockets.connection import Connection 14 | 15 | self.assertIs(Connection, Protocol) 16 | -------------------------------------------------------------------------------- /tests/test_exports.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import websockets 4 | import websockets.asyncio.client 5 | import websockets.asyncio.router 6 | import websockets.asyncio.server 7 | import websockets.client 8 | import websockets.datastructures 9 | import websockets.exceptions 10 | import websockets.server 11 | import websockets.typing 12 | import websockets.uri 13 | 14 | 15 | combined_exports = [ 16 | name 17 | for name in ( 18 | [] 19 | + websockets.asyncio.client.__all__ 20 | + websockets.asyncio.router.__all__ 21 | + websockets.asyncio.server.__all__ 22 | + websockets.client.__all__ 23 | + websockets.datastructures.__all__ 24 | + websockets.exceptions.__all__ 25 | + websockets.frames.__all__ 26 | + websockets.http11.__all__ 27 | + websockets.protocol.__all__ 28 | + websockets.server.__all__ 29 | + websockets.typing.__all__ 30 | ) 31 | if not name.isupper() # filter out constants 32 | ] 33 | 34 | 35 | class ExportsTests(unittest.TestCase): 36 | def test_top_level_module_reexports_submodule_exports(self): 37 | self.assertEqual( 38 | set(combined_exports), 39 | set(websockets.__all__), 40 | ) 41 | 42 | def test_submodule_exports_are_globally_unique(self): 43 | self.assertEqual( 44 | len(set(combined_exports)), 45 | len(combined_exports), 46 | ) 47 | -------------------------------------------------------------------------------- /tests/test_http.py: -------------------------------------------------------------------------------- 1 | from websockets.datastructures import Headers 2 | 3 | from .utils import DeprecationTestCase 4 | 5 | 6 | class BackwardsCompatibilityTests(DeprecationTestCase): 7 | def test_headers_class(self): 8 | with self.assertDeprecationWarning( 9 | "Headers and MultipleValuesError were moved " 10 | "from websockets.http to websockets.datastructures" 11 | "and read_request and read_response were moved " 12 | "from websockets.http to websockets.legacy.http", 13 | ): 14 | from websockets.http import Headers as OldHeaders 15 | 16 | self.assertIs(OldHeaders, Headers) 17 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | import types 2 | import unittest 3 | import warnings 4 | 5 | from websockets.imports import * 6 | 7 | 8 | foo = object() 9 | 10 | bar = object() 11 | 12 | 13 | class ImportsTests(unittest.TestCase): 14 | def setUp(self): 15 | self.mod = types.ModuleType("tests.test_imports.test_alias") 16 | self.mod.__package__ = self.mod.__name__ 17 | 18 | def test_get_alias(self): 19 | lazy_import( 20 | vars(self.mod), 21 | aliases={"foo": "...test_imports"}, 22 | ) 23 | 24 | self.assertEqual(self.mod.foo, foo) 25 | 26 | def test_get_deprecated_alias(self): 27 | lazy_import( 28 | vars(self.mod), 29 | deprecated_aliases={"bar": "...test_imports"}, 30 | ) 31 | 32 | with warnings.catch_warnings(record=True) as recorded_warnings: 33 | warnings.simplefilter("always") 34 | self.assertEqual(self.mod.bar, bar) 35 | 36 | self.assertEqual(len(recorded_warnings), 1) 37 | warning = recorded_warnings[0].message 38 | self.assertEqual( 39 | str(warning), "tests.test_imports.test_alias.bar is deprecated" 40 | ) 41 | self.assertEqual(type(warning), DeprecationWarning) 42 | 43 | def test_dir(self): 44 | lazy_import( 45 | vars(self.mod), 46 | aliases={"foo": "...test_imports"}, 47 | deprecated_aliases={"bar": "...test_imports"}, 48 | ) 49 | 50 | self.assertEqual( 51 | [item for item in dir(self.mod) if not item[:2] == item[-2:] == "__"], 52 | ["bar", "foo"], 53 | ) 54 | 55 | def test_attribute_error(self): 56 | lazy_import(vars(self.mod)) 57 | 58 | with self.assertRaises(AttributeError) as raised: 59 | self.mod.foo 60 | 61 | self.assertEqual( 62 | str(raised.exception), 63 | "module 'tests.test_imports.test_alias' has no attribute 'foo'", 64 | ) 65 | -------------------------------------------------------------------------------- /tests/test_localhost.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | 3 | default_md = sha256 4 | encrypt_key = no 5 | 6 | prompt = no 7 | 8 | distinguished_name = dn 9 | x509_extensions = ext 10 | 11 | [ dn ] 12 | 13 | C = "FR" 14 | L = "Paris" 15 | O = "Aymeric Augustin" 16 | CN = "localhost" 17 | 18 | [ ext ] 19 | 20 | subjectAltName = @san 21 | 22 | [ san ] 23 | 24 | DNS.1 = localhost 25 | DNS.2 = overridden 26 | IP.3 = 127.0.0.1 27 | IP.4 = ::1 28 | -------------------------------------------------------------------------------- /tests/test_localhost.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYOOQyq8yYtn5x 3 | K3yRborFxTFse16JIVb4x/ZhZgGm49eARCi09fmczQxJdQpHz81Ij6z0xi7AUYH7 4 | 9wS8T0Lh3uGFDDS1GzITUVPIqSUi0xim2T6XPzXFVQYI1D/OjUxlHm+3/up+WwbL 5 | sBgBO/lDmzoa3ZN7kt9HQoGc/14oQz1Qsv1QTDQs69r+o7mmBJr/hf/g7S0Csyy3 6 | iC6aaq+yCUyzDbjXceTI7WJqbTGNnK0/DjdFD/SJS/uSDNEg0AH53eqcCSjm+Ei/ 7 | UF8qR5Pu4sSsNwToOW2MVgjtHFazc+kG3rzD6+3Dp+t6x6uI/npyuudOMCmOtd6z 8 | kX0UPQaNAgMBAAECggEAS4eMBztGC+5rusKTEAZKSY15l0h9HG/d/qdzJFDKsO6T 9 | /8VPZu8pk6F48kwFHFK1hexSYWq9OAcA3fBK4jDZzybZJm2+F6l5U5AsMUMMqt6M 10 | lPP8Tj8RXG433muuIkvvbL82DVLpvNu1Qv+vUvcNOpWFtY7DDv6eKjlMJ3h4/pzh 11 | 89MNt26VMCYOlq1NSjuZBzFohL2u9nsFehlOpcVsqNfNfcYCq9+5yoH8fWJP90Op 12 | hqhvqUoGLN7DRKV1f+AWHSA4nmGgvVviV5PQgMhtk5exlN7kG+rDc3LbzhefS1Sp 13 | Tat1qIgm8fK2n+Q/obQPjHOGOGuvE5cIF7E275ZKgQKBgQDt87BqALKWnbkbQnb7 14 | GS1h6LRcKyZhFbxnO2qbviBWSo15LEF8jPGV33Dj+T56hqufa/rUkbZiUbIR9yOX 15 | dnOwpAVTo+ObAwZfGfHvrnufiIbHFqJBumaYLqjRZ7AC0QtS3G+kjS9dbllrr7ok 16 | fO4JdfKRXzBJKrkQdCn8hR22rQKBgQDon0b49Dxs1EfdSDbDode2TSwE83fI3vmR 17 | SKUkNY8ma6CRbomVRWijhBM458wJeuhpjPZOvjNMsnDzGwrtdAp2VfFlMIDnA8ZC 18 | fEWIAAH2QYKXKGmkoXOcWB2QbvbI154zCm6zFGtzvRKOCGmTXuhFajO8VPwOyJVt 19 | aSJA3bLrYQKBgQDJM2/tAfAAKRdW9GlUwqI8Ep9G+/l0yANJqtTnIemH7XwYhJJO 20 | 9YJlPszfB2aMBgliQNSUHy1/jyKpzDYdITyLlPUoFwEilnkxuud2yiuf5rpH51yF 21 | hU6wyWtXvXv3tbkEdH42PmdZcjBMPQeBSN2hxEi6ISncBDL9tau26PwJ9QKBgQCs 22 | cNYl2reoXTzgtpWSNDk6NL769JjJWTFcF6QD0YhKjOI8rNpkw00sWc3+EybXqDr9 23 | c7dq6+gPZQAB1vwkxi6zRkZqIqiLl+qygnjwtkC+EhYCg7y8g8q2DUPtO7TJcb0e 24 | TQ9+xRZad8B3dZj93A8G1hF//OfU9bB/qL3xo+bsQQKBgC/9YJvgLIWA/UziLcB2 25 | 29Ai0nbPkN5df7z4PifUHHSlbQJHKak8UKbMP+8S064Ul0F7g8UCjZMk2LzSbaNY 26 | XU5+2j0sIOnGUFoSlvcpdowzYrD2LN5PkKBot7AOq/v7HlcOoR8J8RGWAMpCrHsI 27 | a/u/dlZs+/K16RcavQwx8rag 28 | -----END PRIVATE KEY----- 29 | -----BEGIN CERTIFICATE----- 30 | MIIDWTCCAkGgAwIBAgIJAOL9UKiOOxupMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV 31 | BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp 32 | bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIyMTAxNTE5Mjg0MVoYDzIwNjQxMDE0 33 | MTkyODQxWjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM 34 | EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI 35 | hvcNAQEBBQADggEPADCCAQoCggEBANg45DKrzJi2fnErfJFuisXFMWx7XokhVvjH 36 | 9mFmAabj14BEKLT1+ZzNDEl1CkfPzUiPrPTGLsBRgfv3BLxPQuHe4YUMNLUbMhNR 37 | U8ipJSLTGKbZPpc/NcVVBgjUP86NTGUeb7f+6n5bBsuwGAE7+UObOhrdk3uS30dC 38 | gZz/XihDPVCy/VBMNCzr2v6juaYEmv+F/+DtLQKzLLeILppqr7IJTLMNuNdx5Mjt 39 | YmptMY2crT8ON0UP9IlL+5IM0SDQAfnd6pwJKOb4SL9QXypHk+7ixKw3BOg5bYxW 40 | CO0cVrNz6QbevMPr7cOn63rHq4j+enK6504wKY613rORfRQ9Bo0CAwEAAaM8MDow 41 | OAYDVR0RBDEwL4IJbG9jYWxob3N0ggpvdmVycmlkZGVuhwR/AAABhxAAAAAAAAAA 42 | AAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBPNDGDdl4wsCRlDuyCHBC8o+vW 43 | Vb14thUw9Z6UrlsQRXLONxHOXbNAj1sYQACNwIWuNz36HXu5m8Xw/ID/bOhnIg+b 44 | Y6l/JU/kZQYB7SV1aR3ZdbCK0gjfkE0POBHuKOjUFIOPBCtJ4tIBUX94zlgJrR9v 45 | 2rqJC3TIYrR7pVQumHZsI5GZEMpM5NxfreWwxcgltgxmGdm7elcizHfz7k5+szwh 46 | 4eZ/rxK9bw1q8BIvVBWelRvUR55mIrCjzfZp5ZObSYQTZlW7PzXBe5Jk+1w31YHM 47 | RSBA2EpPhYlGNqPidi7bg7rnQcsc6+hE0OqzTL/hWxPm9Vbp9dj3HFTik1wa 48 | -----END CERTIFICATE----- 49 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = 3 | py39 4 | py310 5 | py311 6 | py312 7 | py313 8 | coverage 9 | ruff 10 | mypy 11 | 12 | [testenv] 13 | commands = 14 | python -W error::DeprecationWarning -W error::PendingDeprecationWarning -m unittest {posargs} 15 | pass_env = 16 | WEBSOCKETS_* 17 | deps = 18 | py311,py312,py313,coverage,maxi_cov: mitmproxy 19 | py311,py312,py313,coverage,maxi_cov: python-socks[asyncio] 20 | werkzeug 21 | 22 | [testenv:coverage] 23 | commands = 24 | python -m coverage run --source {envsitepackagesdir}/websockets,tests -m unittest {posargs} 25 | python -m coverage report --show-missing --fail-under=100 26 | deps = 27 | coverage 28 | {[testenv]deps} 29 | 30 | [testenv:maxi_cov] 31 | commands = 32 | python tests/maxi_cov.py {envsitepackagesdir} 33 | python -m coverage report --show-missing --fail-under=100 34 | deps = 35 | coverage 36 | {[testenv]deps} 37 | 38 | [testenv:ruff] 39 | commands = 40 | ruff format --check src tests 41 | ruff check src tests 42 | deps = 43 | ruff 44 | 45 | [testenv:mypy] 46 | commands = 47 | mypy --strict src 48 | deps = 49 | mypy 50 | python-socks 51 | werkzeug 52 | --------------------------------------------------------------------------------