diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
commit | 8dd16259287f58f9273002717ec4d27e97127719 (patch) | |
tree | 3863e62a53829a84037444beab3abd4ed9dfc7d0 /testing/web-platform/tests/tools | |
parent | Releasing progress-linux version 126.0.1-1~progress7.99u1. (diff) | |
download | firefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz firefox-8dd16259287f58f9273002717ec4d27e97127719.zip |
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/tools')
401 files changed, 31497 insertions, 4290 deletions
diff --git a/testing/web-platform/tests/tools/ci/azure/install_chrome.yml b/testing/web-platform/tests/tools/ci/azure/install_chrome.yml index 7599321be2..2dde99286c 100644 --- a/testing/web-platform/tests/tools/ci/azure/install_chrome.yml +++ b/testing/web-platform/tests/tools/ci/azure/install_chrome.yml @@ -1,11 +1,11 @@ steps: # The conflicting google-chrome and chromedriver casks are first uninstalled. -# The raw google-chrome-dev cask URL is used to bypass caching. +# The raw google-chrome@dev.rb cask URL is used to bypass caching. - script: | set -eux -o pipefail HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --cask google-chrome || true HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --cask chromedriver || true - curl https://raw.githubusercontent.com/Homebrew/homebrew-cask-versions/master/Casks/google-chrome-dev.rb > google-chrome-dev.rb - HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask google-chrome-dev.rb + curl https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/g/google-chrome@dev.rb > google-chrome@dev.rb + HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask google-chrome@dev.rb displayName: 'Install Chrome Dev' condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) diff --git a/testing/web-platform/tests/tools/ci/azure/install_firefox.yml b/testing/web-platform/tests/tools/ci/azure/install_firefox.yml index 73af597665..d43e28b274 100644 --- a/testing/web-platform/tests/tools/ci/azure/install_firefox.yml +++ b/testing/web-platform/tests/tools/ci/azure/install_firefox.yml @@ -1,9 +1,8 @@ steps: -# This is equivalent to `Homebrew/homebrew-cask-versions/firefox-nightly`, -# but the raw URL is used to bypass caching. +# The raw firefox@nightly.rb cask URL is used to bypass caching. - script: | set -eux -o pipefail - curl https://raw.githubusercontent.com/Homebrew/homebrew-cask-versions/master/Casks/firefox-nightly.rb > firefox-nightly.rb - HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask firefox-nightly.rb + curl https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/f/firefox@nightly.rb > firefox@nightly.rb + HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask firefox@nightly.rb displayName: 'Install Firefox Nightly' condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) diff --git a/testing/web-platform/tests/tools/ci/jobs.py b/testing/web-platform/tests/tools/ci/jobs.py index 44de9fe1ad..fe8eaae069 100644 --- a/testing/web-platform/tests/tools/ci/jobs.py +++ b/testing/web-platform/tests/tools/ci/jobs.py @@ -23,7 +23,7 @@ EXCLUDES = [ ] # Rules are just regex on the path, with a leading ! indicating a regex that must not -# match for the job. Paths should be kept in sync with update-built-tests.sh. +# match for the job. Paths should be kept in sync with scripts in update_built.py. job_path_map = { "affected_tests": [".*/.*", "!resources/(?!idlharness.js)"] + EXCLUDES, "stability": [".*/.*", "!resources/.*"] + EXCLUDES, @@ -32,10 +32,11 @@ job_path_map = { "resources_unittest": ["resources/", "tools/"], "tools_unittest": ["tools/"], "wptrunner_unittest": ["tools/"], - "update_built": ["update-built-tests\\.sh", - "conformance-checkers/", + "update_built": ["conformance-checkers/", + "css/css-images/", "css/css-ui/", "css/css-writing-modes/", + "fetch/metadata/", "html/", "infrastructure/", "mimesniff/"], diff --git a/testing/web-platform/tests/tools/ci/requirements_build.txt b/testing/web-platform/tests/tools/ci/requirements_build.txt index 54f21efbd9..7b4f8619b2 100644 --- a/testing/web-platform/tests/tools/ci/requirements_build.txt +++ b/testing/web-platform/tests/tools/ci/requirements_build.txt @@ -1,5 +1,5 @@ -cairocffi==1.6.1 -fonttools==4.47.2 +cairocffi==1.7.0 +fonttools==4.51.0 genshi==0.7.7 jinja2==3.1.3 pyyaml==6.0.1 diff --git a/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt b/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt index 7505a98d9f..8e178d1d2c 100644 --- a/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt +++ b/testing/web-platform/tests/tools/ci/requirements_macos_color_profile.txt @@ -1,4 +1,4 @@ -pyobjc-core==9.2 +pyobjc-core==10.2 pyobjc-framework-Cocoa==9.2 pyobjc-framework-ColorSync==9.2 pyobjc-framework-Quartz==9.2 diff --git a/testing/web-platform/tests/tools/ci/requirements_tc.txt b/testing/web-platform/tests/tools/ci/requirements_tc.txt index e1ae4dbf70..aa57643b9b 100644 --- a/testing/web-platform/tests/tools/ci/requirements_tc.txt +++ b/testing/web-platform/tests/tools/ci/requirements_tc.txt @@ -1,4 +1,4 @@ -pygithub==2.2.0 +pygithub==2.3.0 pyyaml==6.0.1 requests==2.31.0 -taskcluster==60.4.1 +taskcluster==64.2.7 diff --git a/testing/web-platform/tests/tools/ci/tc/tasks/test.yml b/testing/web-platform/tests/tools/ci/tc/tasks/test.yml index c172e6b731..a9ca07c6ce 100644 --- a/testing/web-platform/tests/tools/ci/tc/tasks/test.yml +++ b/testing/web-platform/tests/tools/ci/tc/tasks/test.yml @@ -115,25 +115,24 @@ components: chunks-override: testharness: 24 - tox-python3_7: + tox-python3_8: env: - TOXENV: py37 + TOXENV: py38 PY_COLORS: "0" install: - - python3.7 - - python3.7-distutils - - python3.7-dev - - python3.7-venv + - python3.8 + - python3.8-distutils + - python3.8-dev + - python3.8-venv - tox-python3_11: + tox-python3_12: env: - TOXENV: py311 + TOXENV: py312 PY_COLORS: "0" install: - - python3.11 - - python3.11-distutils - - python3.11-dev - - python3.11-venv + - python3.12 + - python3.12-dev + - python3.12-venv tests-affected: options: browser: @@ -438,13 +437,13 @@ tasks: - update_built command: "./tools/ci/ci_built_diff.sh" - - tools/ unittests (Python 3.7): + - tools/ unittests (Python 3.8): description: >- - Unit tests for tools running under Python 3.7, excluding wptrunner + Unit tests for tools running under Python 3.8, excluding wptrunner use: - wpt-base - trigger-pr - - tox-python3_7 + - tox-python3_8 command: ./tools/ci/ci_tools_unittest.sh env: HYPOTHESIS_PROFILE: ci @@ -452,13 +451,13 @@ tasks: run-job: - tools_unittest - - tools/ unittests (Python 3.11): + - tools/ unittests (Python 3.12): description: >- - Unit tests for tools running under Python 3.11, excluding wptrunner + Unit tests for tools running under Python 3.12, excluding wptrunner use: - wpt-base - trigger-pr - - tox-python3_11 + - tox-python3_12 command: ./tools/ci/ci_tools_unittest.sh env: HYPOTHESIS_PROFILE: ci @@ -466,13 +465,13 @@ tasks: run-job: - tools_unittest - - tools/ integration tests (Python 3.7): + - tools/ integration tests (Python 3.8): description: >- - Integration tests for tools running under Python 3.7 + Integration tests for tools running under Python 3.8 use: - wpt-base - trigger-pr - - tox-python3_7 + - tox-python3_8 command: ./tools/ci/ci_tools_integration_test.sh install: - libnss3-tools @@ -488,13 +487,13 @@ tasks: run-job: - wpt_integration - - tools/ integration tests (Python 3.11): + - tools/ integration tests (Python 3.12): description: >- - Integration tests for tools running under Python 3.11 + Integration tests for tools running under Python 3.12 use: - wpt-base - trigger-pr - - tox-python3_11 + - tox-python3_12 command: ./tools/ci/ci_tools_integration_test.sh install: - libnss3-tools @@ -510,13 +509,13 @@ tasks: run-job: - wpt_integration - - resources/ tests (Python 3.7): + - resources/ tests (Python 3.8): description: >- - Tests for testharness.js and other files in resources/ under Python 3.7 + Tests for testharness.js and other files in resources/ under Python 3.8 use: - wpt-base - trigger-pr - - tox-python3_7 + - tox-python3_8 command: ./tools/ci/ci_resources_unittest.sh install: - libnss3-tools @@ -529,13 +528,13 @@ tasks: run-job: - resources_unittest - - resources/ tests (Python 3.11): + - resources/ tests (Python 3.12): description: >- - Tests for testharness.js and other files in resources/ under Python 3.11 + Tests for testharness.js and other files in resources/ under Python 3.12 use: - wpt-base - trigger-pr - - tox-python3_11 + - tox-python3_12 command: ./tools/ci/ci_resources_unittest.sh install: - libnss3-tools diff --git a/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py b/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py index 62bb09a1c3..dd8d732654 100644 --- a/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py +++ b/testing/web-platform/tests/tools/ci/tc/tests/test_valid.py @@ -202,12 +202,12 @@ def test_verify_payload(): 'lint']), ("pr_event.json", True, {".taskcluster.yml", ".travis.yml", "tools/ci/start.sh"}, ['lint', - 'tools/ unittests (Python 3.7)', - 'tools/ unittests (Python 3.11)', - 'tools/ integration tests (Python 3.7)', - 'tools/ integration tests (Python 3.11)', - 'resources/ tests (Python 3.7)', - 'resources/ tests (Python 3.11)', + 'tools/ unittests (Python 3.8)', + 'tools/ unittests (Python 3.12)', + 'tools/ integration tests (Python 3.8)', + 'tools/ integration tests (Python 3.12)', + 'resources/ tests (Python 3.8)', + 'resources/ tests (Python 3.12)', 'download-firefox-nightly', 'infrastructure/ tests', 'sink-task']), @@ -224,8 +224,8 @@ def test_verify_payload(): 'sink-task']), ("pr_event_tests_affected.json", True, {"resources/testharness.js"}, ['lint', - 'resources/ tests (Python 3.7)', - 'resources/ tests (Python 3.11)', + 'resources/ tests (Python 3.8)', + 'resources/ tests (Python 3.12)', 'download-firefox-nightly', 'infrastructure/ tests', 'sink-task']), diff --git a/testing/web-platform/tests/tools/ci/update_built.py b/testing/web-platform/tests/tools/ci/update_built.py index 8e0f18589d..929b09f9fe 100644 --- a/testing/web-platform/tests/tools/ci/update_built.py +++ b/testing/web-platform/tests/tools/ci/update_built.py @@ -9,6 +9,7 @@ logger = logging.getLogger() wpt_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) +# These paths should be kept in sync with job_path_map in jobs.py. scripts = { "canvas": ["html/canvas/tools/gentest.py"], "conformance-checkers": ["conformance-checkers/tools/dl.py", diff --git a/testing/web-platform/tests/tools/manifest/item.py b/testing/web-platform/tests/tools/manifest/item.py index 86f7bd6020..e25f7ca2c2 100644 --- a/testing/web-platform/tests/tools/manifest/item.py +++ b/testing/web-platform/tests/tools/manifest/item.py @@ -279,7 +279,7 @@ class PrintRefTest(RefTest): @property def page_ranges(self) -> PageRanges: - return self._extras.get("page_ranges", {}) + return cast(PageRanges, self._extras.get("page_ranges", {})) def to_json(self): # type: ignore rv = super().to_json() diff --git a/testing/web-platform/tests/tools/manifest/requirements.txt b/testing/web-platform/tests/tools/manifest/requirements.txt index d7c173723e..70ad0df0e9 100644 --- a/testing/web-platform/tests/tools/manifest/requirements.txt +++ b/testing/web-platform/tests/tools/manifest/requirements.txt @@ -1 +1 @@ -zstandard==0.21.0 +zstandard==0.22.0 diff --git a/testing/web-platform/tests/tools/manifest/sourcefile.py b/testing/web-platform/tests/tools/manifest/sourcefile.py index 71eab54bea..3563fb9e5e 100644 --- a/testing/web-platform/tests/tools/manifest/sourcefile.py +++ b/testing/web-platform/tests/tools/manifest/sourcefile.py @@ -4,8 +4,8 @@ import os from collections import deque from fnmatch import fnmatch from io import BytesIO -from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List, Optional, Pattern, - Set, Text, Tuple, Union, cast) +from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List, + Optional, Pattern, Set, Text, Tuple, TypedDict, Union, cast) from urllib.parse import urljoin try: @@ -68,7 +68,13 @@ def read_script_metadata(f: BinaryIO, regexp: Pattern[bytes]) -> Iterable[Tuple[ yield (m.groups()[0].decode("utf8"), m.groups()[1].decode("utf8")) -_any_variants: Dict[Text, Dict[Text, Any]] = { +class VariantData(TypedDict, total=False): + suffix: str + force_https: bool + longhand: Set[str] + + +_any_variants: Dict[Text, VariantData] = { "window": {"suffix": ".any.html"}, "window-module": {}, "serviceworker": {"force_https": True}, @@ -205,9 +211,11 @@ class SourceFile: type_flag = None if "-" in name: - type_flag = name.rsplit("-", 1)[1].split(".")[0] - - meta_flags = name.split(".")[1:] + type_meta = name.rsplit("-", 1)[1].split(".") + type_flag = type_meta[0] + meta_flags = type_meta[1:] + else: + meta_flags = name.split(".")[1:] self.tests_root: Text = tests_root self.rel_path: Text = rel_path diff --git a/testing/web-platform/tests/tools/mypy.ini b/testing/web-platform/tests/tools/mypy.ini index 1e58e9625e..cc22a770b0 100644 --- a/testing/web-platform/tests/tools/mypy.ini +++ b/testing/web-platform/tests/tools/mypy.ini @@ -57,9 +57,6 @@ ignore_missing_imports = True [mypy-marionette_driver.*] ignore_missing_imports = True -[mypy-mod_pywebsocket.*] -ignore_missing_imports = True - [mypy-mozcrash.*] ignore_missing_imports = True @@ -108,6 +105,9 @@ ignore_missing_imports = True [mypy-pytest.*] ignore_missing_imports = True +[mypy-pywebsocket3.*] +ignore_missing_imports = True + [mypy-selenium.*] ignore_missing_imports = True diff --git a/testing/web-platform/tests/tools/pytest.ini b/testing/web-platform/tests/tools/pytest.ini index 81666e01db..650d07caf3 100644 --- a/testing/web-platform/tests/tools/pytest.ini +++ b/testing/web-platform/tests/tools/pytest.ini @@ -15,7 +15,7 @@ filterwarnings = # ignore mozinfo deprecation warnings ignore:distutils Version classes are deprecated\. Use packaging\.version instead\.:DeprecationWarning:mozinfo # ingore mozinfo's dependency on distro - ignore:distro\.linux_distribution\(\) is deprecated\. It should only be used as a compatibility shim with Python's platform\.linux_distribution\(\)\. Please use distro\.id\(\), distro\.version\(\) and distro\.name\(\) instead\.:DeprecationWarning + ignore:distro\.linux_distribution\(\) is deprecated\. It should only be used as a compatibility shim with Python\'s platform\.linux_distribution\(\)\. Please use distro\.id\(\), distro\.version\(\) and distro\.name\(\) instead\.:DeprecationWarning # ignore mozversion deprecation warnings ignore:This method will be removed in .*\.\s+Use 'parser\.read_file\(\)' instead\.:DeprecationWarning:mozversion # ignore mozversion not cleanly closing .ini files @@ -32,3 +32,8 @@ filterwarnings = always:the imp module is deprecated in favour of importlib:DeprecationWarning # https://github.com/web-platform-tests/wpt/issues/39827 always:pkg_resources is deprecated as an API:DeprecationWarning + # taskcluster and jsone use datetime.utcnow() + ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC:DeprecationWarning:jsone + ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC:DeprecationWarning:taskcluster + # mozfile not yet updated to pass a filter argument to tarfile.extract + ignore:Python 3\.14 will, by default, filter extracted tar archives and reject files or modify their metadata\. Use the filter argument to control this behavior\.:DeprecationWarning diff --git a/testing/web-platform/tests/tools/requirements_flake8.txt b/testing/web-platform/tests/tools/requirements_flake8.txt index fc1f92a69f..3f7f3121ca 100644 --- a/testing/web-platform/tests/tools/requirements_flake8.txt +++ b/testing/web-platform/tests/tools/requirements_flake8.txt @@ -1,7 +1,5 @@ flake8==5.0.4; python_version < '3.9' -pycodestyle==2.9.1; python_version < '3.8' -pyflakes==2.5.0; python_version < '3.8' flake8==6.1.0; python_version >= '3.9' -pycodestyle==2.11.0; python_version >= '3.9' +pycodestyle==2.11.1; python_version >= '3.9' pyflakes==3.1.0; python_version >= '3.9' pep8-naming==0.13.3 diff --git a/testing/web-platform/tests/tools/requirements_mypy.txt b/testing/web-platform/tests/tools/requirements_mypy.txt index c3db2292af..3b1d3b03d6 100644 --- a/testing/web-platform/tests/tools/requirements_mypy.txt +++ b/testing/web-platform/tests/tools/requirements_mypy.txt @@ -1,14 +1,14 @@ -mypy==1.4.1 +mypy==1.10.0 mypy-extensions==1.0.0 toml==0.10.2 tomli==2.0.1 typed-ast==1.5.5 types-atomicwrites==1.4.5.1 -types-python-dateutil==2.8.19.14 +types-python-dateutil==2.9.0.20240316 types-PyYAML==6.0.12.12 types-requests==2.31.0.20231231 -types-setuptools==68.0.0.3 -types-six==1.16.21.9 +types-setuptools==69.5.0.20240423 +types-six==1.16.21.20240425 types-ujson==5.9.0.0 types-urllib3==1.26.25.14 typing_extensions==4.7.1 diff --git a/testing/web-platform/tests/tools/requirements_pytest.txt b/testing/web-platform/tests/tools/requirements_pytest.txt index 9034cda719..64d38583a2 100644 --- a/testing/web-platform/tests/tools/requirements_pytest.txt +++ b/testing/web-platform/tests/tools/requirements_pytest.txt @@ -1,3 +1,3 @@ pytest==7.4.4 pytest-cov==4.1.0 -hypothesis==6.78.2 +hypothesis==6.100.2 diff --git a/testing/web-platform/tests/tools/requirements_tests.txt b/testing/web-platform/tests/tools/requirements_tests.txt index 6455286736..24785f3531 100644 --- a/testing/web-platform/tests/tools/requirements_tests.txt +++ b/testing/web-platform/tests/tools/requirements_tests.txt @@ -1,6 +1,6 @@ -httpx[http2]==0.24.1 -json-e==4.5.3 +httpx[http2]==0.27.0 +json-e==4.7.0 jsonschema==4.17.3 pyyaml==6.0.1 -taskcluster==60.4.1 +taskcluster==64.2.7 mozterm==1.0.0 diff --git a/testing/web-platform/tests/tools/serve/serve.py b/testing/web-platform/tests/tools/serve/serve.py index 300f8270a6..42d8091802 100644 --- a/testing/web-platform/tests/tools/serve/serve.py +++ b/testing/web-platform/tests/tools/serve/serve.py @@ -30,7 +30,7 @@ from wptserve import config from wptserve.handlers import filesystem_path, wrap_pipeline from wptserve.response import ResponseHeaders from wptserve.utils import get_port, HTTPException, http2_compatible -from mod_pywebsocket import standalone as pywebsocket +from pywebsocket3 import standalone as pywebsocket EDIT_HOSTS_HELP = ("Please ensure all the necessary WPT subdomains " @@ -829,7 +829,8 @@ def start_http_server(logger, host, port, paths, routes, bind_address, config, * key_file=None, certificate=None, latency=kwargs.get("latency")) - except Exception: + except Exception as error: + logger.critical(f"start_http_server: Caught exception from wptserve.WebTestHttpd: {error}") startup_failed(logger) @@ -847,7 +848,8 @@ def start_https_server(logger, host, port, paths, routes, bind_address, config, certificate=config.ssl_config["cert_path"], encrypt_after_connect=config.ssl_config["encrypt_after_connect"], latency=kwargs.get("latency")) - except Exception: + except Exception as error: + logger.critical(f"start_https_server: Caught exception from wptserve.WebTestHttpd: {error}") startup_failed(logger) @@ -868,7 +870,8 @@ def start_http2_server(logger, host, port, paths, routes, bind_address, config, encrypt_after_connect=config.ssl_config["encrypt_after_connect"], latency=kwargs.get("latency"), http2=True) - except Exception: + except Exception as error: + logger.critical(f"start_http2_server: Caught exception from wptserve.WebTestHttpd: {error}") startup_failed(logger) @@ -935,7 +938,8 @@ def start_ws_server(logger, host, port, paths, routes, bind_address, config, **k config.paths["ws_doc_root"], bind_address, ssl_config=None) - except Exception: + except Exception as error: + logger.critical(f"start_ws_server: Caught exception from WebSocketDomain: {error}") startup_failed(logger) @@ -947,7 +951,8 @@ def start_wss_server(logger, host, port, paths, routes, bind_address, config, ** config.paths["ws_doc_root"], bind_address, config.ssl_config) - except Exception: + except Exception as error: + logger.critical(f"start_wss_server: Caught exception from WebSocketDomain: {error}") startup_failed(logger) diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore b/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore deleted file mode 100644 index 70f2867054..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.pyc -build/ -*.egg-info/ -dist/ diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml b/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml deleted file mode 100644 index 2065a644dd..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: python -python: - - 2.7 - - 3.5 - - 3.6 - - 3.7 - - 3.8 - - nightly - -matrix: - allow_failures: - - python: 3.5, nightly -install: - - pip install six yapf -script: - - python test/run_all.py - - yapf --diff --recursive . diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING b/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING deleted file mode 100644 index f975be126f..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING +++ /dev/null @@ -1,30 +0,0 @@ -# How to Contribute - -We'd love to accept your patches and contributions to this project. There are -just a few small guidelines you need to follow. - -## Contributor License Agreement - -Contributions to this project must be accompanied by a Contributor License -Agreement. You (or your employer) retain the copyright to your contribution; -this simply gives us permission to use and redistribute your contributions as -part of the project. Head over to <https://cla.developers.google.com/> to see -your current agreements on file or to sign a new one. - -You generally only need to submit a CLA once, so if you've already submitted one -(even if it was for a different project), you probably don't need to do it -again. - -## Code reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. -For instructions for contributing code, please read: -https://github.com/google/pywebsocket/wiki/CodeReviewInstruction - -## Community Guidelines - -This project follows -[Google's Open Source Community Guidelines](https://opensource.google/conduct/). diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in index 19256882c5..116235d18f 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in @@ -2,5 +2,5 @@ include COPYING include MANIFEST.in include README recursive-include example *.py -recursive-include mod_pywebsocket *.py +recursive-include pywebsocket3 *.py recursive-include test *.py diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/PKG-INFO b/testing/web-platform/tests/tools/third_party/pywebsocket3/PKG-INFO new file mode 100644 index 0000000000..289dfa8649 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/PKG-INFO @@ -0,0 +1,13 @@ +Metadata-Version: 2.1 +Name: pywebsocket3 +Version: 4.0.2 +Summary: Standalone WebSocket Server for testing purposes. +Home-page: https://github.com/GoogleChromeLabs/pywebsocket3 +Author: Yuzo Fujishima +Author-email: yuzo@chromium.org +License: See LICENSE +Requires-Python: >=2.7 +License-File: LICENSE +Requires-Dist: six + +pywebsocket3 is a standalone server for the WebSocket Protocol (RFC 6455). See pywebsocket3/__init__.py for more detail. diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md index 8684f2cc7e..b46b416735 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md @@ -1,13 +1,14 @@ # pywebsocket3 # -The pywebsocket project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server. +The pywebsocket3 project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server. pywebsocket is intended for **testing** or **experimental** purposes. Run this to read the general document: -``` -$ pydoc mod_pywebsocket + +```bash +pydoc pywebsocket3 ``` Please see [Wiki](https://github.com/GoogleChromeLabs/pywebsocket3/wiki) for more details. @@ -15,22 +16,27 @@ Please see [Wiki](https://github.com/GoogleChromeLabs/pywebsocket3/wiki) for mor # INSTALL # To install this package to the system, run this: -``` -$ python setup.py build -$ sudo python setup.py install + +```bash +python setup.py build +sudo python setup.py install ``` To install this package as a normal user, run this instead: +```bash +python setup.py build +python setup.py install --user ``` -$ python setup.py build -$ python setup.py install --user -``` + # LAUNCH # To use pywebsocket as standalone server, run this to read the document: + +```bash +pydoc pywebsocket3.standalone ``` -$ pydoc mod_pywebsocket.standalone -``` + # Disclaimer # + This is not an officially supported Google product diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py index 1b719ca897..1bad8c02f2 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py @@ -28,7 +28,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import -from mod_pywebsocket import handshake + +from pywebsocket3 import handshake def web_socket_do_extra_handshake(request): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py index d4c240bf2c..c0495c7107 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py @@ -28,7 +28,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import -from mod_pywebsocket import handshake + +from pywebsocket3 import handshake def web_socket_do_extra_handshake(request): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html deleted file mode 100644 index 869cd7e1ee..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html +++ /dev/null @@ -1,134 +0,0 @@ -<!-- -Copyright 2013, Google Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---> - -<html> -<head> -<title>ArrayBuffer benchmark</title> -<script src="util.js"></script> -<script> -var PRINT_SIZE = true; - -// Initial size of arrays. -var START_SIZE = 10 * 1024; -// Stops benchmark when the size of an array exceeds this threshold. -var STOP_THRESHOLD = 100000 * 1024; -// If the size of each array is small, write/read the array multiple times -// until the sum of sizes reaches this threshold. -var MIN_TOTAL = 100000 * 1024; -var MULTIPLIERS = [5, 2]; - -// Repeat benchmark for several times to measure performance of optimized -// (such as JIT) run. -var REPEAT_FOR_WARMUP = 3; - -function writeBenchmark(size, minTotal) { - var totalSize = 0; - while (totalSize < minTotal) { - var arrayBuffer = new ArrayBuffer(size); - - // Write 'a's. - fillArrayBuffer(arrayBuffer, 0x61); - - totalSize += size; - } - return totalSize; -} - -function readBenchmark(size, minTotal) { - var totalSize = 0; - while (totalSize < minTotal) { - var arrayBuffer = new ArrayBuffer(size); - - if (!verifyArrayBuffer(arrayBuffer, 0x00)) { - queueLog('Verification failed'); - return -1; - } - - totalSize += size; - } - return totalSize; -} - -function runBenchmark(benchmarkFunction, - size, - stopThreshold, - minTotal, - multipliers, - multiplierIndex) { - while (size <= stopThreshold) { - var maxSpeed = 0; - - for (var i = 0; i < REPEAT_FOR_WARMUP; ++i) { - var startTimeInMs = getTimeStamp(); - - var totalSize = benchmarkFunction(size, minTotal); - - maxSpeed = Math.max(maxSpeed, - calculateSpeedInKB(totalSize, startTimeInMs)); - } - queueLog(formatResultInKiB(size, maxSpeed, PRINT_SIZE)); - - size *= multipliers[multiplierIndex]; - multiplierIndex = (multiplierIndex + 1) % multipliers.length; - } -} - -function runBenchmarks() { - queueLog('Message size in KiB, Speed in kB/s'); - - queueLog('Write benchmark'); - runBenchmark( - writeBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0); - queueLog('Finished'); - - queueLog('Read benchmark'); - runBenchmark( - readBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0); - addToLog('Finished'); -} - -function init() { - logBox = document.getElementById('log'); - - queueLog(window.navigator.userAgent.toLowerCase()); - - addToLog('Started...'); - - setTimeout(runBenchmarks, 0); -} - -</script> -</head> -<body onload="init()"> -<textarea - id="log" rows="50" style="width: 100%" readonly></textarea> -</body> -</html> diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py index 2df50e77db..9ea2f93159 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py @@ -35,7 +35,9 @@ value. <count> must be an integer value. """ from __future__ import absolute_import + import time + from six.moves import range diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html deleted file mode 100644 index f1e5c97b3a..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html +++ /dev/null @@ -1,175 +0,0 @@ -<!-- -Copyright 2013, Google Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---> - -<html> -<head> -<title>WebSocket benchmark</title> -<script src="util_main.js"></script> -<script src="util.js"></script> -<script src="benchmark.js"></script> -<script> -var addressBox = null; - -function getConfig() { - return { - prefixUrl: addressBox.value, - printSize: getBoolFromCheckBox('printsize'), - numSockets: getIntFromInput('numsockets'), - // Initial size of messages. - numIterations: getIntFromInput('numiterations'), - numWarmUpIterations: getIntFromInput('numwarmupiterations'), - startSize: getIntFromInput('startsize'), - // Stops benchmark when the size of message exceeds this threshold. - stopThreshold: getIntFromInput('stopthreshold'), - // If the size of each message is small, send/receive multiple messages - // until the sum of sizes reaches this threshold. - minTotal: getIntFromInput('mintotal'), - multipliers: getFloatArrayFromInput('multipliers'), - verifyData: getBoolFromCheckBox('verifydata'), - addToLog: addToLog, - addToSummary: addToSummary, - measureValue: measureValue, - notifyAbort: notifyAbort - }; -} - -function onSendBenchmark() { - var config = getConfig(); - doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark'); -} - -function onReceiveBenchmark() { - var config = getConfig(); - doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark'); -} - -function onBatchBenchmark() { - var config = getConfig(); - doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark'); -} - -function onStop() { - var config = getConfig(); - doAction(config, getBoolFromCheckBox('worker'), 'stop'); -} - -function init() { - addressBox = document.getElementById('address'); - logBox = document.getElementById('log'); - - summaryBox = document.getElementById('summary'); - - var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://'; - var defaultAddress = scheme + window.location.host + '/benchmark_helper'; - - addressBox.value = defaultAddress; - - addToLog(window.navigator.userAgent.toLowerCase()); - addToSummary(window.navigator.userAgent.toLowerCase()); - - if (!('WebSocket' in window)) { - addToLog('WebSocket is not available'); - } - - initWorker(''); -} -</script> -</head> -<body onload="init()"> - -<div id="benchmark_div"> - url <input type="text" id="address" size="40"> - <input type="button" value="send" onclick="onSendBenchmark()"> - <input type="button" value="receive" onclick="onReceiveBenchmark()"> - <input type="button" value="batch" onclick="onBatchBenchmark()"> - <input type="button" value="stop" onclick="onStop()"> - - <br/> - - <input type="checkbox" id="printsize" checked> - <label for="printsize">Print size and time per message</label> - <input type="checkbox" id="verifydata" checked> - <label for="verifydata">Verify data</label> - <input type="checkbox" id="worker"> - <label for="worker">Run on worker</label> - - <br/> - - Parameters: - - <br/> - - <table> - <tr> - <td>Num sockets</td> - <td><input type="text" id="numsockets" value="1"></td> - </tr> - <tr> - <td>Number of iterations</td> - <td><input type="text" id="numiterations" value="1"></td> - </tr> - <tr> - <td>Number of warm-up iterations</td> - <td><input type="text" id="numwarmupiterations" value="0"></td> - </tr> - <tr> - <td>Start size</td> - <td><input type="text" id="startsize" value="10240"></td> - </tr> - <tr> - <td>Stop threshold</td> - <td><input type="text" id="stopthreshold" value="102400000"></td> - </tr> - <tr> - <td>Minimum total</td> - <td><input type="text" id="mintotal" value="102400000"></td> - </tr> - <tr> - <td>Multipliers</td> - <td><input type="text" id="multipliers" value="5, 2"></td> - </tr> - </table> -</div> - -<div id="log_div"> - <textarea - id="log" rows="20" style="width: 100%" readonly></textarea> -</div> -<div id="summary_div"> - Summary - <textarea - id="summary" rows="20" style="width: 100%" readonly></textarea> -</div> - -Note: Effect of RTT is not eliminated. - -</body> -</html> diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js deleted file mode 100644 index 2701472a4f..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -if (typeof importScripts !== "undefined") { - // Running on a worker - importScripts('util.js', 'util_worker.js'); -} - -// Namespace for holding globals. -var benchmark = {startTimeInMs: 0}; - -var sockets = []; -var numEstablishedSockets = 0; - -var timerID = null; - -function destroySocket(socket) { - socket.onopen = null; - socket.onmessage = null; - socket.onerror = null; - socket.onclose = null; - socket.close(); -} - -function destroyAllSockets() { - for (var i = 0; i < sockets.length; ++i) { - destroySocket(sockets[i]); - } - sockets = []; -} - -function sendBenchmarkStep(size, config, isWarmUp) { - timerID = null; - - var totalSize = 0; - var totalReplied = 0; - - var onMessageHandler = function(event) { - if (!verifyAcknowledgement(config, event.data, size)) { - destroyAllSockets(); - config.notifyAbort(); - return; - } - - totalReplied += size; - - if (totalReplied < totalSize) { - return; - } - - calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize, - isWarmUp); - - runNextTask(config); - }; - - for (var i = 0; i < sockets.length; ++i) { - var socket = sockets[i]; - socket.onmessage = onMessageHandler; - } - - var dataArray = []; - - while (totalSize < config.minTotal) { - var buffer = new ArrayBuffer(size); - - fillArrayBuffer(buffer, 0x61); - - dataArray.push(buffer); - totalSize += size; - } - - benchmark.startTimeInMs = getTimeStamp(); - - totalSize = 0; - - var socketIndex = 0; - var dataIndex = 0; - while (totalSize < config.minTotal) { - var command = ['send']; - command.push(config.verifyData ? '1' : '0'); - sockets[socketIndex].send(command.join(' ')); - sockets[socketIndex].send(dataArray[dataIndex]); - socketIndex = (socketIndex + 1) % sockets.length; - - totalSize += size; - ++dataIndex; - } -} - -function receiveBenchmarkStep(size, config, isWarmUp) { - timerID = null; - - var totalSize = 0; - var totalReplied = 0; - - var onMessageHandler = function(event) { - var bytesReceived = event.data.byteLength; - if (bytesReceived != size) { - config.addToLog('Expected ' + size + 'B but received ' + - bytesReceived + 'B'); - destroyAllSockets(); - config.notifyAbort(); - return; - } - - if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) { - config.addToLog('Response verification failed'); - destroyAllSockets(); - config.notifyAbort(); - return; - } - - totalReplied += bytesReceived; - - if (totalReplied < totalSize) { - return; - } - - calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize, - isWarmUp); - - runNextTask(config); - }; - - for (var i = 0; i < sockets.length; ++i) { - var socket = sockets[i]; - socket.binaryType = 'arraybuffer'; - socket.onmessage = onMessageHandler; - } - - benchmark.startTimeInMs = getTimeStamp(); - - var socketIndex = 0; - while (totalSize < config.minTotal) { - sockets[socketIndex].send('receive ' + size); - socketIndex = (socketIndex + 1) % sockets.length; - - totalSize += size; - } -} - -function createSocket(config) { - // TODO(tyoshino): Add TCP warm up. - var url = config.prefixUrl; - - config.addToLog('Connect ' + url); - - var socket = new WebSocket(url); - socket.onmessage = function(event) { - config.addToLog('Unexpected message received. Aborting.'); - }; - socket.onerror = function() { - config.addToLog('Error'); - }; - socket.onclose = function(event) { - config.addToLog('Closed'); - config.notifyAbort(); - }; - return socket; -} - -function startBenchmark(config) { - clearTimeout(timerID); - destroyAllSockets(); - - numEstablishedSockets = 0; - - for (var i = 0; i < config.numSockets; ++i) { - var socket = createSocket(config); - socket.onopen = function() { - config.addToLog('Opened'); - - ++numEstablishedSockets; - - if (numEstablishedSockets == sockets.length) { - runNextTask(config); - } - }; - sockets.push(socket); - } -} - -function getConfigString(config) { - return '(WebSocket' + - ', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') + - ', numSockets=' + config.numSockets + - ', numIterations=' + config.numIterations + - ', verifyData=' + config.verifyData + - ', minTotal=' + config.minTotal + - ', numWarmUpIterations=' + config.numWarmUpIterations + - ')'; -} - -function batchBenchmark(config) { - config.addToLog('Batch benchmark'); - config.addToLog(buildLegendString(config)); - - tasks = []; - clearAverageData(); - addTasks(config, sendBenchmarkStep); - addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config)); - addTasks(config, receiveBenchmarkStep); - addResultReportingTask(config, 'Receive Benchmark ' + - getConfigString(config)); - startBenchmark(config); -} - -function cleanup() { - destroyAllSockets(); -} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py index fc17533335..9ea9f56422 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py @@ -27,7 +27,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Handler for benchmark.html.""" + from __future__ import absolute_import + import six diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py index 8f0005ffea..2463bc7b31 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py @@ -28,10 +28,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import -import struct -from mod_pywebsocket import common -from mod_pywebsocket import stream +from pywebsocket3 import common def web_socket_do_extra_handshake(request): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html deleted file mode 100644 index ccd6d8f806..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html +++ /dev/null @@ -1,317 +0,0 @@ -<!-- -Copyright 2011, Google Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---> - -<!-- -A simple console for testing WebSocket server. - -Type an address into the top text input and click connect to establish -WebSocket. Then, type some message into the bottom text input and click send -to send the message. Received/sent messages and connection state will be shown -on the middle textarea. ---> - -<html> -<head> -<title>WebSocket console</title> -<script> -var socket = null; - -var showTimeStamp = false; - -var addressBox = null; -var protocolsBox = null; -var logBox = null; -var messageBox = null; -var fileBox = null; -var codeBox = null; -var reasonBox = null; - -function getTimeStamp() { - return new Date().getTime(); -} - -function addToLog(log) { - if (showTimeStamp) { - logBox.value += '[' + getTimeStamp() + '] '; - } - logBox.value += log + '\n' - // Large enough to keep showing the latest message. - logBox.scrollTop = 1000000; -} - -function setbinarytype(binaryType) { - if (!socket) { - addToLog('Not connected'); - return; - } - - socket.binaryType = binaryType; - addToLog('Set binaryType to ' + binaryType); -} - -function send() { - if (!socket) { - addToLog('Not connected'); - return; - } - - socket.send(messageBox.value); - addToLog('> ' + messageBox.value); - messageBox.value = ''; -} - -function sendfile() { - if (!socket) { - addToLog('Not connected'); - return; - } - - var files = fileBox.files; - - if (files.length == 0) { - addToLog('File not selected'); - return; - } - - socket.send(files[0]); - addToLog('> Send ' + files[0].name); -} - -function parseProtocols(protocolsText) { - var protocols = protocolsText.split(','); - for (var i = 0; i < protocols.length; ++i) { - protocols[i] = protocols[i].trim(); - } - - if (protocols.length == 0) { - // Don't pass. - protocols = null; - } else if (protocols.length == 1) { - if (protocols[0].length == 0) { - // Don't pass. - protocols = null; - } else { - // Pass as a string. - protocols = protocols[0]; - } - } - - return protocols; -} - -function connect() { - var url = addressBox.value; - var protocols = parseProtocols(protocolsBox.value); - - if ('WebSocket' in window) { - if (protocols) { - socket = new WebSocket(url, protocols); - } else { - socket = new WebSocket(url); - } - } else { - return; - } - - socket.onopen = function () { - var extraInfo = []; - if (('protocol' in socket) && socket.protocol) { - extraInfo.push('protocol = ' + socket.protocol); - } - if (('extensions' in socket) && socket.extensions) { - extraInfo.push('extensions = ' + socket.extensions); - } - - var logMessage = 'Opened'; - if (extraInfo.length > 0) { - logMessage += ' (' + extraInfo.join(', ') + ')'; - } - addToLog(logMessage); - }; - socket.onmessage = function (event) { - if (('ArrayBuffer' in window) && (event.data instanceof ArrayBuffer)) { - addToLog('< Received an ArrayBuffer of ' + event.data.byteLength + - ' bytes') - } else if (('Blob' in window) && (event.data instanceof Blob)) { - addToLog('< Received a Blob of ' + event.data.size + ' bytes') - } else { - addToLog('< ' + event.data); - } - }; - socket.onerror = function () { - addToLog('Error'); - }; - socket.onclose = function (event) { - var logMessage = 'Closed ('; - if ((arguments.length == 1) && ('CloseEvent' in window) && - (event instanceof CloseEvent)) { - logMessage += 'wasClean = ' + event.wasClean; - // code and reason are present only for - // draft-ietf-hybi-thewebsocketprotocol-06 and later - if ('code' in event) { - logMessage += ', code = ' + event.code; - } - if ('reason' in event) { - logMessage += ', reason = ' + event.reason; - } - } else { - logMessage += 'CloseEvent is not available'; - } - addToLog(logMessage + ')'); - }; - - if (protocols) { - addToLog('Connect ' + url + ' (protocols = ' + protocols + ')'); - } else { - addToLog('Connect ' + url); - } -} - -function closeSocket() { - if (!socket) { - addToLog('Not connected'); - return; - } - - if (codeBox.value || reasonBox.value) { - socket.close(codeBox.value, reasonBox.value); - } else { - socket.close(); - } -} - -function printState() { - if (!socket) { - addToLog('Not connected'); - return; - } - - addToLog( - 'url = ' + socket.url + - ', readyState = ' + socket.readyState + - ', bufferedAmount = ' + socket.bufferedAmount); -} - -function init() { - var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://'; - var defaultAddress = scheme + window.location.host + '/echo'; - - addressBox = document.getElementById('address'); - protocolsBox = document.getElementById('protocols'); - logBox = document.getElementById('log'); - messageBox = document.getElementById('message'); - fileBox = document.getElementById('file'); - codeBox = document.getElementById('code'); - reasonBox = document.getElementById('reason'); - - addressBox.value = defaultAddress; - - if (!('WebSocket' in window)) { - addToLog('WebSocket is not available'); - } -} -</script> -<style type="text/css"> -form { - margin: 0px; -} - -#connect_div, #log_div, #send_div, #sendfile_div, #close_div, #printstate_div { - padding: 5px; - margin: 5px; - border-width: 0px 0px 0px 10px; - border-style: solid; - border-color: silver; -} -</style> -</head> -<body onload="init()"> - -<div> - -<div id="connect_div"> - <form action="#" onsubmit="connect(); return false;"> - url <input type="text" id="address" size="40"> - <input type="submit" value="connect"> - <br/> - protocols <input type="text" id="protocols" size="20"> - </form> -</div> - -<div id="log_div"> - <textarea id="log" rows="10" cols="40" readonly></textarea> - <br/> - <input type="checkbox" - name="showtimestamp" - value="showtimestamp" - onclick="showTimeStamp = this.checked">Show time stamp -</div> - -<div id="send_div"> - <form action="#" onsubmit="send(); return false;"> - data <input type="text" id="message" size="40"> - <input type="submit" value="send"> - </form> -</div> - -<div id="sendfile_div"> - <form action="#" onsubmit="sendfile(); return false;"> - <input type="file" id="file" size="40"> - <input type="submit" value="send file"> - </form> - - Set binaryType - <input type="radio" - name="binarytype" - value="blob" - onclick="setbinarytype('blob')" checked>blob - <input type="radio" - name="binarytype" - value="arraybuffer" - onclick="setbinarytype('arraybuffer')">arraybuffer -</div> - -<div id="close_div"> - <form action="#" onsubmit="closeSocket(); return false;"> - code <input type="text" id="code" size="10"> - reason <input type="text" id="reason" size="20"> - <input type="submit" value="close"> - </form> -</div> - -<div id="printstate_div"> - <input type="button" value="print state" onclick="printState();"> -</div> - -</div> - -</body> -</html> diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py index 815209694e..1ca2c84386 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py @@ -27,6 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import + from six.moves import urllib diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py index 2ed60b3b59..5063f00a51 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py @@ -30,13 +30,13 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Simple WebSocket client named echo_client just because of historical reason. -mod_pywebsocket directory must be in PYTHONPATH. +pywebsocket3 directory must be in PYTHONPATH. Example Usage: # server setup - % cd $pywebsocket - % PYTHONPATH=$cwd/src python ./mod_pywebsocket/standalone.py -p 8880 \ + % cd $pywebsocket3 + % PYTHONPATH=$cwd/src python ./pywebsocket3/standalone.py -p 8880 \ -d $cwd/src/example # run client @@ -47,27 +47,27 @@ Example Usage: from __future__ import absolute_import from __future__ import print_function + +import argparse import base64 import codecs -from hashlib import sha1 import logging -import argparse import os -import random import re -import six import socket import ssl -import struct import sys +from hashlib import sha1 + +import six -from mod_pywebsocket import common -from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor -from mod_pywebsocket.extensions import _PerMessageDeflateFramer -from mod_pywebsocket.extensions import _parse_window_bits -from mod_pywebsocket.stream import Stream -from mod_pywebsocket.stream import StreamOptions -from mod_pywebsocket import util +from pywebsocket3 import common, util +from pywebsocket3.extensions import ( + PerMessageDeflateExtensionProcessor, + _PerMessageDeflateFramer, + _parse_window_bits, +) +from pywebsocket3.stream import Stream, StreamOptions _TIMEOUT_SEC = 10 _UNDEFINED_PORT = -1 diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt deleted file mode 100644 index 21c4c09aa0..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt +++ /dev/null @@ -1,11 +0,0 @@ -# websocket handler map file, used by standalone.py -m option. -# A line starting with '#' is a comment line. -# Each line consists of 'alias_resource_path' and 'existing_resource_path' -# separated by spaces. -# Aliasing is processed from the top to the bottom of the line, and -# 'existing_resource_path' must exist before it is aliased. -# For example, -# / /echo -# means that a request to '/' will be handled by handlers for '/echo'. -/ /echo - diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py index 04aa684283..cbc0fd294e 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py @@ -28,7 +28,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import -from mod_pywebsocket import msgutil + +from pywebsocket3 import msgutil def web_socket_do_extra_handshake(request): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html deleted file mode 100644 index c18b2c08f6..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html +++ /dev/null @@ -1,37 +0,0 @@ -<!-- -Copyright 2020, Google Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---> - -<!DOCTYPE html> -<head> -<script src="util.js"></script> -<script src="performance_test_iframe.js"></script> -<script src="benchmark.js"></script> -</head> diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js deleted file mode 100644 index 270409aa6e..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2020, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -function perfTestAddToLog(text) { - parent.postMessage({'command': 'log', 'value': text}, '*'); -} - -function perfTestAddToSummary(text) { -} - -function perfTestMeasureValue(value) { - parent.postMessage({'command': 'measureValue', 'value': value}, '*'); -} - -function perfTestNotifyAbort() { - parent.postMessage({'command': 'notifyAbort'}, '*'); -} - -function getConfigForPerformanceTest(dataType, async, - verifyData, numIterations, - numWarmUpIterations) { - - return { - prefixUrl: 'ws://' + location.host + '/benchmark_helper', - printSize: true, - numSockets: 1, - // + 1 is for a warmup iteration by the Telemetry framework. - numIterations: numIterations + numWarmUpIterations + 1, - numWarmUpIterations: numWarmUpIterations, - minTotal: 10240000, - startSize: 10240000, - stopThreshold: 10240000, - multipliers: [2], - verifyData: verifyData, - dataType: dataType, - async: async, - addToLog: perfTestAddToLog, - addToSummary: perfTestAddToSummary, - measureValue: perfTestMeasureValue, - notifyAbort: perfTestNotifyAbort - }; -} - -var data; -onmessage = function(message) { - var action; - if (message.data.command === 'start') { - data = message.data; - initWorker('http://' + location.host); - action = data.benchmarkName; - } else { - action = 'stop'; - } - - var config = getConfigForPerformanceTest(data.dataType, data.async, - data.verifyData, - data.numIterations, - data.numWarmUpIterations); - doAction(config, data.isWorker, action); -}; diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi deleted file mode 100755 index 703cb7401b..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/python - -# Copyright 2014 Google Inc. All rights reserved. -# -# Use of this source code is governed by a BSD-style -# license that can be found in the COPYING file or at -# https://developers.google.com/open-source/licenses/bsd -"""CGI script sample for testing effect of HTTP headers on the origin page. - -Note that CGI scripts don't work on the standalone pywebsocket running in TLS -mode. -""" - -print """Content-type: text/html -Content-Security-Policy: connect-src self - -<html> -<head> -<title></title> -</head> -<body> -<script> -var socket = new WebSocket("ws://example.com"); -</script> -</body> -</html>""" diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js deleted file mode 100644 index 990160cb40..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2013, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -// Utilities for example applications (for both main and worker thread). - -var results = {}; - -function getTimeStamp() { - return Date.now(); -} - -function formatResultInKiB(size, timePerMessageInMs, stddevTimePerMessageInMs, - speed, printSize) { - if (printSize) { - return (size / 1024) + - '\t' + timePerMessageInMs.toFixed(3) + - (stddevTimePerMessageInMs == -1 ? - '' : - '\t' + stddevTimePerMessageInMs.toFixed(3)) + - '\t' + speed.toFixed(3); - } else { - return speed.toString(); - } -} - -function clearAverageData() { - results = {}; -} - -function reportAverageData(config) { - config.addToSummary( - 'Size[KiB]\tAverage time[ms]\tStddev time[ms]\tSpeed[KB/s]'); - for (var size in results) { - var averageTimePerMessageInMs = results[size].sum_t / results[size].n; - var speed = calculateSpeedInKB(size, averageTimePerMessageInMs); - // Calculate sample standard deviation - var stddevTimePerMessageInMs = Math.sqrt( - (results[size].sum_t2 / results[size].n - - averageTimePerMessageInMs * averageTimePerMessageInMs) * - results[size].n / - (results[size].n - 1)); - config.addToSummary(formatResultInKiB( - size, averageTimePerMessageInMs, stddevTimePerMessageInMs, speed, - true)); - } -} - -function calculateSpeedInKB(size, timeSpentInMs) { - return Math.round(size / timeSpentInMs * 1000) / 1000; -} - -function calculateAndLogResult(config, size, startTimeInMs, totalSize, - isWarmUp) { - var timeSpentInMs = getTimeStamp() - startTimeInMs; - var speed = calculateSpeedInKB(totalSize, timeSpentInMs); - var timePerMessageInMs = timeSpentInMs / (totalSize / size); - if (!isWarmUp) { - config.measureValue(timePerMessageInMs); - if (!results[size]) { - results[size] = {n: 0, sum_t: 0, sum_t2: 0}; - } - results[size].n ++; - results[size].sum_t += timePerMessageInMs; - results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs; - } - config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed, - config.printSize)); -} - -function repeatString(str, count) { - var data = ''; - var expChunk = str; - var remain = count; - while (true) { - if (remain % 2) { - data += expChunk; - remain = (remain - 1) / 2; - } else { - remain /= 2; - } - - if (remain == 0) - break; - - expChunk = expChunk + expChunk; - } - return data; -} - -function fillArrayBuffer(buffer, c) { - var i; - - var u32Content = c * 0x01010101; - - var u32Blocks = Math.floor(buffer.byteLength / 4); - var u32View = new Uint32Array(buffer, 0, u32Blocks); - // length attribute is slow on Chrome. Don't use it for loop condition. - for (i = 0; i < u32Blocks; ++i) { - u32View[i] = u32Content; - } - - // Fraction - var u8Blocks = buffer.byteLength - u32Blocks * 4; - var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks); - for (i = 0; i < u8Blocks; ++i) { - u8View[i] = c; - } -} - -function verifyArrayBuffer(buffer, expectedChar) { - var i; - - var expectedU32Value = expectedChar * 0x01010101; - - var u32Blocks = Math.floor(buffer.byteLength / 4); - var u32View = new Uint32Array(buffer, 0, u32Blocks); - for (i = 0; i < u32Blocks; ++i) { - if (u32View[i] != expectedU32Value) { - return false; - } - } - - var u8Blocks = buffer.byteLength - u32Blocks * 4; - var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks); - for (i = 0; i < u8Blocks; ++i) { - if (u8View[i] != expectedChar) { - return false; - } - } - - return true; -} - -function verifyBlob(config, blob, expectedChar, doneCallback) { - var reader = new FileReader(blob); - reader.onerror = function() { - config.addToLog('FileReader Error: ' + reader.error.message); - doneCallback(blob.size, false); - } - reader.onloadend = function() { - var result = verifyArrayBuffer(reader.result, expectedChar); - doneCallback(blob.size, result); - } - reader.readAsArrayBuffer(blob); -} - -function verifyAcknowledgement(config, message, size) { - if (typeof message != 'string') { - config.addToLog('Invalid ack type: ' + typeof message); - return false; - } - var parsedAck = parseInt(message); - if (isNaN(parsedAck)) { - config.addToLog('Invalid ack value: ' + message); - return false; - } - if (parsedAck != size) { - config.addToLog( - 'Expected ack for ' + size + 'B but received one for ' + parsedAck + - 'B'); - return false; - } - - return true; -} - -function cloneConfig(obj) { - var newObj = {}; - for (key in obj) { - newObj[key] = obj[key]; - } - return newObj; -} - -var tasks = []; - -function runNextTask(config) { - var task = tasks.shift(); - if (task == undefined) { - config.addToLog('Finished'); - cleanup(); - return; - } - timerID = setTimeout(task, 0); -} - -function buildLegendString(config) { - var legend = '' - if (config.printSize) - legend = 'Message size in KiB, Time/message in ms, '; - legend += 'Speed in kB/s'; - return legend; -} - -function addTasks(config, stepFunc) { - for (var i = 0; - i < config.numWarmUpIterations + config.numIterations; ++i) { - var multiplierIndex = 0; - for (var size = config.startSize; - size <= config.stopThreshold; - ++multiplierIndex) { - var task = stepFunc.bind( - null, - size, - config, - i < config.numWarmUpIterations); - tasks.push(task); - var multiplier = config.multipliers[ - multiplierIndex % config.multipliers.length]; - if (multiplier <= 1) { - config.addToLog('Invalid multiplier ' + multiplier); - config.notifyAbort(); - throw new Error('Invalid multipler'); - } - size = Math.ceil(size * multiplier); - } - } -} - -function addResultReportingTask(config, title) { - tasks.push(function(){ - timerID = null; - config.addToSummary(title); - reportAverageData(config); - clearAverageData(); - runNextTask(config); - }); -} - -function sendBenchmark(config) { - config.addToLog('Send benchmark'); - config.addToLog(buildLegendString(config)); - - tasks = []; - clearAverageData(); - addTasks(config, sendBenchmarkStep); - addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config)); - startBenchmark(config); -} - -function receiveBenchmark(config) { - config.addToLog('Receive benchmark'); - config.addToLog(buildLegendString(config)); - - tasks = []; - clearAverageData(); - addTasks(config, receiveBenchmarkStep); - addResultReportingTask(config, - 'Receive Benchmark ' + getConfigString(config)); - startBenchmark(config); -} - -function stop(config) { - clearTimeout(timerID); - timerID = null; - tasks = []; - config.addToLog('Stopped'); - cleanup(); -} - -var worker; - -function initWorker(origin) { - worker = new Worker(origin + '/benchmark.js'); -} - -function doAction(config, isWindowToWorker, action) { - if (isWindowToWorker) { - worker.onmessage = function(addToLog, addToSummary, - measureValue, notifyAbort, message) { - if (message.data.type === 'addToLog') - addToLog(message.data.data); - else if (message.data.type === 'addToSummary') - addToSummary(message.data.data); - else if (message.data.type === 'measureValue') - measureValue(message.data.data); - else if (message.data.type === 'notifyAbort') - notifyAbort(); - }.bind(undefined, config.addToLog, config.addToSummary, - config.measureValue, config.notifyAbort); - config.addToLog = undefined; - config.addToSummary = undefined; - config.measureValue = undefined; - config.notifyAbort = undefined; - worker.postMessage({type: action, config: config}); - } else { - if (action === 'sendBenchmark') - sendBenchmark(config); - else if (action === 'receiveBenchmark') - receiveBenchmark(config); - else if (action === 'batchBenchmark') - batchBenchmark(config); - else if (action === 'stop') - stop(config); - } -} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js deleted file mode 100644 index 78add48731..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -// Utilities for example applications (for the main thread only). - -var logBox = null; -var queuedLog = ''; - -var summaryBox = null; - -function queueLog(log) { - queuedLog += log + '\n'; -} - -function addToLog(log) { - logBox.value += queuedLog; - queuedLog = ''; - logBox.value += log + '\n'; - logBox.scrollTop = 1000000; -} - -function addToSummary(log) { - summaryBox.value += log + '\n'; - summaryBox.scrollTop = 1000000; -} - -// value: execution time in milliseconds. -// config.measureValue is intended to be used in Performance Tests. -// Do nothing here in non-PerformanceTest. -function measureValue(value) { -} - -// config.notifyAbort is called when the benchmark failed and aborted, and -// intended to be used in Performance Tests. -// Do nothing here in non-PerformanceTest. -function notifyAbort() { -} - -function getIntFromInput(id) { - return parseInt(document.getElementById(id).value); -} - -function getStringFromRadioBox(name) { - var list = document.getElementById('benchmark_form')[name]; - for (var i = 0; i < list.length; ++i) - if (list.item(i).checked) - return list.item(i).value; - return undefined; -} -function getBoolFromCheckBox(id) { - return document.getElementById(id).checked; -} - -function getIntArrayFromInput(id) { - var strArray = document.getElementById(id).value.split(','); - return strArray.map(function(str) { return parseInt(str, 10); }); -} - -function getFloatArrayFromInput(id) { - var strArray = document.getElementById(id).value.split(','); - return strArray.map(parseFloat); -} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js deleted file mode 100644 index dd90449a90..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -// Utilities for example applications (for the worker threads only). - -onmessage = function (message) { - var config = message.data.config; - config.addToLog = function(text) { - postMessage({type: 'addToLog', data: text}); }; - config.addToSummary = function(text) { - postMessage({type: 'addToSummary', data: text}); }; - config.measureValue = function(value) { - postMessage({type: 'measureValue', data: value}); }; - config.notifyAbort = function() { postMessage({type: 'notifyAbort'}); }; - - doAction(config, false, message.data.type); -}; diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i deleted file mode 100644 index ddaad27f53..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2013, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -%module fast_masking - -%include "cstring.i" - -%{ -#include <cstring> - -#ifdef __SSE2__ -#include <emmintrin.h> -#endif -%} - -%apply (char *STRING, int LENGTH) { - (const char* payload, int payload_length), - (const char* masking_key, int masking_key_length) }; -%cstring_output_allocate_size( - char** result, int* result_length, delete [] *$1); - -%inline %{ - -void mask( - const char* payload, int payload_length, - const char* masking_key, int masking_key_length, - int masking_key_index, - char** result, int* result_length) { - *result = new char[payload_length]; - *result_length = payload_length; - memcpy(*result, payload, payload_length); - - char* cursor = *result; - char* cursor_end = *result + *result_length; - -#ifdef __SSE2__ - while ((cursor < cursor_end) && - (reinterpret_cast<size_t>(cursor) & 0xf)) { - *cursor ^= masking_key[masking_key_index]; - ++cursor; - masking_key_index = (masking_key_index + 1) % masking_key_length; - } - if (cursor == cursor_end) { - return; - } - - const int kBlockSize = 16; - __m128i masking_key_block; - for (int i = 0; i < kBlockSize; ++i) { - *(reinterpret_cast<char*>(&masking_key_block) + i) = - masking_key[masking_key_index]; - masking_key_index = (masking_key_index + 1) % masking_key_length; - } - - while (cursor + kBlockSize <= cursor_end) { - __m128i payload_block = - _mm_load_si128(reinterpret_cast<__m128i*>(cursor)); - _mm_stream_si128(reinterpret_cast<__m128i*>(cursor), - _mm_xor_si128(payload_block, masking_key_block)); - cursor += kBlockSize; - } -#endif - - while (cursor < cursor_end) { - *cursor ^= masking_key[masking_key_index]; - ++cursor; - masking_key_index = (masking_key_index + 1) % masking_key_length; - } -} - -%} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/PKG-INFO b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/PKG-INFO new file mode 100644 index 0000000000..289dfa8649 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/PKG-INFO @@ -0,0 +1,13 @@ +Metadata-Version: 2.1 +Name: pywebsocket3 +Version: 4.0.2 +Summary: Standalone WebSocket Server for testing purposes. +Home-page: https://github.com/GoogleChromeLabs/pywebsocket3 +Author: Yuzo Fujishima +Author-email: yuzo@chromium.org +License: See LICENSE +Requires-Python: >=2.7 +License-File: LICENSE +Requires-Dist: six + +pywebsocket3 is a standalone server for the WebSocket Protocol (RFC 6455). See pywebsocket3/__init__.py for more detail. diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/SOURCES.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/SOURCES.txt new file mode 100644 index 0000000000..9a74c73196 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/SOURCES.txt @@ -0,0 +1,64 @@ +LICENSE +MANIFEST.in +README.md +setup.py +example/abort_handshake_wsh.py +example/abort_wsh.py +example/bench_wsh.py +example/benchmark_helper_wsh.py +example/close_wsh.py +example/cookie_wsh.py +example/echo_client.py +example/echo_noext_wsh.py +example/echo_wsh.py +example/hsts_wsh.py +example/internal_error_wsh.py +example/origin_check_wsh.py +example/cgi-bin/hi.py +pywebsocket3/__init__.py +pywebsocket3/_stream_exceptions.py +pywebsocket3/common.py +pywebsocket3/dispatch.py +pywebsocket3/extensions.py +pywebsocket3/http_header_util.py +pywebsocket3/memorizingfile.py +pywebsocket3/msgutil.py +pywebsocket3/request_handler.py +pywebsocket3/server_util.py +pywebsocket3/standalone.py +pywebsocket3/stream.py +pywebsocket3/util.py +pywebsocket3/websocket_server.py +pywebsocket3.egg-info/PKG-INFO +pywebsocket3.egg-info/SOURCES.txt +pywebsocket3.egg-info/dependency_links.txt +pywebsocket3.egg-info/requires.txt +pywebsocket3.egg-info/top_level.txt +pywebsocket3/handshake/__init__.py +pywebsocket3/handshake/base.py +pywebsocket3/handshake/hybi.py +test/__init__.py +test/client_for_testing.py +test/mock.py +test/run_all.py +test/set_sys_path.py +test/test_dispatch.py +test/test_endtoend.py +test/test_extensions.py +test/test_handshake.py +test/test_handshake_hybi.py +test/test_http_header_util.py +test/test_memorizingfile.py +test/test_mock.py +test/test_msgutil.py +test/test_stream.py +test/test_util.py +test/testdata/handlers/abort_by_user_wsh.py +test/testdata/handlers/blank_wsh.py +test/testdata/handlers/origin_check_wsh.py +test/testdata/handlers/sub/exception_in_transfer_wsh.py +test/testdata/handlers/sub/no_wsh_at_the_end.py +test/testdata/handlers/sub/non_callable_wsh.py +test/testdata/handlers/sub/plain_wsh.py +test/testdata/handlers/sub/wrong_handshake_sig_wsh.py +test/testdata/handlers/sub/wrong_transfer_sig_wsh.py
\ No newline at end of file diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/dependency_links.txt index 8b13789179..8b13789179 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/dependency_links.txt diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/requires.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/requires.txt new file mode 100644 index 0000000000..ffe2fce498 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/requires.txt @@ -0,0 +1 @@ +six diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/top_level.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/top_level.txt new file mode 100644 index 0000000000..db62422f0b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3.egg-info/top_level.txt @@ -0,0 +1 @@ +pywebsocket3 diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/__init__.py index 28d5f5950f..8f4ade0582 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/__init__.py @@ -28,7 +28,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ A Standalone WebSocket Server for testing purposes -mod_pywebsocket is an API that provides WebSocket functionalities with +pywebsocket3 is an API that provides WebSocket functionalities with a standalone WebSocket server. It is intended for testing or experimental purposes. @@ -37,7 +37,7 @@ Installation 1. Follow standalone server documentation to start running the standalone server. It can be read by running the following command: - $ pydoc mod_pywebsocket.standalone + $ pydoc pywebsocket3.standalone 2. Once the standalone server is launched verify it by accessing http://localhost[:port]/console.html. Include the port number when @@ -96,7 +96,7 @@ Data Transfer web_socket_transfer_data is called after the handshake completed successfully. A handler can receive/send messages from/to the client -using request. mod_pywebsocket.msgutil module provides utilities +using request. pywebsocket3.msgutil module provides utilities for data transfer. You can receive a message by the following statement. diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/_stream_exceptions.py index b47878bc4a..b47878bc4a 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/_stream_exceptions.py diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/common.py index 9cb11f15cb..a3321e4c68 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/common.py @@ -30,7 +30,9 @@ """ from __future__ import absolute_import -from mod_pywebsocket import http_header_util + +from pywebsocket3 import http_header_util + # Additional log level definitions. LOGLEVEL_FINE = 9 diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/dispatch.py index 56cbb3c8a5..fd35ceab29 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/dispatch.py @@ -30,17 +30,19 @@ """ from __future__ import absolute_import + import io -import logging import os import re import traceback -from mod_pywebsocket import common -from mod_pywebsocket import handshake -from mod_pywebsocket import msgutil -from mod_pywebsocket import stream -from mod_pywebsocket import util +from pywebsocket3 import ( + common, + handshake, + msgutil, + stream, + util +) _SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$') _SOURCE_SUFFIX = '_wsh.py' diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/extensions.py index 314a949d45..4b5b9e8fb2 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/extensions.py @@ -28,9 +28,9 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import -from mod_pywebsocket import common -from mod_pywebsocket import util -from mod_pywebsocket.http_header_util import quote_if_necessary + +from pywebsocket3 import common, util +from pywebsocket3.http_header_util import quote_if_necessary # The list of available server side extension processor classes. _available_processors = {} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/__init__.py index 4bc1c67c57..275e447552 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/__init__.py @@ -32,15 +32,19 @@ successfully established. """ from __future__ import absolute_import + import logging -from mod_pywebsocket import common -from mod_pywebsocket.handshake import hybi +from pywebsocket3 import common +from pywebsocket3.handshake import hybi # Export AbortedByUserException, HandshakeException, and VersionException # symbol from this module. -from mod_pywebsocket.handshake.base import AbortedByUserException -from mod_pywebsocket.handshake.base import HandshakeException -from mod_pywebsocket.handshake.base import VersionException +from pywebsocket3.handshake.base import ( + AbortedByUserException, + HandshakeException, + VersionException +) + _LOGGER = logging.getLogger(__name__) diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/base.py index ffad0614d6..561f7b650a 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/base.py @@ -32,15 +32,12 @@ processors. from __future__ import absolute_import -from mod_pywebsocket import common -from mod_pywebsocket import http_header_util -from mod_pywebsocket.extensions import get_extension_processor -from mod_pywebsocket.stream import StreamOptions -from mod_pywebsocket.stream import Stream -from mod_pywebsocket import util - -from six.moves import map -from six.moves import range +from pywebsocket3 import common, http_header_util, util +from pywebsocket3.extensions import get_extension_processor +from pywebsocket3.stream import Stream, StreamOptions + +from six.moves import map, range + # Defining aliases for values used frequently. _VERSION_LATEST = common.VERSION_HYBI_LATEST diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/hybi.py index cf931db5a5..2e26532c3f 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/handshake/hybi.py @@ -34,17 +34,20 @@ http://tools.ietf.org/html/rfc6455 """ from __future__ import absolute_import + import base64 import re from hashlib import sha1 -from mod_pywebsocket import common -from mod_pywebsocket.handshake.base import get_mandatory_header -from mod_pywebsocket.handshake.base import HandshakeException -from mod_pywebsocket.handshake.base import parse_token_list -from mod_pywebsocket.handshake.base import validate_mandatory_header -from mod_pywebsocket.handshake.base import HandshakerBase -from mod_pywebsocket import util +from pywebsocket3 import common, util +from pywebsocket3.handshake.base import ( + get_mandatory_header, + HandshakeException, + parse_token_list, + validate_mandatory_header, + HandshakerBase +) + # Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648 # disallows non-zero padding, so the character right before == must be any of diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/http_header_util.py index 21fde59af1..63e698bc16 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/http_header_util.py @@ -31,8 +31,10 @@ in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt. """ from __future__ import absolute_import + import six.moves.urllib.parse + _SEPARATORS = '()<>@,;:\\"/[]?={} \t' diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/memorizingfile.py index d353967618..4ee132fae6 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/memorizingfile.py @@ -34,6 +34,7 @@ A memorizing file wraps a file and memorizes lines read by readline. """ from __future__ import absolute_import + import sys @@ -61,7 +62,7 @@ class MemorizingFile(object): def __getattribute__(self, name): """Return a file attribute. - + Returns the value overridden by this class for some attributes, and forwards the call to _file for the other attributes. """ diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/msgutil.py index f58ca78e14..dd6a6fc410 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/msgutil.py @@ -36,14 +36,18 @@ bytes writing/reading. """ from __future__ import absolute_import -import six.moves.queue + import threading +import six.moves.queue + # Export Exception symbols from msgutil for backward compatibility -from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException -from mod_pywebsocket._stream_exceptions import InvalidFrameException -from mod_pywebsocket._stream_exceptions import BadOperationException -from mod_pywebsocket._stream_exceptions import UnsupportedFrameException +from pywebsocket3._stream_exceptions import ( + ConnectionTerminatedException, + InvalidFrameException, + BadOperationException, + UnsupportedFrameException +) # An API for handler to send/receive WebSocket messages. diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/request_handler.py index 5e9c875dc7..9d89b47c69 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/request_handler.py @@ -34,12 +34,14 @@ import os from six.moves import CGIHTTPServer from six.moves import http_client -from mod_pywebsocket import common -from mod_pywebsocket import dispatch -from mod_pywebsocket import handshake -from mod_pywebsocket import http_header_util -from mod_pywebsocket import memorizingfile -from mod_pywebsocket import util +from pywebsocket3 import ( + common, + dispatch, + handshake, + http_header_util, + memorizingfile, + util +) # 1024 is practically large enough to contain WebSocket handshake lines. _MAX_MEMORIZED_LINES = 1024 diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/server_util.py index 8f9e273e97..3bf07f885b 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/server_util.py @@ -33,8 +33,7 @@ import logging.handlers import threading import time -from mod_pywebsocket import common -from mod_pywebsocket import util +from pywebsocket3 import common, util def _get_logger_from_class(c): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/standalone.py index bd32158790..0c324c4221 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/standalone.py @@ -38,7 +38,7 @@ BASIC USAGE Go to the src directory and run - $ python mod_pywebsocket/standalone.py [-p <ws_port>] + $ python pywebsocket3/standalone.py [-p <ws_port>] [-w <websock_handlers>] [-d <document_root>] @@ -48,11 +48,11 @@ Go to the src directory and run <websock_handlers> is the path to the root directory of WebSocket handlers. If not specified, <document_root> will be used. See __init__.py (or -run $ pydoc mod_pywebsocket) for how to write WebSocket handlers. +run $ pydoc pywebsocket3) for how to write WebSocket handlers. For more detail and other options, run - $ python mod_pywebsocket/standalone.py --help + $ python pywebsocket3/standalone.py --help or see _build_option_parser method below. @@ -66,7 +66,7 @@ Go to the src directory and run standalone.py with -d option to set the document root to the directory containing example HTMLs and handlers like this: $ cd src - $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example + $ PYTHONPATH=. python pywebsocket3/standalone.py -d example to launch pywebsocket with the sample handler and html on port 80. Open http://localhost/console.html, click the connect button, type something into @@ -85,7 +85,7 @@ TLS connection silently fails while pyOpenSSL fails on startup. Example: - $ PYTHONPATH=. python mod_pywebsocket/standalone.py \ + $ PYTHONPATH=. python pywebsocket3/standalone.py \ -d example \ -p 10443 \ -t \ @@ -105,7 +105,7 @@ TLS support. Example: - $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \ + $ PYTHONPATH=. python pywebsocket3/standalone.py -d example -p 10443 -t \ -c ../test/cert/cert.pem -k ../test/cert/key.pem \ --tls-client-auth \ --tls-client-ca=../test/cert/cacert.pem @@ -154,19 +154,20 @@ used outside a firewall. """ from __future__ import absolute_import -from six.moves import configparser + +import argparse import base64 import logging -import argparse import os -import six import sys import traceback -from mod_pywebsocket import common -from mod_pywebsocket import util -from mod_pywebsocket import server_util -from mod_pywebsocket.websocket_server import WebSocketServer +import six +from six.moves import configparser + +from pywebsocket3 import common, server_util, util +from pywebsocket3.websocket_server import WebSocketServer + _DEFAULT_LOG_MAX_BYTES = 1024 * 256 _DEFAULT_LOG_BACKUP_COUNT = 5 @@ -480,8 +481,8 @@ def _main(args=None): server = WebSocketServer(options) server.serve_forever() except Exception as e: - logging.critical('mod_pywebsocket: %s' % e) - logging.critical('mod_pywebsocket: %s' % traceback.format_exc()) + logging.critical('pywebsocket3: %s' % e) + logging.critical('pywebsocket3: %s' % traceback.format_exc()) sys.exit(1) diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/stream.py index 82d1ea619c..dd41850dc4 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/stream.py @@ -33,21 +33,22 @@ Specification: http://tools.ietf.org/html/rfc6455 """ -from collections import deque import logging import os import struct import time -import socket +from collections import deque + import six -from mod_pywebsocket import common -from mod_pywebsocket import util -from mod_pywebsocket._stream_exceptions import BadOperationException -from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException -from mod_pywebsocket._stream_exceptions import InvalidFrameException -from mod_pywebsocket._stream_exceptions import InvalidUTF8Exception -from mod_pywebsocket._stream_exceptions import UnsupportedFrameException +from pywebsocket3 import common, util +from pywebsocket3._stream_exceptions import ( + BadOperationException, + ConnectionTerminatedException, + InvalidFrameException, + InvalidUTF8Exception, + UnsupportedFrameException +) _NOOP_MASKER = util.NoopMasker() diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/util.py index 04006ecacd..9c25ab8315 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/util.py @@ -29,20 +29,18 @@ """WebSocket utilities.""" from __future__ import absolute_import -import array -import errno + import logging import os import re -import six -from six.moves import map -from six.moves import range -import socket import struct import zlib +import six +from six.moves import map, range + try: - from mod_pywebsocket import fast_masking + from pywebsocket3 import fast_masking except ImportError: pass diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/websocket_server.py index 9f67c9f02d..dab2f079ff 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/pywebsocket3/websocket_server.py @@ -34,8 +34,7 @@ to use standalone.py, since it is intended to act as a skeleton of this module. """ from __future__ import absolute_import -from six.moves import BaseHTTPServer -from six.moves import socketserver + import logging import re import select @@ -44,9 +43,10 @@ import ssl import threading import traceback -from mod_pywebsocket import dispatch -from mod_pywebsocket import util -from mod_pywebsocket.request_handler import WebSocketRequestHandler +from six.moves import BaseHTTPServer, socketserver + +from pywebsocket3 import dispatch, util +from pywebsocket3.request_handler import WebSocketRequestHandler def _alias_handlers(dispatcher, websock_handlers_map_file): @@ -157,12 +157,13 @@ class WebSocketServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer): client_cert_ = ssl.CERT_REQUIRED else: client_cert_ = ssl.CERT_NONE - socket_ = ssl.wrap_socket( - socket_, - keyfile=server_options.private_key, - certfile=server_options.certificate, - ca_certs=server_options.tls_client_ca, - cert_reqs=client_cert_) + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.verify_mode = client_cert_ + ssl_context.load_cert_chain(keyfile=server_options.private_key, + certfile=server_options.certificate) + if client_cert_ != ssl.CERT_NONE: + ssl_context.load_verify_locations(cafile=server_options.tls_client_ca) + socket_ = ssl_context.wrap_socket(socket_, server_side=True) self._sockets.append((socket_, addrinfo)) def server_bind(self): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.cfg b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.cfg new file mode 100644 index 0000000000..8bfd5a12f8 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py index 12c60d8617..ab9a24a3e7 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py @@ -28,7 +28,8 @@ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Set up script for mod_pywebsocket. + +"""Set up script for pywebsocket3. """ from __future__ import absolute_import @@ -36,7 +37,7 @@ from __future__ import print_function from setuptools import setup, Extension import sys -_PACKAGE_NAME = 'mod_pywebsocket' +_PACKAGE_NAME = 'pywebsocket3' # Build and use a C++ extension for faster masking. SWIG is required. _USE_FAST_MASKING = False @@ -49,8 +50,8 @@ if sys.hexversion < 0x020700f0: if _USE_FAST_MASKING: setup(ext_modules=[ - Extension('mod_pywebsocket/_fast_masking', - ['mod_pywebsocket/fast_masking.i'], + Extension('pywebsocket3/_fast_masking', + ['pywebsocket3/fast_masking.i'], swig_opts=['-c++']) ]) @@ -58,16 +59,16 @@ setup( author='Yuzo Fujishima', author_email='yuzo@chromium.org', description='Standalone WebSocket Server for testing purposes.', - long_description=('mod_pywebsocket is a standalone server for ' + long_description=('pywebsocket3 is a standalone server for ' 'the WebSocket Protocol (RFC 6455). ' - 'See mod_pywebsocket/__init__.py for more detail.'), + 'See pywebsocket3/__init__.py for more detail.'), license='See LICENSE', name=_PACKAGE_NAME, packages=[_PACKAGE_NAME, _PACKAGE_NAME + '.handshake'], python_requires='>=2.7', install_requires=['six'], url='https://github.com/GoogleChromeLabs/pywebsocket3', - version='3.0.2', + version='4.0.2', ) # vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem deleted file mode 100644 index 4dadae121b..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICvDCCAiWgAwIBAgIJAKqVghkGF1rSMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV -BAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQxFDAS -BgNVBAMTC3B5d2Vic29ja2V0MB4XDTEyMDYwNjA3MjQzM1oXDTM5MTAyMzA3MjQz -M1owSTELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMRQwEgYDVQQKEwtweXdl -YnNvY2tldDEUMBIGA1UEAxMLcHl3ZWJzb2NrZXQwgZ8wDQYJKoZIhvcNAQEBBQAD -gY0AMIGJAoGBAKoSEW2biQxVrMMKdn/8PJzDYiSXDPR9WQbLRRQ1Gm5jkCYiahXW -u2CbTThfPPfi2NHA3I+HlT7gO9yR7RVUvN6ISUzGwXDEq4f4UNqtQOhQaqqK+CZ9 -LO/BhO/YYfNrbSPlYzHUKaT9ese7xO9VzVKLW+qUf2Mjh4/+SzxBDNP7AgMBAAGj -gaswgagwHQYDVR0OBBYEFOsWdxCSuyhwaZeab6BoTho3++bzMHkGA1UdIwRyMHCA -FOsWdxCSuyhwaZeab6BoTho3++bzoU2kSzBJMQswCQYDVQQGEwJKUDEOMAwGA1UE -CBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtweXdlYnNv -Y2tldIIJAKqVghkGF1rSMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA -gsMI1WEYqNw/jhUIdrTBcCxJ0X6hJvA9ziKANVm1Rs+4P3YDArkQ8bCr6xY+Kw7s -Zp0yE7dM8GMdi+DU6hL3t3E5eMkTS1yZr9WCK4f2RLo+et98selZydpHemF3DJJ3 -gAj8Sx4LBaG8Cb/WnEMPv3MxG3fBE5favF6V4jU07hQ= ------END CERTIFICATE----- diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem deleted file mode 100644 index 25379a72b0..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem +++ /dev/null @@ -1,61 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 1 (0x1) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket - Validity - Not Before: Jun 6 07:25:08 2012 GMT - Not After : Oct 23 07:25:08 2039 GMT - Subject: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public Key: (1024 bit) - Modulus (1024 bit): - 00:de:10:ce:3a:5a:04:a4:1c:29:93:5c:23:82:1a: - f2:06:01:e6:2b:a4:0f:dd:77:49:76:89:03:a2:21: - de:04:75:c6:e2:dd:fb:35:27:3a:a2:92:8e:12:62: - 2b:3e:1f:f4:78:df:b6:94:cb:27:d6:cb:d6:37:d7: - 5c:08:f0:09:3e:c9:ce:24:2d:00:c9:df:4a:e0:99: - e5:fb:23:a9:e2:d6:c9:3d:96:fa:01:88:de:5a:89: - b0:cf:03:67:6f:04:86:1d:ef:62:1c:55:a9:07:9a: - 2e:66:2a:73:5b:4c:62:03:f9:82:83:db:68:bf:b8: - 4b:0b:8b:93:11:b8:54:73:7b - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: - CA:FALSE - Netscape Cert Type: - SSL Server - Netscape Comment: - OpenSSL Generated Certificate - X509v3 Subject Key Identifier: - 82:A1:73:8B:16:0C:7C:E4:D3:46:95:13:95:1A:32:C1:84:E9:06:00 - X509v3 Authority Key Identifier: - keyid:EB:16:77:10:92:BB:28:70:69:97:9A:6F:A0:68:4E:1A:37:FB:E6:F3 - - Signature Algorithm: sha1WithRSAEncryption - 6b:b3:46:29:02:df:b0:c8:8e:c4:d7:7f:a0:1e:0d:1a:eb:2f: - df:d1:48:57:36:5f:95:8c:1b:f0:51:d6:52:e7:8d:84:3b:9f: - d8:ed:22:9c:aa:bd:ee:9b:90:1d:84:a3:4c:0b:cb:eb:64:73: - ba:f7:15:ce:da:5f:db:8b:15:07:a6:28:7f:b9:8c:11:9b:64: - d3:f1:be:52:4f:c3:d8:58:fe:de:56:63:63:3b:51:ed:a7:81: - f9:05:51:70:63:32:09:0e:94:7e:05:fe:a1:56:18:34:98:d5: - 99:1e:4e:27:38:89:90:6a:e5:ce:60:35:01:f5:de:34:60:b1: - cb:ae ------BEGIN CERTIFICATE----- -MIICmDCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJKUDEO -MAwGA1UECBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtw -eXdlYnNvY2tldDAeFw0xMjA2MDYwNzI1MDhaFw0zOTEwMjMwNzI1MDhaMEkxCzAJ -BgNVBAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQx -FDASBgNVBAMTC3B5d2Vic29ja2V0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB -gQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1Jzqiko4SYis+ -H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoBiN5aibDPA2dv -BIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQABo4GPMIGMMAkG -A1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgZAMCwGCWCGSAGG+EIBDQQfFh1PcGVu -U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUgqFzixYMfOTTRpUT -lRoywYTpBgAwHwYDVR0jBBgwFoAU6xZ3EJK7KHBpl5pvoGhOGjf75vMwDQYJKoZI -hvcNAQEFBQADgYEAa7NGKQLfsMiOxNd/oB4NGusv39FIVzZflYwb8FHWUueNhDuf -2O0inKq97puQHYSjTAvL62RzuvcVztpf24sVB6Yof7mMEZtk0/G+Uk/D2Fj+3lZj -YztR7aeB+QVRcGMyCQ6UfgX+oVYYNJjVmR5OJziJkGrlzmA1AfXeNGCxy64= ------END CERTIFICATE----- diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem deleted file mode 100644 index fae858318f..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1 -Jzqiko4SYis+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoB -iN5aibDPA2dvBIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQAB -AoGBAIuCuV1Vcnb7rm8CwtgZP5XgmY8vSjxTldafa6XvawEYUTP0S77v/1llg1Yv -UIV+I+PQgG9oVoYOl22LoimHS/Z3e1fsot5tDYszGe8/Gkst4oaReSoxvBUa6WXp -QSo7YFCajuHtE+W/gzF+UHbdzzXIDjQZ314LNF5t+4UnsEPBAkEA+girImqWoM2t -3UR8f8oekERwsmEMf9DH5YpH4cvUnvI+kwesC/r2U8Sho++fyEMUNm7aIXGqNLga -ogAM+4NX4QJBAONdSxSay22egTGNoIhLndljWkuOt/9FWj2klf/4QxD4blMJQ5Oq -QdOGAh7nVQjpPLQ5D7CBVAKpGM2CD+QJBtsCQEP2kz35pxPylG3urcC2mfQxBkkW -ZCViBNP58GwJ0bOauTOSBEwFXWuLqTw8aDwxL49UNmqc0N0fpe2fAehj3UECQQCm -FH/DjU8Lw7ybddjNtm6XXPuYNagxz3cbkB4B3FchDleIUDwMoVF0MW9bI5/54mV1 -QDk1tUKortxvQZJaAD4BAkEAhGOHQqPd6bBBoFBvpaLzPJMxwLKrB+Wtkq/QlC72 -ClRiMn2g8SALiIL3BDgGXKcKE/Wy7jo/af/JCzQ/cPqt/A== ------END RSA PRIVATE KEY----- diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py index a45e8f5cf2..6275676371 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py @@ -33,8 +33,8 @@ This module contains helper methods for performing handshake, frame sending/receiving as a WebSocket client. -This is code for testing mod_pywebsocket. Keep this code independent from -mod_pywebsocket. Don't import e.g. Stream class for generating frame for +This is code for testing pywebsocket3. Keep this code independent from +pywebsocket3. Don't import e.g. Stream class for generating frame for testing. Using util.hexify, etc. that are not related to protocol processing is allowed. @@ -43,22 +43,20 @@ This code is far from robust, e.g., we cut corners in handshake. """ from __future__ import absolute_import + import base64 import errno -import logging import os -import random import re import socket import struct import time from hashlib import sha1 -from six import iterbytes -from six import indexbytes -from mod_pywebsocket import common -from mod_pywebsocket import util -from mod_pywebsocket.handshake import HandshakeException +from six import indexbytes, iterbytes + +from pywebsocket3 import common, util +from pywebsocket3.handshake import HandshakeException DEFAULT_PORT = 80 DEFAULT_SECURE_PORT = 443 @@ -702,15 +700,15 @@ class Client(object): try: read_data = receive_bytes(self._socket, 1) except Exception as e: - if str(e).find( - 'Connection closed before receiving requested length ' - ) == 0: + if str(e).find('Connection closed before receiving requested length ') == 0: return + try: - error_number, message = e for error_name in ['ECONNRESET', 'WSAECONNRESET']: - if (error_name in dir(errno) - and error_number == getattr(errno, error_name)): + if ( + error_name in dir(errno) and + e.errno == getattr(errno, error_name) + ): return except: raise e diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py index eeaef52ecf..c460d9b7f0 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py @@ -30,17 +30,14 @@ """ from __future__ import absolute_import -import six.moves.queue -import threading -import struct -import six -from mod_pywebsocket import common -from mod_pywebsocket import util -from mod_pywebsocket.stream import Stream -from mod_pywebsocket.stream import StreamOptions +import six +import six.moves.queue from six.moves import range +from pywebsocket3 import common, util +from pywebsocket3.stream import Stream, StreamOptions + class _MockConnBase(object): """Base class of mocks for mod_python.apache.mp_conn. diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py index ea52223cea..569bdb4c07 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py @@ -31,7 +31,7 @@ """Run all tests in the same directory. This suite is expected to be run under pywebsocket's src directory, i.e. the -directory containing mod_pywebsocket, test, etc. +directory containing pywebsocket3, test, etc. To change loggin level, please specify --log-level option. python test/run_test.py --log-level debug @@ -42,14 +42,16 @@ example, run this for making the test runner verbose. """ from __future__ import absolute_import -import logging + import argparse +import logging import os import re -import six import sys import unittest +import six + _TEST_MODULE_PATTERN = re.compile(r'^(test_.+)\.py$') diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py index 48d0e116a5..c35cb6f972 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py @@ -28,14 +28,15 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Configuration for testing. -Test files should import this module before mod_pywebsocket. +Test files should import this module before pywebsocket3. """ from __future__ import absolute_import + import os import sys -# Add the parent directory to sys.path to enable importing mod_pywebsocket. +# Add the parent directory to sys.path to enable importing pywebsocket3. sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) # vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py index 132dd92d76..18a39d1af9 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py @@ -31,15 +31,16 @@ """Tests for dispatch module.""" from __future__ import absolute_import + import os import unittest -import set_sys_path # Update sys.path to locate mod_pywebsocket module. +from six.moves import zip -from mod_pywebsocket import dispatch -from mod_pywebsocket import handshake +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3 import dispatch, handshake from test import mock -from six.moves import zip + _TEST_HANDLERS_DIR = os.path.join(os.path.dirname(__file__), 'testdata', 'handlers') diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py index 2789e4a57e..9718c6a2b2 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py @@ -28,23 +28,23 @@ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""End-to-end tests for pywebsocket. Tests standalone.py. +"""End-to-end tests for pywebsocket3. Tests standalone.py. """ from __future__ import absolute_import -from six.moves import urllib + import locale import logging import os -import signal import socket import subprocess import sys import time import unittest -import set_sys_path # Update sys.path to locate mod_pywebsocket module. +from six.moves import urllib +import set_sys_path # Update sys.path to locate pywebsocket3 module. from test import client_for_testing # Special message that tells the echo server to start closing handshake @@ -137,7 +137,7 @@ class EndToEndTestBase(unittest.TestCase): self.server_stderr = None self.top_dir = os.path.join(os.path.dirname(__file__), '..') os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path)) - self.standalone_command = os.path.join(self.top_dir, 'mod_pywebsocket', + self.standalone_command = os.path.join(self.top_dir, 'pywebsocket3', 'standalone.py') self.document_root = os.path.join(self.top_dir, 'example') s = socket.socket() diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py index 39a111888b..008df24827 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py @@ -31,13 +31,12 @@ """Tests for extensions module.""" from __future__ import absolute_import + import unittest import zlib -import set_sys_path # Update sys.path to locate mod_pywebsocket module. - -from mod_pywebsocket import common -from mod_pywebsocket import extensions +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3 import common, extensions class ExtensionsTest(unittest.TestCase): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py index 7f4acf56ff..c8e25ab099 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py @@ -31,16 +31,17 @@ """Tests for handshake.base module.""" from __future__ import absolute_import -import unittest -import set_sys_path # Update sys.path to locate mod_pywebsocket module. +import unittest -from mod_pywebsocket.common import ExtensionParameter -from mod_pywebsocket.common import ExtensionParsingException -from mod_pywebsocket.common import format_extensions -from mod_pywebsocket.common import parse_extensions -from mod_pywebsocket.handshake.base import HandshakeException -from mod_pywebsocket.handshake.base import validate_subprotocol +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3.common import ( + ExtensionParameter, + ExtensionParsingException, + format_extensions, + parse_extensions, +) +from pywebsocket3.handshake.base import HandshakeException, validate_subprotocol class ValidateSubprotocolTest(unittest.TestCase): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py index 8c65822170..ee63ed45b4 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py @@ -31,15 +31,17 @@ """Tests for handshake module.""" from __future__ import absolute_import -import unittest -import set_sys_path # Update sys.path to locate mod_pywebsocket module. -from mod_pywebsocket import common -from mod_pywebsocket.handshake.base import AbortedByUserException -from mod_pywebsocket.handshake.base import HandshakeException -from mod_pywebsocket.handshake.base import VersionException -from mod_pywebsocket.handshake.hybi import Handshaker +import unittest +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3 import common +from pywebsocket3.handshake.base import ( + AbortedByUserException, + HandshakeException, + VersionException, +) +from pywebsocket3.handshake.hybi import Handshaker from test import mock diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py index f8c8e7a981..bd9b9bfc2e 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py @@ -31,10 +31,11 @@ """Tests for http_header_util module.""" from __future__ import absolute_import + import unittest import sys -from mod_pywebsocket import http_header_util +from pywebsocket3 import http_header_util class UnitTest(unittest.TestCase): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py index f7288c510b..4749085962 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py @@ -31,12 +31,13 @@ """Tests for memorizingfile module.""" from __future__ import absolute_import + import unittest -import six -import set_sys_path # Update sys.path to locate mod_pywebsocket module. +import six -from mod_pywebsocket import memorizingfile +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3 import memorizingfile class UtilTest(unittest.TestCase): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py index 073873dde9..df5353bc59 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py @@ -31,12 +31,13 @@ """Tests for mock module.""" from __future__ import absolute_import -import six.moves.queue + import threading import unittest -import set_sys_path # Update sys.path to locate mod_pywebsocket module. +import six.moves.queue +import set_sys_path # Update sys.path to locate pywebsocket3 module. from test import mock diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py index 1122c281b7..99aa200ba4 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py @@ -33,26 +33,26 @@ from __future__ import absolute_import from __future__ import print_function from __future__ import division -import array -import six.moves.queue + import random import struct import unittest import zlib -import set_sys_path # Update sys.path to locate mod_pywebsocket module. - -from mod_pywebsocket import common -from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor -from mod_pywebsocket import msgutil -from mod_pywebsocket.stream import InvalidUTF8Exception -from mod_pywebsocket.stream import Stream -from mod_pywebsocket.stream import StreamOptions -from mod_pywebsocket import util -from test import mock +from six import iterbytes from six.moves import map from six.moves import range -from six import iterbytes +import six.moves.queue + +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3 import common, msgutil, util +from pywebsocket3.extensions import PerMessageDeflateExtensionProcessor +from pywebsocket3.stream import ( + InvalidUTF8Exception, + Stream, + StreamOptions, +) +from test import mock # We use one fixed nonce for testing instead of cryptographically secure PRNG. _MASKING_NONCE = b'ABCD' diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py index 153899d205..c165e84688 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py @@ -31,12 +31,11 @@ """Tests for stream module.""" from __future__ import absolute_import -import unittest -import set_sys_path # Update sys.path to locate mod_pywebsocket module. +import unittest -from mod_pywebsocket import common -from mod_pywebsocket import stream +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3 import common, stream class StreamTest(unittest.TestCase): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py index bf4bd32bba..24c4b5bfbd 100755 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py @@ -32,18 +32,16 @@ from __future__ import absolute_import from __future__ import print_function + import os import random -import sys import unittest -import struct - -import set_sys_path # Update sys.path to locate mod_pywebsocket module. -from mod_pywebsocket import util +from six import int2byte, PY3 from six.moves import range -from six import PY3 -from six import int2byte + +import set_sys_path # Update sys.path to locate pywebsocket3 module. +from pywebsocket3 import util _TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'testdata') diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README deleted file mode 100644 index c001aa5595..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README +++ /dev/null @@ -1 +0,0 @@ -Test data directory diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py index 63cb541bb7..a6e0831847 100644 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py @@ -27,7 +27,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from mod_pywebsocket import handshake +from pywebsocket3 import handshake def web_socket_do_extra_handshake(request): diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl deleted file mode 100644 index 882ef5a100..0000000000 --- a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/perl -wT -# -# Copyright 2012, Google Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -print "Hello\n"; diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml new file mode 100644 index 0000000000..c6c5426a5a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: python-websockets +open_collective: websockets +tidelift: pypi/websockets diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 0000000000..3cf4e3b770 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,29 @@ +--- +name: Report an issue +about: Let us know about a problem with websockets +title: '' +labels: '' +assignees: '' + +--- + +<!-- + +Thanks for taking the time to report an issue! + +Did you check the FAQ? Perhaps you'll find the answer you need: +https://websockets.readthedocs.io/en/stable/faq/index.html + +Is your question really about asyncio? Perhaps the dev guide will help: +https://docs.python.org/3/library/asyncio-dev.html + +Did you look for similar issues? Please keep the discussion in one place :-) +https://github.com/python-websockets/websockets/issues?q=is%3Aissue + +Is your issue related to cryptocurrency in any way? Please don't file it. +https://websockets.readthedocs.io/en/stable/project/contributing.html#cryptocurrency-users + +For bugs, providing a reproduction helps a lot. Take an existing example and tweak it! +https://github.com/python-websockets/websockets/tree/main/example + +--> diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/dependabot.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/dependabot.yml new file mode 100644 index 0000000000..ad1e824b4a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + time: "07:00" + timezone: "Europe/Paris" diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/tests.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/tests.yml new file mode 100644 index 0000000000..470f5bc960 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/tests.yml @@ -0,0 +1,83 @@ +name: Run tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + WEBSOCKETS_TESTS_TIMEOUT_FACTOR: 10 + +jobs: + coverage: + name: Run test coverage checks + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install tox + run: pip install tox + - name: Run tests with coverage + run: tox -e coverage + - name: Run tests with per-module coverage + run: tox -e maxi_cov + + quality: + name: Run code quality checks + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install tox + run: pip install tox + - name: Check code formatting + run: tox -e black + - name: Check code style + run: tox -e ruff + - name: Check types statically + run: tox -e mypy + + matrix: + name: Run tests on Python ${{ matrix.python }} + needs: + - coverage + - quality + runs-on: ubuntu-latest + strategy: + matrix: + python: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "pypy-3.8" + - "pypy-3.9" + is_main: + - ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + exclude: + - python: "pypy-3.8" + is_main: false + - python: "pypy-3.9" + is_main: false + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: pip install tox + - name: Run tests + run: tox -e py diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/wheels.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/wheels.yml new file mode 100644 index 0000000000..707ef2c60d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.github/workflows/wheels.yml @@ -0,0 +1,88 @@ +name: Build wheels + +on: + push: + tags: + - '*' + workflow_dispatch: + +jobs: + sdist: + name: Build source distribution and architecture-independent wheel + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Build sdist + run: python setup.py sdist + - name: Save sdist + uses: actions/upload-artifact@v3 + with: + path: dist/*.tar.gz + - name: Install wheel + run: pip install wheel + - name: Build wheel + env: + BUILD_EXTENSION: no + run: python setup.py bdist_wheel + - name: Save wheel + uses: actions/upload-artifact@v3 + with: + path: dist/*.whl + + wheels: + name: Build architecture-specific wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + - macOS-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v3 + with: + platforms: all + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.2 + env: + BUILD_EXTENSION: yes + - name: Save wheels + uses: actions/upload-artifact@v3 + with: + path: wheelhouse/*.whl + + release: + name: Release + needs: + - sdist + - wheels + runs-on: ubuntu-latest + # Don't release when running the workflow manually from GitHub's UI. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + - name: Create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release create ${{ github.ref_name }} --notes "See https://websockets.readthedocs.io/en/stable/project/changelog.html for details." diff --git a/testing/web-platform/tests/tools/third_party/websockets/.gitignore b/testing/web-platform/tests/tools/third_party/websockets/.gitignore new file mode 100644 index 0000000000..324e77069a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.gitignore @@ -0,0 +1,16 @@ +*.pyc +*.so +.coverage +.direnv +.envrc +.idea/ +.mypy_cache +.tox +build/ +compliance/reports/ +experiments/compression/corpus.pkl +dist/ +docs/_build/ +htmlcov/ +MANIFEST +websockets.egg-info/ diff --git a/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml new file mode 100644 index 0000000000..0369e06565 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "3.10" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt diff --git a/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md b/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..80f80d51b1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aymeric DOT augustin AT fractalideas DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/testing/web-platform/tests/tools/third_party/websockets/LICENSE b/testing/web-platform/tests/tools/third_party/websockets/LICENSE index 119b29ef35..5d61ece22a 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/LICENSE +++ b/testing/web-platform/tests/tools/third_party/websockets/LICENSE @@ -1,5 +1,4 @@ -Copyright (c) 2013-2021 Aymeric Augustin and contributors. -All rights reserved. +Copyright (c) Aymeric Augustin and contributors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -9,9 +8,9 @@ modification, are permitted provided that the following conditions are met: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of websockets nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. + * Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED diff --git a/testing/web-platform/tests/tools/third_party/websockets/Makefile b/testing/web-platform/tests/tools/third_party/websockets/Makefile new file mode 100644 index 0000000000..cf3b533939 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/Makefile @@ -0,0 +1,35 @@ +.PHONY: default style types tests coverage maxi_cov build clean + +export PYTHONASYNCIODEBUG=1 +export PYTHONPATH=src +export PYTHONWARNINGS=default + +default: style types tests + +style: + black src tests + ruff --fix src tests + +types: + mypy --strict src + +tests: + python -m unittest + +coverage: + coverage run --source src/websockets,tests -m unittest + coverage html + coverage report --show-missing --fail-under=100 + +maxi_cov: + python tests/maxi_cov.py + coverage html + coverage report --show-missing --fail-under=100 + +build: + python setup.py build_ext --inplace + +clean: + find . -name '*.pyc' -o -name '*.so' -delete + find . -name __pycache__ -delete + rm -rf .coverage .mypy_cache build compliance/reports dist docs/_build htmlcov MANIFEST src/websockets.egg-info diff --git a/testing/web-platform/tests/tools/third_party/websockets/PKG-INFO b/testing/web-platform/tests/tools/third_party/websockets/PKG-INFO deleted file mode 100644 index 3b042a3f9f..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/PKG-INFO +++ /dev/null @@ -1,174 +0,0 @@ -Metadata-Version: 2.1 -Name: websockets -Version: 10.3 -Summary: An implementation of the WebSocket Protocol (RFC 6455 & 7692) -Home-page: https://github.com/aaugustin/websockets -Author: Aymeric Augustin -Author-email: aymeric.augustin@m4x.org -License: BSD -Project-URL: Changelog, https://websockets.readthedocs.io/en/stable/project/changelog.html -Project-URL: Documentation, https://websockets.readthedocs.io/ -Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme -Project-URL: Tracker, https://github.com/aaugustin/websockets/issues -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Requires-Python: >=3.7 -License-File: LICENSE - -.. image:: logo/horizontal.svg - :width: 480px - :alt: websockets - -|licence| |version| |pyversions| |wheel| |tests| |docs| - -.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |version| image:: https://img.shields.io/pypi/v/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main - :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml - -.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg - :target: https://websockets.readthedocs.io/ - -What is ``websockets``? ------------------------ - -websockets is a library for building WebSocket_ servers and clients in Python -with a focus on correctness, simplicity, robustness, and performance. - -.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API - -Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it -provides an elegant coroutine-based API. - -`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ - -Here's how a client sends and receives messages: - -.. copy-pasted because GitHub doesn't support the include directive - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import connect - - async def hello(uri): - async with connect(uri) as websocket: - await websocket.send("Hello world!") - await websocket.recv() - - asyncio.run(hello("ws://localhost:8765")) - -And here's an echo server: - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import serve - - async def echo(websocket): - async for message in websocket: - await websocket.send(message) - - async def main(): - async with serve(echo, "localhost", 8765): - await asyncio.Future() # run forever - - asyncio.run(main()) - -Does that look good? - -`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_ - -Why should I use ``websockets``? --------------------------------- - -The development of ``websockets`` is shaped by four principles: - -1. **Correctness**: ``websockets`` is heavily tested for compliance - with :rfc:`6455`. Continuous integration fails under 100% branch - coverage. - -2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and - ``await ws.send(msg)``. ``websockets`` takes care of managing connections - so you can focus on your application. - -3. **Robustness**: ``websockets`` is built for production. For example, it was - the only library to `handle backpressure correctly`_ before the issue - became widely known in the Python community. - -4. **Performance**: memory usage is optimized and configurable. A C extension - accelerates expensive operations. It's pre-compiled for Linux, macOS and - Windows and packaged in the wheel format for each system and Python version. - -Documentation is a first class concern in the project. Head over to `Read the -Docs`_ and see for yourself. - -.. _Read the Docs: https://websockets.readthedocs.io/ -.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers - -Why shouldn't I use ``websockets``? ------------------------------------ - -* If you prefer callbacks over coroutines: ``websockets`` was created to - provide the best coroutine-based API to manage WebSocket connections in - Python. Pick another library for a callback-based API. - -* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims - at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol - and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP - is minimal — just enough for a HTTP health check. - - If you want to do both in the same server, look at HTTP frameworks that - build on top of ``websockets`` to support WebSocket connections, like - Sanic_. - -.. _Sanic: https://sanicframework.org/en/ - -What else? ----------- - -Bug reports, patches and suggestions are welcome! - -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -.. _Tidelift security contact: https://tidelift.com/security - -For anything else, please open an issue_ or send a `pull request`_. - -.. _issue: https://github.com/aaugustin/websockets/issues/new -.. _pull request: https://github.com/aaugustin/websockets/compare/ - -Participants must uphold the `Contributor Covenant code of conduct`_. - -.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md - -``websockets`` is released under the `BSD license`_. - -.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE - - diff --git a/testing/web-platform/tests/tools/third_party/websockets/README.rst b/testing/web-platform/tests/tools/third_party/websockets/README.rst index 2b9a445ea5..870b208baa 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/README.rst +++ b/testing/web-platform/tests/tools/third_party/websockets/README.rst @@ -2,7 +2,7 @@ :width: 480px :alt: websockets -|licence| |version| |pyversions| |wheel| |tests| |docs| +|licence| |version| |pyversions| |tests| |docs| |openssf| .. |licence| image:: https://img.shields.io/pypi/l/websockets.svg :target: https://pypi.python.org/pypi/websockets @@ -13,15 +13,15 @@ .. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg :target: https://pypi.python.org/pypi/websockets -.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main - :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml +.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests + :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml .. |docs| image:: https://img.shields.io/readthedocs/websockets.svg :target: https://websockets.readthedocs.io/ +.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge + :target: https://bestpractices.coreinfrastructure.org/projects/6475 + What is ``websockets``? ----------------------- @@ -30,37 +30,24 @@ with a focus on correctness, simplicity, robustness, and performance. .. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API -Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it -provides an elegant coroutine-based API. +Built on top of ``asyncio``, Python's standard asynchronous I/O framework, the +default implementation provides an elegant coroutine-based API. -`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ +An implementation on top of ``threading`` and a Sans-I/O implementation are also +available. -Here's how a client sends and receives messages: +`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ .. copy-pasted because GitHub doesn't support the include directive -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import connect - - async def hello(uri): - async with connect(uri) as websocket: - await websocket.send("Hello world!") - await websocket.recv() - - asyncio.run(hello("ws://localhost:8765")) - -And here's an echo server: +Here's an echo server with the ``asyncio`` API: .. code:: python #!/usr/bin/env python import asyncio - from websockets import serve + from websockets.server import serve async def echo(websocket): async for message in websocket: @@ -72,6 +59,23 @@ And here's an echo server: asyncio.run(main()) +Here's how a client sends and receives messages with the ``threading`` API: + +.. code:: python + + #!/usr/bin/env python + + from websockets.sync.client import connect + + def hello(): + with connect("ws://localhost:8765") as websocket: + websocket.send("Hello world!") + message = websocket.recv() + print(f"Received: {message}") + + hello() + + Does that look good? `Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_ @@ -79,7 +83,7 @@ Does that look good? .. raw:: html <hr> - <img align="left" height="150" width="150" src="https://raw.githubusercontent.com/aaugustin/websockets/main/logo/tidelift.png"> + <img align="left" height="150" width="150" src="https://raw.githubusercontent.com/python-websockets/websockets/main/logo/tidelift.png"> <h3 align="center"><i>websockets for enterprise</i></h3> <p align="center"><i>Available as part of the Tidelift Subscription</i></p> <p align="center"><i>The maintainers of websockets and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. <a href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme">Learn more.</a></i></p> @@ -91,9 +95,8 @@ Why should I use ``websockets``? The development of ``websockets`` is shaped by four principles: -1. **Correctness**: ``websockets`` is heavily tested for compliance - with :rfc:`6455`. Continuous integration fails under 100% branch - coverage. +1. **Correctness**: ``websockets`` is heavily tested for compliance with + :rfc:`6455`. Continuous integration fails under 100% branch coverage. 2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and ``await ws.send(msg)``. ``websockets`` takes care of managing connections @@ -123,7 +126,7 @@ Why shouldn't I use ``websockets``? * If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP - is minimal — just enough for a HTTP health check. + is minimal — just enough for an HTTP health check. If you want to do both in the same server, look at HTTP frameworks that build on top of ``websockets`` to support WebSocket connections, like @@ -143,13 +146,13 @@ contact`_. Tidelift will coordinate the fix and disclosure. For anything else, please open an issue_ or send a `pull request`_. -.. _issue: https://github.com/aaugustin/websockets/issues/new -.. _pull request: https://github.com/aaugustin/websockets/compare/ +.. _issue: https://github.com/python-websockets/websockets/issues/new +.. _pull request: https://github.com/python-websockets/websockets/compare/ Participants must uphold the `Contributor Covenant code of conduct`_. -.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md +.. _Contributor Covenant code of conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md ``websockets`` is released under the `BSD license`_. -.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE +.. _BSD license: https://github.com/python-websockets/websockets/blob/main/LICENSE diff --git a/testing/web-platform/tests/tools/third_party/websockets/SECURITY.md b/testing/web-platform/tests/tools/third_party/websockets/SECURITY.md new file mode 100644 index 0000000000..175b20c589 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/SECURITY.md @@ -0,0 +1,12 @@ +# Security + +## Policy + +Only the latest version receives security updates. + +## Contact information + +Please report security vulnerabilities to the +[Tidelift security team](https://tidelift.com/security). + +Tidelift will coordinate the fix and disclosure. diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst b/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst new file mode 100644 index 0000000000..8570f9176d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst @@ -0,0 +1,50 @@ +Autobahn Testsuite +================== + +General information and installation instructions are available at +https://github.com/crossbario/autobahn-testsuite. + +To improve performance, you should compile the C extension first:: + + $ python setup.py build_ext --inplace + +Running the test suite +---------------------- + +All commands below must be run from the directory containing this file. + +To test the server:: + + $ PYTHONPATH=.. python test_server.py + $ wstest -m fuzzingclient + +To test the client:: + + $ wstest -m fuzzingserver + $ PYTHONPATH=.. python test_client.py + +Run the first command in a shell. Run the second command in another shell. +It should take about ten minutes to complete — wstest is the bottleneck. +Then kill the first one with Ctrl-C. + +The test client or server shouldn't display any exceptions. The results are +stored in reports/clients/index.html. + +Note that the Autobahn software only supports Python 2, while ``websockets`` +only supports Python 3; you need two different environments. + +Conformance notes +----------------- + +Some test cases are more strict than the RFC. Given the implementation of the +library and the test echo client or server, ``websockets`` gets a "Non-Strict" +in these cases. + +In 3.2, 3.3, 4.1.3, 4.1.4, 4.2.3, 4.2.4, and 5.15 ``websockets`` notices the +protocol error and closes the connection before it has had a chance to echo +the previous frame. + +In 6.4.3 and 6.4.4, even though it uses an incremental decoder, ``websockets`` +doesn't notice the invalid utf-8 fast enough to get a "Strict" pass. These +tests are more strict than the RFC. + diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json new file mode 100644 index 0000000000..202ff49a03 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json @@ -0,0 +1,11 @@ + +{ + "options": {"failByDrop": false}, + "outdir": "./reports/servers", + + "servers": [{"agent": "websockets", "url": "ws://localhost:8642", "options": {"version": 18}}], + + "cases": ["*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json new file mode 100644 index 0000000000..1bdb42723e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json @@ -0,0 +1,12 @@ + +{ + "url": "ws://localhost:8642", + + "options": {"failByDrop": false}, + "outdir": "./reports/clients", + "webport": 8080, + + "cases": ["*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py new file mode 100644 index 0000000000..1ed4d711e9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py @@ -0,0 +1,48 @@ +import json +import logging +import urllib.parse + +import asyncio +import websockets + + +logging.basicConfig(level=logging.WARNING) + +# Uncomment this line to make only websockets more verbose. +# logging.getLogger('websockets').setLevel(logging.DEBUG) + + +SERVER = "ws://127.0.0.1:8642" +AGENT = "websockets" + + +async def get_case_count(server): + uri = f"{server}/getCaseCount" + async with websockets.connect(uri) as ws: + msg = ws.recv() + return json.loads(msg) + + +async def run_case(server, case, agent): + uri = f"{server}/runCase?case={case}&agent={agent}" + async with websockets.connect(uri, max_size=2 ** 25, max_queue=1) as ws: + async for msg in ws: + await ws.send(msg) + + +async def update_reports(server, agent): + uri = f"{server}/updateReports?agent={agent}" + async with websockets.connect(uri): + pass + + +async def run_tests(server, agent): + cases = await get_case_count(server) + for case in range(1, cases + 1): + print(f"Running test case {case} out of {cases}", end="\r") + await run_case(server, case, agent) + print(f"Ran {cases} test cases ") + await update_reports(server, agent) + + +asyncio.run(run_tests(SERVER, urllib.parse.quote(AGENT))) diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py new file mode 100644 index 0000000000..92f895d926 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py @@ -0,0 +1,29 @@ +import logging + +import asyncio +import websockets + + +logging.basicConfig(level=logging.WARNING) + +# Uncomment this line to make only websockets more verbose. +# logging.getLogger('websockets').setLevel(logging.DEBUG) + + +HOST, PORT = "127.0.0.1", 8642 + + +async def echo(ws): + async for msg in ws: + await ws.send(msg) + + +async def main(): + with websockets.serve(echo, HOST, PORT, max_size=2 ** 25, max_queue=1): + try: + await asyncio.Future() + except KeyboardInterrupt: + pass + + +asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile new file mode 100644 index 0000000000..0458706458 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile @@ -0,0 +1,23 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +livehtml: + sphinx-autobuild --watch "$(SOURCEDIR)/../src" "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png Binary files differdeleted file mode 100644 index 317dc4d985..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png +++ /dev/null diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py new file mode 100644 index 0000000000..9d61dc7173 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py @@ -0,0 +1,171 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import datetime +import importlib +import inspect +import os +import subprocess +import sys + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.join(os.path.abspath(".."), "src")) + + +# -- Project information ----------------------------------------------------- + +project = "websockets" +copyright = f"2013-{datetime.date.today().year}, Aymeric Augustin and contributors" +author = "Aymeric Augustin" + +from websockets.version import tag as version, version as release + + +# -- General configuration --------------------------------------------------- + +nitpicky = True + +nitpick_ignore = [ + # topics/design.rst discusses undocumented APIs + ("py:meth", "client.WebSocketClientProtocol.handshake"), + ("py:meth", "server.WebSocketServerProtocol.handshake"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.is_client"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.messages"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.close_connection"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.close_connection_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.transfer_data"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.transfer_data_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_open"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.ensure_open"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.fail_connection"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_lost"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.read_message"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.write_frame"), +] + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.linkcode", + "sphinx.ext.napoleon", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinxcontrib.spelling", + "sphinxcontrib_trio", + "sphinxext.opengraph", +] +# It is currently inconvenient to install PyEnchant on Apple Silicon. +try: + import sphinxcontrib.spelling +except ImportError: + extensions.remove("sphinxcontrib.spelling") + +autodoc_typehints = "description" + +autodoc_typehints_description_target = "documented" + +# Workaround for https://github.com/sphinx-doc/sphinx/issues/9560 +from sphinx.domains.python import PythonDomain + +assert PythonDomain.object_types["data"].roles == ("data", "obj") +PythonDomain.object_types["data"].roles = ("data", "class", "obj") + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + +spelling_show_suggestions = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# Configure viewcode extension. +from websockets.version import commit + +code_url = f"https://github.com/python-websockets/websockets/blob/{commit}" + +def linkcode_resolve(domain, info): + # Non-linkable objects from the starter kit in the tutorial. + if domain == "js" or info["module"] == "connect4": + return + + assert domain == "py", "expected only Python objects" + + mod = importlib.import_module(info["module"]) + if "." in info["fullname"]: + objname, attrname = info["fullname"].split(".") + obj = getattr(mod, objname) + try: + # object is a method of a class + obj = getattr(obj, attrname) + except AttributeError: + # object is an attribute of a class + return None + else: + obj = getattr(mod, info["fullname"]) + + try: + file = inspect.getsourcefile(obj) + lines = inspect.getsourcelines(obj) + except TypeError: + # e.g. object is a typing.Union + return None + file = os.path.relpath(file, os.path.abspath("..")) + if not file.startswith("src/websockets"): + # e.g. object is a typing.NewType + return None + start, end = lines[1], lines[1] + len(lines[0]) - 1 + + return f"{code_url}/{file}#L{start}-L{end}" + +# Configure opengraph extension + +# Social cards don't support the SVG logo. Also, the text preview looks bad. +ogp_social_cards = {"enable": False} + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "furo" + +html_theme_options = { + "light_css_variables": { + "color-brand-primary": "#306998", # blue from logo + "color-brand-content": "#0b487a", # blue more saturated and less dark + }, + "dark_css_variables": { + "color-brand-primary": "#ffd43bcc", # yellow from logo, more muted than content + "color-brand-content": "#ffd43bd9", # yellow from logo, transparent like text + }, + "sidebar_hide_name": True, +} + +html_logo = "_static/websockets.svg" + +html_favicon = "_static/favicon.ico" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +html_copy_source = False + +html_show_sphinx = False diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst new file mode 100644 index 0000000000..e77f50addd --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst @@ -0,0 +1,69 @@ +Using asyncio +============= + +.. currentmodule:: websockets + +How do I run two coroutines in parallel? +---------------------------------------- + +You must start two tasks, which the event loop will run concurrently. You can +achieve this with :func:`asyncio.gather` or :func:`asyncio.create_task`. + +Keep track of the tasks and make sure they terminate or you cancel them when +the connection terminates. + +Why does my program never receive any messages? +----------------------------------------------- + +Your program runs a coroutine that never yields control to the event loop. The +coroutine that receives messages never gets a chance to run. + +Putting an ``await`` statement in a ``for`` or a ``while`` loop isn't enough +to yield control. Awaiting a coroutine may yield control, but there's no +guarantee that it will. + +For example, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` only yields +control when send buffers are full, which never happens in most practical +cases. + +If you run a loop that contains only synchronous operations and +a :meth:`~legacy.protocol.WebSocketCommonProtocol.send` call, you must yield +control explicitly with :func:`asyncio.sleep`:: + + async def producer(websocket): + message = generate_next_message() + await websocket.send(message) + await asyncio.sleep(0) # yield control to the event loop + +:func:`asyncio.sleep` always suspends the current task, allowing other tasks +to run. This behavior is documented precisely because it isn't expected from +every coroutine. + +See `issue 867`_. + +.. _issue 867: https://github.com/python-websockets/websockets/issues/867 + +Why am I having problems with threads? +-------------------------------------- + +If you choose websockets' default implementation based on :mod:`asyncio`, then +you shouldn't use threads. Indeed, choosing :mod:`asyncio` to handle concurrency +is mutually exclusive with :mod:`threading`. + +If you believe that you need to run websockets in a thread and some logic in +another thread, you should run that logic in a :class:`~asyncio.Task` instead. +If it blocks the event loop, :meth:`~asyncio.loop.run_in_executor` will help. + +This question is really about :mod:`asyncio`. Please review the advice about +:ref:`asyncio-multithreading` in the Python documentation. + +Why does my simple program misbehave mysteriously? +-------------------------------------------------- + +You are using :func:`time.sleep` instead of :func:`asyncio.sleep`, which +blocks the event loop and prevents asyncio from operating normally. + +This may lead to messages getting send but not received, to connection +timeouts, and to unexpected results of shotgun debugging e.g. adding an +unnecessary call to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +makes the program functional. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst new file mode 100644 index 0000000000..c590ac107d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst @@ -0,0 +1,101 @@ +Client +====== + +.. currentmodule:: websockets + +Why does the client close the connection prematurely? +----------------------------------------------------- + +You're exiting the context manager prematurely. Wait for the work to be +finished before exiting. + +For example, if your code has a structure similar to:: + + async with connect(...) as websocket: + asyncio.create_task(do_some_work()) + +change it to:: + + async with connect(...) as websocket: + await do_some_work() + +How do I access HTTP headers? +----------------------------- + +Once the connection is established, HTTP headers are available in +:attr:`~client.WebSocketClientProtocol.request_headers` and +:attr:`~client.WebSocketClientProtocol.response_headers`. + +How do I set HTTP headers? +-------------------------- + +To set the ``Origin``, ``Sec-WebSocket-Extensions``, or +``Sec-WebSocket-Protocol`` headers in the WebSocket handshake request, use the +``origin``, ``extensions``, or ``subprotocols`` arguments of +:func:`~client.connect`. + +To override the ``User-Agent`` header, use the ``user_agent_header`` argument. +Set it to :obj:`None` to remove the header. + +To set other HTTP headers, for example the ``Authorization`` header, use the +``extra_headers`` argument:: + + async with connect(..., extra_headers={"Authorization": ...}) as websocket: + ... + +In the :mod:`threading` API, this argument is named ``additional_headers``:: + + with connect(..., additional_headers={"Authorization": ...}) as websocket: + ... + +How do I force the IP address that the client connects to? +---------------------------------------------------------- + +Use the ``host`` argument of :meth:`~asyncio.loop.create_connection`:: + + await websockets.connect("ws://example.com", host="192.168.0.1") + +:func:`~client.connect` accepts the same arguments as +:meth:`~asyncio.loop.create_connection`. + +How do I close a connection? +---------------------------- + +The easiest is to use :func:`~client.connect` as a context manager:: + + async with connect(...) as websocket: + ... + +The connection is closed when exiting the context manager. + +How do I reconnect when the connection drops? +--------------------------------------------- + +Use :func:`~client.connect` as an asynchronous iterator:: + + async for websocket in websockets.connect(...): + try: + ... + except websockets.ConnectionClosed: + continue + +Make sure you handle exceptions in the ``async for`` loop. Uncaught exceptions +will break out of the loop. + +How do I stop a client that is processing messages in a loop? +------------------------------------------------------------- + +You can close the connection. + +Here's an example that terminates cleanly when it receives SIGTERM on Unix: + +.. literalinclude:: ../../example/faq/shutdown_client.py + :emphasize-lines: 10-13 + +How do I disable TLS/SSL certificate verification? +-------------------------------------------------- + +Look at the ``ssl`` argument of :meth:`~asyncio.loop.create_connection`. + +:func:`~client.connect` accepts the same arguments as +:meth:`~asyncio.loop.create_connection`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst new file mode 100644 index 0000000000..2c63c4f36f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst @@ -0,0 +1,161 @@ +Both sides +========== + +.. currentmodule:: websockets + +What does ``ConnectionClosedError: no close frame received or sent`` mean? +-------------------------------------------------------------------------- + +If you're seeing this traceback in the logs of a server: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + asyncio.exceptions.IncompleteReadError: 0 bytes read on a total of 2 expected bytes + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: no close frame received or sent + +or if a client crashes with this traceback: + +.. code-block:: pytb + + Traceback (most recent call last): + ... + ConnectionResetError: [Errno 54] Connection reset by peer + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: no close frame received or sent + +it means that the TCP connection was lost. As a consequence, the WebSocket +connection was closed without receiving and sending a close frame, which is +abnormal. + +You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it +from being logged. + +There are several reasons why long-lived connections may be lost: + +* End-user devices tend to lose network connectivity often and unpredictably + because they can move out of wireless network coverage, get unplugged from + a wired network, enter airplane mode, be put to sleep, etc. +* HTTP load balancers or proxies that aren't configured for long-lived + connections may terminate connections after a short amount of time, usually + 30 seconds, despite websockets' keepalive mechanism. + +If you're facing a reproducible issue, :ref:`enable debug logs <debugging>` to +see when and how connections are closed. + +What does ``ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received`` mean? +--------------------------------------------------------------------------------------------------------------------- + +If you're seeing this traceback in the logs of a server: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + asyncio.exceptions.CancelledError + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received + +or if a client crashes with this traceback: + +.. code-block:: pytb + + Traceback (most recent call last): + ... + asyncio.exceptions.CancelledError + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received + +it means that the WebSocket connection suffered from excessive latency and was +closed after reaching the timeout of websockets' keepalive mechanism. + +You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it +from being logged. + +There are two main reasons why latency may increase: + +* Poor network connectivity. +* More traffic than the recipient can handle. + +See the discussion of :doc:`timeouts <../topics/timeouts>` for details. + +If websockets' default timeout of 20 seconds is too short for your use case, +you can adjust it with the ``ping_timeout`` argument. + +How do I set a timeout on :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`? +-------------------------------------------------------------------------------- + +On Python ≥ 3.11, use :func:`asyncio.timeout`:: + + async with asyncio.timeout(timeout=10): + message = await websocket.recv() + +On older versions of Python, use :func:`asyncio.wait_for`:: + + message = await asyncio.wait_for(websocket.recv(), timeout=10) + +This technique works for most APIs. When it doesn't, for example with +asynchronous context managers, websockets provides an ``open_timeout`` argument. + +How can I pass arguments to a custom protocol subclass? +------------------------------------------------------- + +You can bind additional arguments to the protocol factory with +:func:`functools.partial`:: + + import asyncio + import functools + import websockets + + class MyServerProtocol(websockets.WebSocketServerProtocol): + def __init__(self, *args, extra_argument=None, **kwargs): + super().__init__(*args, **kwargs) + # do something with extra_argument + + create_protocol = functools.partial(MyServerProtocol, extra_argument=42) + start_server = websockets.serve(..., create_protocol=create_protocol) + +This example was for a server. The same pattern applies on a client. + +How do I keep idle connections open? +------------------------------------ + +websockets sends pings at 20 seconds intervals to keep the connection open. + +It closes the connection if it doesn't get a pong within 20 seconds. + +You can adjust this behavior with ``ping_interval`` and ``ping_timeout``. + +See :doc:`../topics/timeouts` for details. + +How do I respond to pings? +-------------------------- + +If you are referring to Ping_ and Pong_ frames defined in the WebSocket +protocol, don't bother, because websockets handles them for you. + +.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 +.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + +If you are connecting to a server that defines its own heartbeat at the +application level, then you need to build that logic into your application. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst new file mode 100644 index 0000000000..9d5b0d538a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst @@ -0,0 +1,21 @@ +Frequently asked questions +========================== + +.. currentmodule:: websockets + +.. admonition:: Many questions asked in websockets' issue tracker are really + about :mod:`asyncio`. + :class: seealso + + Python's documentation about `developing with asyncio`_ is a good + complement. + + .. _developing with asyncio: https://docs.python.org/3/library/asyncio-dev.html + +.. toctree:: + + server + client + common + asyncio + misc diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst new file mode 100644 index 0000000000..ee5ad23728 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst @@ -0,0 +1,49 @@ +Miscellaneous +============= + +.. currentmodule:: websockets + +Why do I get the error: ``module 'websockets' has no attribute '...'``? +....................................................................... + +Often, this is because you created a script called ``websockets.py`` in your +current working directory. Then ``import websockets`` imports this module +instead of the websockets library. + +.. _real-import-paths: + +Why is the default implementation located in ``websockets.legacy``? +................................................................... + +This is an artifact of websockets' history. For its first eight years, only the +:mod:`asyncio` implementation existed. Then, the Sans-I/O implementation was +added. Moving the code in a ``legacy`` submodule eased this refactoring and +optimized maintainability. + +All public APIs were kept at their original locations. ``websockets.legacy`` +isn't a public API. It's only visible in the source code and in stack traces. +There is no intent to deprecate this implementation — at least until a superior +alternative exists. + +Why is websockets slower than another library in my benchmark? +.............................................................. + +Not all libraries are as feature-complete as websockets. For a fair benchmark, +you should disable features that the other library doesn't provide. Typically, +you may need to disable: + +* Compression: set ``compression=None`` +* Keepalive: set ``ping_interval=None`` +* UTF-8 decoding: send ``bytes`` rather than ``str`` + +If websockets is still slower than another Python library, please file a bug. + +Are there ``onopen``, ``onmessage``, ``onerror``, and ``onclose`` callbacks? +............................................................................ + +No, there aren't. + +websockets provides high-level, coroutine-based APIs. Compared to callbacks, +coroutines make it easier to manage control flow in concurrent code. + +If you prefer callback-based APIs, you should use another library. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst new file mode 100644 index 0000000000..08b412d306 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst @@ -0,0 +1,336 @@ +Server +====== + +.. currentmodule:: websockets + +Why does the server close the connection prematurely? +----------------------------------------------------- + +Your connection handler exits prematurely. Wait for the work to be finished +before returning. + +For example, if your handler has a structure similar to:: + + async def handler(websocket): + asyncio.create_task(do_some_work()) + +change it to:: + + async def handler(websocket): + await do_some_work() + +Why does the server close the connection after one message? +----------------------------------------------------------- + +Your connection handler exits after processing one message. Write a loop to +process multiple messages. + +For example, if your handler looks like this:: + + async def handler(websocket): + print(websocket.recv()) + +change it like this:: + + async def handler(websocket): + async for message in websocket: + print(message) + +*Don't feel bad if this happens to you — it's the most common question in +websockets' issue tracker :-)* + +Why can only one client connect at a time? +------------------------------------------ + +Your connection handler blocks the event loop. Look for blocking calls. + +Any call that may take some time must be asynchronous. + +For example, this connection handler prevents the event loop from running during +one second:: + + async def handler(websocket): + time.sleep(1) + ... + +Change it to:: + + async def handler(websocket): + await asyncio.sleep(1) + ... + +In addition, calling a coroutine doesn't guarantee that it will yield control to +the event loop. + +For example, this connection handler blocks the event loop by sending messages +continuously:: + + async def handler(websocket): + while True: + await websocket.send("firehose!") + +:meth:`~legacy.protocol.WebSocketCommonProtocol.send` completes synchronously as +long as there's space in send buffers. The event loop never runs. (This pattern +is uncommon in real-world applications. It occurs mostly in toy programs.) + +You can avoid the issue by yielding control to the event loop explicitly:: + + async def handler(websocket): + while True: + await websocket.send("firehose!") + await asyncio.sleep(0) + +All this is part of learning asyncio. It isn't specific to websockets. + +See also Python's documentation about `running blocking code`_. + +.. _running blocking code: https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code + +.. _send-message-to-all-users: + +How do I send a message to all users? +------------------------------------- + +Record all connections in a global variable:: + + CONNECTIONS = set() + + async def handler(websocket): + CONNECTIONS.add(websocket) + try: + await websocket.wait_closed() + finally: + CONNECTIONS.remove(websocket) + +Then, call :func:`~websockets.broadcast`:: + + import websockets + + def message_all(message): + websockets.broadcast(CONNECTIONS, message) + +If you're running multiple server processes, make sure you call ``message_all`` +in each process. + +.. _send-message-to-single-user: + +How do I send a message to a single user? +----------------------------------------- + +Record connections in a global variable, keyed by user identifier:: + + CONNECTIONS = {} + + async def handler(websocket): + user_id = ... # identify user in your app's context + CONNECTIONS[user_id] = websocket + try: + await websocket.wait_closed() + finally: + del CONNECTIONS[user_id] + +Then, call :meth:`~legacy.protocol.WebSocketCommonProtocol.send`:: + + async def message_user(user_id, message): + websocket = CONNECTIONS[user_id] # raises KeyError if user disconnected + await websocket.send(message) # may raise websockets.ConnectionClosed + +Add error handling according to the behavior you want if the user disconnected +before the message could be sent. + +This example supports only one connection per user. To support concurrent +connections by the same user, you can change ``CONNECTIONS`` to store a set of +connections for each user. + +If you're running multiple server processes, call ``message_user`` in each +process. The process managing the user's connection sends the message; other +processes do nothing. + +When you reach a scale where server processes cannot keep up with the stream of +all messages, you need a better architecture. For example, you could deploy an +external publish / subscribe system such as Redis_. Server processes would +subscribe their clients. Then, they would receive messages only for the +connections that they're managing. + +.. _Redis: https://redis.io/ + +How do I send a message to a channel, a topic, or some users? +------------------------------------------------------------- + +websockets doesn't provide built-in publish / subscribe functionality. + +Record connections in a global variable, keyed by user identifier, as shown in +:ref:`How do I send a message to a single user?<send-message-to-single-user>` + +Then, build the set of recipients and broadcast the message to them, as shown in +:ref:`How do I send a message to all users?<send-message-to-all-users>` + +:doc:`../howto/django` contains a complete implementation of this pattern. + +Again, as you scale, you may reach the performance limits of a basic in-process +implementation. You may need an external publish / subscribe system like Redis_. + +.. _Redis: https://redis.io/ + +How do I pass arguments to the connection handler? +-------------------------------------------------- + +You can bind additional arguments to the connection handler with +:func:`functools.partial`:: + + import asyncio + import functools + import websockets + + async def handler(websocket, extra_argument): + ... + + bound_handler = functools.partial(handler, extra_argument=42) + start_server = websockets.serve(bound_handler, ...) + +Another way to achieve this result is to define the ``handler`` coroutine in +a scope where the ``extra_argument`` variable exists instead of injecting it +through an argument. + +How do I access the request path? +--------------------------------- + +It is available in the :attr:`~server.WebSocketServerProtocol.path` attribute. + +You may route a connection to different handlers depending on the request path:: + + async def handler(websocket): + if websocket.path == "/blue": + await blue_handler(websocket) + elif websocket.path == "/green": + await green_handler(websocket) + else: + # No handler for this path; close the connection. + return + +You may also route the connection based on the first message received from the +client, as shown in the :doc:`tutorial <../intro/tutorial2>`. When you want to +authenticate the connection before routing it, this is usually more convenient. + +Generally speaking, there is far less emphasis on the request path in WebSocket +servers than in HTTP servers. When a WebSocket server provides a single endpoint, +it may ignore the request path entirely. + +How do I access HTTP headers? +----------------------------- + +To access HTTP headers during the WebSocket handshake, you can override +:attr:`~server.WebSocketServerProtocol.process_request`:: + + async def process_request(self, path, request_headers): + authorization = request_headers["Authorization"] + +Once the connection is established, HTTP headers are available in +:attr:`~server.WebSocketServerProtocol.request_headers` and +:attr:`~server.WebSocketServerProtocol.response_headers`:: + + async def handler(websocket): + authorization = websocket.request_headers["Authorization"] + +How do I set HTTP headers? +-------------------------- + +To set the ``Sec-WebSocket-Extensions`` or ``Sec-WebSocket-Protocol`` headers in +the WebSocket handshake response, use the ``extensions`` or ``subprotocols`` +arguments of :func:`~server.serve`. + +To override the ``Server`` header, use the ``server_header`` argument. Set it to +:obj:`None` to remove the header. + +To set other HTTP headers, use the ``extra_headers`` argument. + +How do I get the IP address of the client? +------------------------------------------ + +It's available in :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address`:: + + async def handler(websocket): + remote_ip = websocket.remote_address[0] + +How do I set the IP addresses that my server listens on? +-------------------------------------------------------- + +Use the ``host`` argument of :meth:`~asyncio.loop.create_server`:: + + await websockets.serve(handler, host="192.168.0.1", port=8080) + +:func:`~server.serve` accepts the same arguments as +:meth:`~asyncio.loop.create_server`. + +What does ``OSError: [Errno 99] error while attempting to bind on address ('::1', 80, 0, 0): address not available`` mean? +-------------------------------------------------------------------------------------------------------------------------- + +You are calling :func:`~server.serve` without a ``host`` argument in a context +where IPv6 isn't available. + +To listen only on IPv4, specify ``host="0.0.0.0"`` or ``family=socket.AF_INET``. + +Refer to the documentation of :meth:`~asyncio.loop.create_server` for details. + +How do I close a connection? +---------------------------- + +websockets takes care of closing the connection when the handler exits. + +How do I stop a server? +----------------------- + +Exit the :func:`~server.serve` context manager. + +Here's an example that terminates cleanly when it receives SIGTERM on Unix: + +.. literalinclude:: ../../example/faq/shutdown_server.py + :emphasize-lines: 12-15,18 + +How do I stop a server while keeping existing connections open? +--------------------------------------------------------------- + +Call the server's :meth:`~server.WebSocketServer.close` method with +``close_connections=False``. + +Here's how to adapt the example just above:: + + async def server(): + ... + + server = await websockets.serve(echo, "localhost", 8765) + await stop + await server.close(close_connections=False) + +How do I implement a health check? +---------------------------------- + +Intercept WebSocket handshake requests with the +:meth:`~server.WebSocketServerProtocol.process_request` hook. + +When a request is sent to the health check endpoint, treat is as an HTTP request +and return a ``(status, headers, body)`` tuple, as in this example: + +.. literalinclude:: ../../example/faq/health_check_server.py + :emphasize-lines: 7-9,18 + +How do I run HTTP and WebSocket servers on the same port? +--------------------------------------------------------- + +You don't. + +HTTP and WebSocket have widely different operational characteristics. Running +them with the same server becomes inconvenient when you scale. + +Providing an HTTP server is out of scope for websockets. It only aims at +providing a WebSocket server. + +There's limited support for returning HTTP responses with the +:attr:`~server.WebSocketServerProtocol.process_request` hook. + +If you need more, pick an HTTP server and run it separately. + +Alternatively, pick an HTTP framework that builds on top of ``websockets`` to +support WebSocket connections, like Sanic_. + +.. _Sanic: https://sanicframework.org/en/ diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst new file mode 100644 index 0000000000..fc736a5918 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst @@ -0,0 +1,31 @@ +Reload on code changes +====================== + +When developing a websockets server, you may run it locally to test changes. +Unfortunately, whenever you want to try a new version of the code, you must +stop the server and restart it, which slows down your development process. + +Web frameworks such as Django or Flask provide a development server that +reloads the application automatically when you make code changes. There is no +such functionality in websockets because it's designed for production rather +than development. + +However, you can achieve the same result easily. + +Install watchdog_ with the ``watchmedo`` shell utility: + +.. code-block:: console + + $ pip install 'watchdog[watchmedo]' + +.. _watchdog: https://pypi.org/project/watchdog/ + +Run your server with ``watchmedo auto-restart``: + +.. code-block:: console + + $ watchmedo auto-restart --pattern "*.py" --recursive --signal SIGTERM \ + python app.py + +This example assumes that the server is defined in a script called ``app.py``. +Adapt it as necessary. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst new file mode 100644 index 0000000000..95b551f673 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst @@ -0,0 +1,87 @@ +Cheat sheet +=========== + +.. currentmodule:: websockets + +Server +------ + +* Write a coroutine that handles a single connection. It receives a WebSocket + protocol instance and the URI path in argument. + + * Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send + messages at any time. + + * When :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` raises + :exc:`~exceptions.ConnectionClosed`, clean up and exit. If you started + other :class:`asyncio.Task`, terminate them before exiting. + + * If you aren't awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, + consider awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed` + to detect quickly when the connection is closed. + + * You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't + needed in general. + +* Create a server with :func:`~server.serve` which is similar to asyncio's + :meth:`~asyncio.loop.create_server`. You can also use it as an asynchronous + context manager. + + * The server takes care of establishing connections, then lets the handler + execute the application logic, and finally closes the connection after the + handler exits normally or with an exception. + + * For advanced customization, you may subclass + :class:`~server.WebSocketServerProtocol` and pass either this subclass or + a factory function as the ``create_protocol`` argument. + +Client +------ + +* Create a client with :func:`~client.connect` which is similar to asyncio's + :meth:`~asyncio.loop.create_connection`. You can also use it as an + asynchronous context manager. + + * For advanced customization, you may subclass + :class:`~client.WebSocketClientProtocol` and pass either this subclass or + a factory function as the ``create_protocol`` argument. + +* Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send messages + at any time. + +* You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't + needed in general. + +* If you aren't using :func:`~client.connect` as a context manager, call + :meth:`~legacy.protocol.WebSocketCommonProtocol.close` to terminate the connection. + +.. _debugging: + +Debugging +--------- + +If you don't understand what websockets is doing, enable logging:: + + import logging + logger = logging.getLogger('websockets') + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + +The logs contain: + +* Exceptions in the connection handler at the ``ERROR`` level +* Exceptions in the opening or closing handshake at the ``INFO`` level +* All frames at the ``DEBUG`` level — this can be very verbose + +If you're new to ``asyncio``, you will certainly encounter issues that are +related to asynchronous programming in general rather than to websockets in +particular. Fortunately Python's official documentation provides advice to +`develop with asyncio`_. Check it out: it's invaluable! + +.. _develop with asyncio: https://docs.python.org/3/library/asyncio-dev.html + diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst new file mode 100644 index 0000000000..e3da0a878b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst @@ -0,0 +1,294 @@ +Integrate with Django +===================== + +If you're looking at adding real-time capabilities to a Django project with +WebSocket, you have two main options. + +1. Using Django Channels_, a project adding WebSocket to Django, among other + features. This approach is fully supported by Django. However, it requires + switching to a new deployment architecture. + +2. Deploying a separate WebSocket server next to your Django project. This + technique is well suited when you need to add a small set of real-time + features — maybe a notification service — to an HTTP application. + +.. _Channels: https://channels.readthedocs.io/ + +This guide shows how to implement the second technique with websockets. It +assumes familiarity with Django. + +Authenticate connections +------------------------ + +Since the websockets server runs outside of Django, we need to integrate it +with ``django.contrib.auth``. + +We will generate authentication tokens in the Django project. Then we will +send them to the websockets server, where they will authenticate the user. + +Generating a token for the current user and making it available in the browser +is up to you. You could render the token in a template or fetch it with an API +call. + +Refer to the topic guide on :doc:`authentication <../topics/authentication>` +for details on this design. + +Generate tokens +............... + +We want secure, short-lived tokens containing the user ID. We'll rely on +`django-sesame`_, a small library designed exactly for this purpose. + +.. _django-sesame: https://github.com/aaugustin/django-sesame + +Add django-sesame to the dependencies of your Django project, install it, and +configure it in the settings of the project: + +.. code-block:: python + + AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "sesame.backends.ModelBackend", + ] + +(If your project already uses another authentication backend than the default +``"django.contrib.auth.backends.ModelBackend"``, adjust accordingly.) + +You don't need ``"sesame.middleware.AuthenticationMiddleware"``. It is for +authenticating users in the Django server, while we're authenticating them in +the websockets server. + +We'd like our tokens to be valid for 30 seconds. We expect web pages to load +and to establish the WebSocket connection within this delay. Configure +django-sesame accordingly in the settings of your Django project: + +.. code-block:: python + + SESAME_MAX_AGE = 30 + +If you expect your web site to load faster for all clients, a shorter lifespan +is possible. However, in the context of this document, it would make manual +testing more difficult. + +You could also enable single-use tokens. However, this would update the last +login date of the user every time a WebSocket connection is established. This +doesn't seem like a good idea, both in terms of behavior and in terms of +performance. + +Now you can generate tokens in a ``django-admin shell`` as follows: + +.. code-block:: pycon + + >>> from django.contrib.auth import get_user_model + >>> User = get_user_model() + >>> user = User.objects.get(username="<your username>") + >>> from sesame.utils import get_token + >>> get_token(user) + '<your token>' + +Keep this console open: since tokens expire after 30 seconds, you'll have to +generate a new token every time you want to test connecting to the server. + +Validate tokens +............... + +Let's move on to the websockets server. + +Add websockets to the dependencies of your Django project and install it. +Indeed, we're going to reuse the environment of the Django project, so we can +call its APIs in the websockets server. + +Now here's how to implement authentication. + +.. literalinclude:: ../../example/django/authentication.py + +Let's unpack this code. + +We're calling ``django.setup()`` before doing anything with Django because +we're using Django in a `standalone script`_. This assumes that the +``DJANGO_SETTINGS_MODULE`` environment variable is set to the Python path to +your settings module. + +.. _standalone script: https://docs.djangoproject.com/en/stable/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage + +The connection handler reads the first message received from the client, which +is expected to contain a django-sesame token. Then it authenticates the user +with ``get_user()``, the API for `authentication outside a view`_. If +authentication fails, it closes the connection and exits. + +.. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view + +When we call an API that makes a database query such as ``get_user()``, we +wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't +support asynchronous I/O. It would block the event loop if it didn't run in a +separate thread. :func:`~asyncio.to_thread` is available since Python 3.9. In +earlier versions, use :meth:`~asyncio.loop.run_in_executor` instead. + +Finally, we start a server with :func:`~websockets.server.serve`. + +We're ready to test! + +Save this code to a file called ``authentication.py``, make sure the +``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the +websockets server: + +.. code-block:: console + + $ python authentication.py + +Generate a new token — remember, they're only valid for 30 seconds — and use +it to connect to your server. Paste your token and press Enter when you get a +prompt: + +.. code-block:: console + + $ python -m websockets ws://localhost:8888/ + Connected to ws://localhost:8888/ + > <your token> + < Hello <your username>! + Connection closed: 1000 (OK). + +It works! + +If you enter an expired or invalid token, authentication fails and the server +closes the connection: + +.. code-block:: console + + $ python -m websockets ws://localhost:8888/ + Connected to ws://localhost:8888. + > not a token + Connection closed: 1011 (internal error) authentication failed. + +You can also test from a browser by generating a new token and running the +following code in the JavaScript console of the browser: + +.. code-block:: javascript + + websocket = new WebSocket("ws://localhost:8888/"); + websocket.onopen = (event) => websocket.send("<your token>"); + websocket.onmessage = (event) => console.log(event.data); + +If you don't want to import your entire Django project into the websockets +server, you can build a separate Django project with ``django.contrib.auth``, +``django-sesame``, a suitable ``User`` model, and a subset of the settings of +the main project. + +Stream events +------------- + +We can connect and authenticate but our server doesn't do anything useful yet! + +Let's send a message every time a user makes an action in the admin. This +message will be broadcast to all users who can access the model on which the +action was made. This may be used for showing notifications to other users. + +Many use cases for WebSocket with Django follow a similar pattern. + +Set up event bus +................ + +We need a event bus to enable communications between Django and websockets. +Both sides connect permanently to the bus. Then Django writes events and +websockets reads them. For the sake of simplicity, we'll rely on `Redis +Pub/Sub`_. + +.. _Redis Pub/Sub: https://redis.io/topics/pubsub + +The easiest way to add Redis to a Django project is by configuring a cache +backend with `django-redis`_. This library manages connections to Redis +efficiently, persisting them between requests, and provides an API to access +the Redis connection directly. + +.. _django-redis: https://github.com/jazzband/django-redis + +Install Redis, add django-redis to the dependencies of your Django project, +install it, and configure it in the settings of the project: + +.. code-block:: python + + CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/1", + }, + } + +If you already have a default cache, add a new one with a different name and +change ``get_redis_connection("default")`` in the code below to the same name. + +Publish events +.............. + +Now let's write events to the bus. + +Add the following code to a module that is imported when your Django project +starts. Typically, you would put it in a ``signals.py`` module, which you +would import in the ``AppConfig.ready()`` method of one of your apps: + +.. literalinclude:: ../../example/django/signals.py + +This code runs every time the admin saves a ``LogEntry`` object to keep track +of a change. It extracts interesting data, serializes it to JSON, and writes +an event to Redis. + +Let's check that it works: + +.. code-block:: console + + $ redis-cli + 127.0.0.1:6379> SELECT 1 + OK + 127.0.0.1:6379[1]> SUBSCRIBE events + Reading messages... (press Ctrl-C to quit) + 1) "subscribe" + 2) "events" + 3) (integer) 1 + +Leave this command running, start the Django development server and make +changes in the admin: add, modify, or delete objects. You should see +corresponding events published to the ``"events"`` stream. + +Broadcast events +................ + +Now let's turn to reading events and broadcasting them to connected clients. +We need to add several features: + +* Keep track of connected clients so we can broadcast messages. +* Tell which content types the user has permission to view or to change. +* Connect to the message bus and read events. +* Broadcast these events to users who have corresponding permissions. + +Here's a complete implementation. + +.. literalinclude:: ../../example/django/notifications.py + +Since the ``get_content_types()`` function makes a database query, it is +wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket +connection is open; then its result is cached for the lifetime of the +connection. Indeed, running it for each message would trigger database queries +for all connected users at the same time, which would hurt the database. + +The connection handler merely registers the connection in a global variable, +associated to the list of content types for which events should be sent to +that connection, and waits until the client disconnects. + +The ``process_events()`` function reads events from Redis and broadcasts them +to all connections that should receive them. We don't care much if a sending a +notification fails — this happens when a connection drops between the moment +we iterate on connections and the moment the corresponding message is sent — +so we start a task with for each message and forget about it. Also, this means +we're immediately ready to process the next event, even if it takes time to +send a message to a slow client. + +Since Redis can publish a message to multiple subscribers, multiple instances +of this server can safely run in parallel. + +Does it scale? +-------------- + +In theory, given enough servers, this design can scale to a hundred million +clients, since Redis can handle ten thousand servers and each server can +handle ten thousand clients. In practice, you would need a more scalable +message bus before reaching that scale, due to the volume of messages. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst new file mode 100644 index 0000000000..3c8a7d72a6 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst @@ -0,0 +1,30 @@ +Write an extension +================== + +.. currentmodule:: websockets.extensions + +During the opening handshake, WebSocket clients and servers negotiate which +extensions_ will be used with which parameters. Then each frame is processed +by extensions before being sent or after being received. + +.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9 + +As a consequence, writing an extension requires implementing several classes: + +* Extension Factory: it negotiates parameters and instantiates the extension. + + Clients and servers require separate extension factories with distinct APIs. + + Extension factories are the public API of an extension. + +* Extension: it decodes incoming frames and encodes outgoing frames. + + If the extension is symmetrical, clients and servers can use the same + class. + + Extensions are initialized by extension factories, so they don't need to be + part of the public API of an extension. + +websockets provides base classes for extension factories and extensions. +See :class:`ClientExtensionFactory`, :class:`ServerExtensionFactory`, +and :class:`Extension` for details. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst new file mode 100644 index 0000000000..ed001a2aee --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst @@ -0,0 +1,177 @@ +Deploy to Fly +================ + +This guide describes how to deploy a websockets server to Fly_. + +.. _Fly: https://fly.io/ + +.. admonition:: The free tier of Fly is sufficient for trying this guide. + :class: tip + + The `free tier`__ include up to three small VMs. This guide uses only one. + + __ https://fly.io/docs/about/pricing/ + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/fly/app.py + :language: python + +This app implements typical requirements for running on a Platform as a Service: + +* it provides a health check at ``/healthz``; +* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal. + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/fly/requirements.txt + :language: text + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Follow the instructions__ to install the Fly CLI, if you haven't done that yet. + +__ https://fly.io/docs/hands-on/install-flyctl/ + +Sign up or log in to Fly. + +Launch the app — you'll have to pick a different name because I'm already using +``websockets-echo``: + +.. code-block:: console + + $ fly launch + Creating app in ... + Scanning source code + Detected a Python app + Using the following build configuration: + Builder: paketobuildpacks/builder:base + ? App Name (leave blank to use an auto-generated name): websockets-echo + ? Select organization: ... + ? Select region: ... + Created app websockets-echo in organization ... + Wrote config file fly.toml + ? Would you like to set up a Postgresql database now? No + We have generated a simple Procfile for you. Modify it to fit your needs and run "fly deploy" to deploy your application. + +.. admonition:: This will build the image with a generic buildpack. + :class: tip + + Fly can `build images`__ with a Dockerfile or a buildpack. Here, ``fly + launch`` configures a generic Paketo buildpack. + + If you'd rather package the app with a Dockerfile, check out the guide to + :ref:`containerize an application <containerize-application>`. + + __ https://fly.io/docs/reference/builders/ + +Replace the auto-generated ``fly.toml`` with: + +.. literalinclude:: ../../example/deployment/fly/fly.toml + :language: toml + +This configuration: + +* listens on port 443, terminates TLS, and forwards to the app on port 8080; +* declares a health check at ``/healthz``; +* requests a ``SIGTERM`` for terminating the app. + +Replace the auto-generated ``Procfile`` with: + +.. literalinclude:: ../../example/deployment/fly/Procfile + :language: text + +This tells Fly how to run the app. + +Now you can deploy it: + +.. code-block:: console + + $ fly deploy + + ... lots of output... + + ==> Monitoring deployment + + 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing] + --> v0 deployed successfully + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Fly app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + > + +In another shell, restart the app — again, replace ``websockets-echo`` with your +app: + +.. code-block:: console + + $ fly restart websockets-echo + websockets-echo is being restarted + +Go back to the first shell. The connection is closed with code 1001 (going +away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst new file mode 100644 index 0000000000..fdaab04011 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst @@ -0,0 +1,61 @@ +Deploy behind HAProxy +===================== + +This guide demonstrates a way to load balance connections across multiple +websockets server processes running on the same machine with HAProxy_. + +We'll run server processes with Supervisor as described in :doc:`this guide +<supervisor>`. + +.. _HAProxy: https://www.haproxy.org/ + +Run server processes +-------------------- + +Save this app to ``app.py``: + +.. literalinclude:: ../../example/deployment/haproxy/app.py + :emphasize-lines: 24 + +Each server process listens on a different port by extracting an incremental +index from an environment variable set by Supervisor. + +Save this configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/haproxy/supervisord.conf + +This configuration runs four instances of the app. + +Install Supervisor and run it: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + +Configure and run HAProxy +------------------------- + +Here's a simple HAProxy configuration to load balance connections across four +processes: + +.. literalinclude:: ../../example/deployment/haproxy/haproxy.cfg + +In the backend configuration, we set the load balancing method to +``leastconn`` in order to balance the number of active connections across +servers. This is best for long running connections. + +Save the configuration to ``haproxy.cfg``, install HAProxy, and run it: + +.. code-block:: console + + $ haproxy -f haproxy.cfg + +You can confirm that HAProxy proxies connections properly: + +.. code-block:: console + + $ PYTHONPATH=src python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst new file mode 100644 index 0000000000..a97d2e7ce0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst @@ -0,0 +1,183 @@ +Deploy to Heroku +================ + +This guide describes how to deploy a websockets server to Heroku_. The same +principles should apply to other Platform as a Service providers. + +.. _Heroku: https://www.heroku.com/ + +.. admonition:: Heroku no longer offers a free tier. + :class: attention + + When this tutorial was written, in September 2021, Heroku offered a free + tier where a websockets app could run at no cost. In November 2022, Heroku + removed the free tier, making it impossible to maintain this document. As a + consequence, it isn't updated anymore and may be removed in the future. + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create repository +----------------- + +Deploying to Heroku requires a git repository. Let's initialize one: + +.. code-block:: console + + $ mkdir websockets-echo + $ cd websockets-echo + $ git init -b main + Initialized empty Git repository in websockets-echo/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) 1e7947d] Initial commit. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/heroku/app.py + :language: python + +Heroku expects the server to `listen on a specific port`_, which is provided +in the ``$PORT`` environment variable. The app reads it and passes it to +:func:`~websockets.server.serve`. + +.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port + +Heroku sends a ``SIGTERM`` signal to all processes when `shutting down a +dyno`_. When the app receives this signal, it closes connections and exits +cleanly. + +.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/heroku/requirements.txt + :language: text + +Create a ``Procfile``. + +.. literalinclude:: ../../example/deployment/heroku/Procfile + +This tells Heroku how to run the app. + +Confirm that you created the correct files and commit them to git: + +.. code-block:: console + + $ ls + Procfile app.py requirements.txt + $ git add . + $ git commit -m "Initial implementation." + [main 8418c62] Initial implementation. + 3 files changed, 32 insertions(+) + create mode 100644 Procfile + create mode 100644 app.py + create mode 100644 requirements.txt + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Follow the instructions_ to install the Heroku CLI, if you haven't done that +yet. + +.. _instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up + +Sign up or log in to Heroku. + +Create a Heroku app — you'll have to pick a different name because I'm already +using ``websockets-echo``: + +.. code-block:: console + + $ heroku create websockets-echo + Creating ⬢ websockets-echo... done + https://websockets-echo.herokuapp.com/ | https://git.heroku.com/websockets-echo.git + +.. code-block:: console + + $ git push heroku + + ... lots of output... + + remote: -----> Launching... + remote: Released v1 + remote: https://websockets-echo.herokuapp.com/ deployed to Heroku + remote: + remote: Verifying deploy... done. + To https://git.heroku.com/websockets-echo.git + * [new branch] main -> main + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Heroku app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + > + +In another shell, restart the app — again, replace ``websockets-echo`` with your +app: + +.. code-block:: console + + $ heroku dyno:restart -a websockets-echo + Restarting dynos on ⬢ websockets-echo... done + +Go back to the first shell. The connection is closed with code 1001 (going +away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst new file mode 100644 index 0000000000..ddbe67d3ae --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst @@ -0,0 +1,56 @@ +How-to guides +============= + +In a hurry? Check out these examples. + +.. toctree:: + :titlesonly: + + quickstart + +If you're stuck, perhaps you'll find the answer here. + +.. toctree:: + :titlesonly: + + cheatsheet + patterns + autoreload + +This guide will help you integrate websockets into a broader system. + +.. toctree:: + :titlesonly: + + django + +The WebSocket protocol makes provisions for extending or specializing its +features, which websockets supports fully. + +.. toctree:: + :titlesonly: + + extensions + +.. _deployment-howto: + +Once your application is ready, learn how to deploy it on various platforms. + +.. toctree:: + :titlesonly: + + render + fly + heroku + kubernetes + supervisor + nginx + haproxy + +If you're integrating the Sans-I/O layer of websockets into a library, rather +than building an application with websockets, follow this guide. + +.. toctree:: + :maxdepth: 2 + + sansio diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst new file mode 100644 index 0000000000..064a6ac4d5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst @@ -0,0 +1,215 @@ +Deploy to Kubernetes +==================== + +This guide describes how to deploy a websockets server to Kubernetes_. It +assumes familiarity with Docker and Kubernetes. + +We're going to deploy a simple app to a local Kubernetes cluster and to ensure +that it scales as expected. + +In a more realistic context, you would follow your organization's practices +for deploying to Kubernetes, but you would apply the same principles as far as +websockets is concerned. + +.. _Kubernetes: https://kubernetes.io/ + +.. _containerize-application: + +Containerize application +------------------------ + +Here's the app we're going to deploy. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/kubernetes/app.py + +This is an echo server with one twist: every message blocks the server for +100ms, which creates artificial starvation of CPU time. This makes it easier +to saturate the server for load testing. + +The app exposes a health check on ``/healthz``. It also provides two other +endpoints for testing purposes: ``/inemuri`` will make the app unresponsive +for 10 seconds and ``/seppuku`` will terminate it. + +The quest for the perfect Python container image is out of scope of this +guide, so we'll go for the simplest possible configuration instead: + +.. literalinclude:: ../../example/deployment/kubernetes/Dockerfile + +After saving this ``Dockerfile``, build the image: + +.. code-block:: console + + $ docker build -t websockets-test:1.0 . + +Test your image by running: + +.. code-block:: console + + $ docker run --name run-websockets-test --publish 32080:80 --rm \ + websockets-test:1.0 + +Then, in another shell, in a virtualenv where websockets is installed, connect +to the app and check that it echoes anything you send: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + > Hey there! + < Hey there! + > + +Now, in yet another shell, stop the app with: + +.. code-block:: console + + $ docker kill -s TERM run-websockets-test + +Going to the shell where you connected to the app, you can confirm that it +shut down gracefully: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + > Hey there! + < Hey there! + Connection closed: 1001 (going away). + +If it didn't, you'd get code 1006 (abnormal closure). + +Deploy application +------------------ + +Configuring Kubernetes is even further beyond the scope of this guide, so +we'll use a basic configuration for testing, with just one Service_ and one +Deployment_: + +.. literalinclude:: ../../example/deployment/kubernetes/deployment.yaml + +For local testing, a service of type NodePort_ is good enough. For deploying +to production, you would configure an Ingress_. + +.. _Service: https://kubernetes.io/docs/concepts/services-networking/service/ +.. _Deployment: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ +.. _NodePort: https://kubernetes.io/docs/concepts/services-networking/service/#nodeport +.. _Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/ + +After saving this to a file called ``deployment.yaml``, you can deploy: + +.. code-block:: console + + $ kubectl apply -f deployment.yaml + service/websockets-test created + deployment.apps/websockets-test created + +Now you have a deployment with one pod running: + +.. code-block:: console + + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 1/1 1 1 10s + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 0 10s + +You can connect to the service — press Ctrl-D to exit: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + Connection closed: 1000 (OK). + +Validate deployment +------------------- + +First, let's ensure the liveness probe works by making the app unresponsive: + +.. code-block:: console + + $ curl http://localhost:32080/inemuri + Sleeping for 10s + +Since we have only one pod, we know that this pod will go to sleep. + +The liveness probe is configured to run every second. By default, liveness +probes time out after one second and have a threshold of three failures. +Therefore Kubernetes should restart the pod after at most 5 seconds. + +Indeed, after a few seconds, the pod reports a restart: + +.. code-block:: console + + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 1 42s + +Next, let's take it one step further and crash the app: + +.. code-block:: console + + $ curl http://localhost:32080/seppuku + Terminating + +The pod reports a second restart: + +.. code-block:: console + + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 2 72s + +All good — Kubernetes delivers on its promise to keep our app alive! + +Scale deployment +---------------- + +Of course, Kubernetes is for scaling. Let's scale — modestly — to 10 pods: + +.. code-block:: console + + $ kubectl scale deployment.apps/websockets-test --replicas=10 + deployment.apps/websockets-test scaled + +After a few seconds, we have 10 pods running: + +.. code-block:: console + + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 10/10 10 10 10m + +Now let's generate load. We'll use this script: + +.. literalinclude:: ../../example/deployment/kubernetes/benchmark.py + +We'll connect 500 clients in parallel, meaning 50 clients per pod, and have +each client send 6 messages. Since the app blocks for 100ms before responding, +if connections are perfectly distributed, we expect a total run time slightly +over 50 * 6 * 0.1 = 30 seconds. + +Let's try it: + +.. code-block:: console + + $ ulimit -n 512 + $ time python benchmark.py 500 6 + python benchmark.py 500 6 2.40s user 0.51s system 7% cpu 36.471 total + +A total runtime of 36 seconds is in the right ballpark. Repeating this +experiment with other parameters shows roughly consistent results, with the +high variability you'd expect from a quick benchmark without any effort to +stabilize the test setup. + +Finally, we can scale back to one pod. + +.. code-block:: console + + $ kubectl scale deployment.apps/websockets-test --replicas=1 + deployment.apps/websockets-test scaled + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 1/1 1 1 15m diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst new file mode 100644 index 0000000000..30545fbc7d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst @@ -0,0 +1,84 @@ +Deploy behind nginx +=================== + +This guide demonstrates a way to load balance connections across multiple +websockets server processes running on the same machine with nginx_. + +We'll run server processes with Supervisor as described in :doc:`this guide +<supervisor>`. + +.. _nginx: https://nginx.org/ + +Run server processes +-------------------- + +Save this app to ``app.py``: + +.. literalinclude:: ../../example/deployment/nginx/app.py + :emphasize-lines: 21,23 + +We'd like to nginx to connect to websockets servers via Unix sockets in order +to avoid the overhead of TCP for communicating between processes running in +the same OS. + +We start the app with :func:`~websockets.server.unix_serve`. Each server +process listens on a different socket thanks to an environment variable set +by Supervisor to a different value. + +Save this configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/nginx/supervisord.conf + +This configuration runs four instances of the app. + +Install Supervisor and run it: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + +Configure and run nginx +----------------------- + +Here's a simple nginx configuration to load balance connections across four +processes: + +.. literalinclude:: ../../example/deployment/nginx/nginx.conf + +We set ``daemon off`` so we can run nginx in the foreground for testing. + +Then we combine the `WebSocket proxying`_ and `load balancing`_ guides: + +* The WebSocket protocol requires HTTP/1.1. We must set the HTTP protocol + version to 1.1, else nginx defaults to HTTP/1.0 for proxying. + +* The WebSocket handshake involves the ``Connection`` and ``Upgrade`` HTTP + headers. We must pass them to the upstream explicitly, else nginx drops + them because they're hop-by-hop headers. + + We deviate from the `WebSocket proxying`_ guide because its example adds a + ``Connection: Upgrade`` header to every upstream request, even if the + original request didn't contain that header. + +* In the upstream configuration, we set the load balancing method to + ``least_conn`` in order to balance the number of active connections across + servers. This is best for long running connections. + +.. _WebSocket proxying: http://nginx.org/en/docs/http/websocket.html +.. _load balancing: http://nginx.org/en/docs/http/load_balancing.html + +Save the configuration to ``nginx.conf``, install nginx, and run it: + +.. code-block:: console + + $ nginx -c nginx.conf -p . + +You can confirm that nginx proxies connections properly: + +.. code-block:: console + + $ PYTHONPATH=src python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst new file mode 100644 index 0000000000..c6f325d213 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst @@ -0,0 +1,110 @@ +Patterns +======== + +.. currentmodule:: websockets + +Here are typical patterns for processing messages in a WebSocket server or +client. You will certainly implement some of them in your application. + +This page gives examples of connection handlers for a server. However, they're +also applicable to a client, simply by assuming that ``websocket`` is a +connection created with :func:`~client.connect`. + +WebSocket connections are long-lived. You will usually write a loop to process +several messages during the lifetime of a connection. + +Consumer +-------- + +To receive messages from the WebSocket connection:: + + async def consumer_handler(websocket): + async for message in websocket: + await consumer(message) + +In this example, ``consumer()`` is a coroutine implementing your business +logic for processing a message received on the WebSocket connection. Each +message may be :class:`str` or :class:`bytes`. + +Iteration terminates when the client disconnects. + +Producer +-------- + +To send messages to the WebSocket connection:: + + async def producer_handler(websocket): + while True: + message = await producer() + await websocket.send(message) + +In this example, ``producer()`` is a coroutine implementing your business +logic for generating the next message to send on the WebSocket connection. +Each message must be :class:`str` or :class:`bytes`. + +Iteration terminates when the client disconnects +because :meth:`~server.WebSocketServerProtocol.send` raises a +:exc:`~exceptions.ConnectionClosed` exception, +which breaks out of the ``while True`` loop. + +Consumer and producer +--------------------- + +You can receive and send messages on the same WebSocket connection by +combining the consumer and producer patterns. This requires running two tasks +in parallel:: + + async def handler(websocket): + await asyncio.gather( + consumer_handler(websocket), + producer_handler(websocket), + ) + +If a task terminates, :func:`~asyncio.gather` doesn't cancel the other task. +This can result in a situation where the producer keeps running after the +consumer finished, which may leak resources. + +Here's a way to exit and close the WebSocket connection as soon as a task +terminates, after canceling the other task:: + + async def handler(websocket): + consumer_task = asyncio.create_task(consumer_handler(websocket)) + producer_task = asyncio.create_task(producer_handler(websocket)) + done, pending = await asyncio.wait( + [consumer_task, producer_task], + return_when=asyncio.FIRST_COMPLETED, + ) + for task in pending: + task.cancel() + +Registration +------------ + +To keep track of currently connected clients, you can register them when they +connect and unregister them when they disconnect:: + + connected = set() + + async def handler(websocket): + # Register. + connected.add(websocket) + try: + # Broadcast a message to all connected clients. + websockets.broadcast(connected, "Hello!") + await asyncio.sleep(10) + finally: + # Unregister. + connected.remove(websocket) + +This example maintains the set of connected clients in memory. This works as +long as you run a single process. It doesn't scale to multiple processes. + +Publish–subscribe +----------------- + +If you plan to run multiple processes and you want to communicate updates +between processes, then you must deploy a messaging system. You may find +publish-subscribe functionality useful. + +A complete implementation of this idea with Redis is described in +the :doc:`Django integration guide <../howto/django>`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst new file mode 100644 index 0000000000..ab870952c1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst @@ -0,0 +1,170 @@ +Quick start +=========== + +.. currentmodule:: websockets + +Here are a few examples to get you started quickly with websockets. + +Say "Hello world!" +------------------ + +Here's a WebSocket server. + +It receives a name from the client, sends a greeting, and closes the connection. + +.. literalinclude:: ../../example/quickstart/server.py + :caption: server.py + :language: python + :linenos: + +:func:`~server.serve` executes the connection handler coroutine ``hello()`` +once for each WebSocket connection. It closes the WebSocket connection when +the handler returns. + +Here's a corresponding WebSocket client. + +It sends a name to the server, receives a greeting, and closes the connection. + +.. literalinclude:: ../../example/quickstart/client.py + :caption: client.py + :language: python + :linenos: + +Using :func:`~client.connect` as an asynchronous context manager ensures the +WebSocket connection is closed. + +.. _secure-server-example: + +Encrypt connections +------------------- + +Secure WebSocket connections improve confidentiality and also reliability +because they reduce the risk of interference by bad proxies. + +The ``wss`` protocol is to ``ws`` what ``https`` is to ``http``. The +connection is encrypted with TLS_ (Transport Layer Security). ``wss`` +requires certificates like ``https``. + +.. _TLS: https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security + +.. admonition:: TLS vs. SSL + :class: tip + + TLS is sometimes referred to as SSL (Secure Sockets Layer). SSL was an + earlier encryption protocol; the name stuck. + +Here's how to adapt the server to encrypt connections. You must download +:download:`localhost.pem <../../example/quickstart/localhost.pem>` and save it +in the same directory as ``server_secure.py``. + +.. literalinclude:: ../../example/quickstart/server_secure.py + :caption: server_secure.py + :language: python + :linenos: + +Here's how to adapt the client similarly. + +.. literalinclude:: ../../example/quickstart/client_secure.py + :caption: client_secure.py + :language: python + :linenos: + +In this example, the client needs a TLS context because the server uses a +self-signed certificate. + +When connecting to a secure WebSocket server with a valid certificate — any +certificate signed by a CA that your Python installation trusts — you can +simply pass ``ssl=True`` to :func:`~client.connect`. + +.. admonition:: Configure the TLS context securely + :class: attention + + This example demonstrates the ``ssl`` argument with a TLS certificate shared + between the client and the server. This is a simplistic setup. + + Please review the advice and security considerations in the documentation of + the :mod:`ssl` module to configure the TLS context securely. + +Connect from a browser +---------------------- + +The WebSocket protocol was invented for the web — as the name says! + +Here's how to connect to a WebSocket server from a browser. + +Run this script in a console: + +.. literalinclude:: ../../example/quickstart/show_time.py + :caption: show_time.py + :language: python + :linenos: + +Save this file as ``show_time.html``: + +.. literalinclude:: ../../example/quickstart/show_time.html + :caption: show_time.html + :language: html + :linenos: + +Save this file as ``show_time.js``: + +.. literalinclude:: ../../example/quickstart/show_time.js + :caption: show_time.js + :language: js + :linenos: + +Then, open ``show_time.html`` in several browsers. Clocks tick irregularly. + +Broadcast messages +------------------ + +Let's change the previous example to send the same timestamps to all browsers, +instead of generating independent sequences for each client. + +Stop the previous script if it's still running and run this script in a console: + +.. literalinclude:: ../../example/quickstart/show_time_2.py + :caption: show_time_2.py + :language: python + :linenos: + +Refresh ``show_time.html`` in all browsers. Clocks tick in sync. + +Manage application state +------------------------ + +A WebSocket server can receive events from clients, process them to update the +application state, and broadcast the updated state to all connected clients. + +Here's an example where any client can increment or decrement a counter. The +concurrency model of :mod:`asyncio` guarantees that updates are serialized. + +Run this script in a console: + +.. literalinclude:: ../../example/quickstart/counter.py + :caption: counter.py + :language: python + :linenos: + +Save this file as ``counter.html``: + +.. literalinclude:: ../../example/quickstart/counter.html + :caption: counter.html + :language: html + :linenos: + +Save this file as ``counter.css``: + +.. literalinclude:: ../../example/quickstart/counter.css + :caption: counter.css + :language: css + :linenos: + +Save this file as ``counter.js``: + +.. literalinclude:: ../../example/quickstart/counter.js + :caption: counter.js + :language: js + :linenos: + +Then open ``counter.html`` file in several browsers and play with [+] and [-]. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst new file mode 100644 index 0000000000..70bf8c376c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst @@ -0,0 +1,172 @@ +Deploy to Render +================ + +This guide describes how to deploy a websockets server to Render_. + +.. _Render: https://render.com/ + +.. admonition:: The free plan of Render is sufficient for trying this guide. + :class: tip + + However, on a `free plan`__, connections are dropped after five minutes, + which is quite short for WebSocket application. + + __ https://render.com/docs/free + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create repository +----------------- + +Deploying to Render requires a git repository. Let's initialize one: + +.. code-block:: console + + $ mkdir websockets-echo + $ cd websockets-echo + $ git init -b main + Initialized empty Git repository in websockets-echo/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) 816c3b1] Initial commit. + +Render requires the git repository to be hosted at GitHub or GitLab. + +Sign up or log in to GitHub. Create a new repository named ``websockets-echo``. +Don't enable any of the initialization options offered by GitHub. Then, follow +instructions for pushing an existing repository from the command line. + +After pushing, refresh your repository's homepage on GitHub. You should see an +empty repository with an empty initial commit. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/render/app.py + :language: python + +This app implements requirements for `zero downtime deploys`_: + +* it provides a health check at ``/healthz``; +* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal. + +.. _zero downtime deploys: https://render.com/docs/deploys#zero-downtime-deploys + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/render/requirements.txt + :language: text + +Confirm that you created the correct files and commit them to git: + +.. code-block:: console + + $ ls + app.py requirements.txt + $ git add . + $ git commit -m "Initial implementation." + [main f26bf7f] Initial implementation. + 2 files changed, 37 insertions(+) + create mode 100644 app.py + create mode 100644 requirements.txt + +Push the changes to GitHub: + +.. code-block:: console + + $ git push + ... + To github.com:<username>/websockets-echo.git + 816c3b1..f26bf7f main -> main + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Sign up or log in to Render. + +Create a new web service. Connect the git repository that you just created. + +Then, finalize the configuration of your app as follows: + +* **Name**: websockets-echo +* **Start Command**: ``python app.py`` + +If you're just experimenting, select the free plan. Create the web service. + +To configure the health check, go to Settings, scroll down to Health & Alerts, +and set: + +* **Health Check Path**: /healthz + +This triggers a new deployment. + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Render app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully when you deploy +a new version. Due to limitations of Render's free plan, you must upgrade to a +paid plan before you perform this test. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + > + +Trigger a new deployment with Manual Deploy > Deploy latest commit. When the +deployment completes, the connection is closed with code 1001 (going away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). + +Remember to downgrade to a free plan if you upgraded just for testing this feature. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst new file mode 100644 index 0000000000..d41519ff09 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst @@ -0,0 +1,322 @@ +Integrate the Sans-I/O layer +============================ + +.. currentmodule:: websockets + +This guide explains how to integrate the `Sans-I/O`_ layer of websockets to +add support for WebSocket in another library. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +As a prerequisite, you should decide how you will handle network I/O and +asynchronous control flow. + +Your integration layer will provide an API for the application on one side, +will talk to the network on the other side, and will rely on websockets to +implement the protocol in the middle. + +.. image:: ../topics/data-flow.svg + :align: center + +Opening a connection +-------------------- + +Client-side +........... + +If you're building a client, parse the URI you'd like to connect to:: + + from websockets.uri import parse_uri + + wsuri = parse_uri("ws://example.com/") + +Open a TCP connection to ``(wsuri.host, wsuri.port)`` and perform a TLS +handshake if ``wsuri.secure`` is :obj:`True`. + +Initialize a :class:`~client.ClientProtocol`:: + + from websockets.client import ClientProtocol + + protocol = ClientProtocol(wsuri) + +Create a WebSocket handshake request +with :meth:`~client.ClientProtocol.connect` and send it +with :meth:`~client.ClientProtocol.send_request`:: + + request = protocol.connect() + protocol.send_request(request) + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network, as described in `Send data`_ below. + +Once you receive enough data, as explained in `Receive data`_ below, the first +event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket +handshake response. + +When the handshake fails, the reason is available in +:attr:`~client.ClientProtocol.handshake_exc`:: + + if protocol.handshake_exc is not None: + raise protocol.handshake_exc + +Else, the WebSocket connection is open. + +A WebSocket client API usually performs the handshake then returns a wrapper +around the network socket and the :class:`~client.ClientProtocol`. + +Server-side +........... + +If you're building a server, accept network connections from clients and +perform a TLS handshake if desired. + +For each connection, initialize a :class:`~server.ServerProtocol`:: + + from websockets.server import ServerProtocol + + protocol = ServerProtocol() + +Once you receive enough data, as explained in `Receive data`_ below, the first +event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket +handshake request. + +Create a WebSocket handshake response +with :meth:`~server.ServerProtocol.accept` and send it +with :meth:`~server.ServerProtocol.send_response`:: + + response = protocol.accept(request) + protocol.send_response(response) + +Alternatively, you may reject the WebSocket handshake and return an HTTP +response with :meth:`~server.ServerProtocol.reject`:: + + response = protocol.reject(status, explanation) + protocol.send_response(response) + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network, as described in `Send data`_ below. + +Even when you call :meth:`~server.ServerProtocol.accept`, the WebSocket +handshake may fail if the request is incorrect or unsupported. + +When the handshake fails, the reason is available in +:attr:`~server.ServerProtocol.handshake_exc`:: + + if protocol.handshake_exc is not None: + raise protocol.handshake_exc + +Else, the WebSocket connection is open. + +A WebSocket server API usually builds a wrapper around the network socket and +the :class:`~server.ServerProtocol`. Then it invokes a connection handler that +accepts the wrapper in argument. + +It may also provide a way to close all connections and to shut down the server +gracefully. + +Going forwards, this guide focuses on handling an individual connection. + +From the network to the application +----------------------------------- + +Go through the five steps below until you reach the end of the data stream. + +Receive data +............ + +When receiving data from the network, feed it to the protocol's +:meth:`~protocol.Protocol.receive_data` method. + +When reaching the end of the data stream, call the protocol's +:meth:`~protocol.Protocol.receive_eof` method. + +For example, if ``sock`` is a :obj:`~socket.socket`:: + + try: + data = sock.recv(65536) + except OSError: # socket closed + data = b"" + if data: + protocol.receive_data(data) + else: + protocol.receive_eof() + +These methods aren't expected to raise exceptions — unless you call them again +after calling :meth:`~protocol.Protocol.receive_eof`, which is an error. +(If you get an exception, please file a bug!) + +Send data +......... + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network:: + + for data in protocol.data_to_send(): + if data: + sock.sendall(data) + else: + sock.shutdown(socket.SHUT_WR) + +The empty bytestring signals the end of the data stream. When you see it, you +must half-close the TCP connection. + +Sending data right after receiving data is necessary because websockets +responds to ping frames, close frames, and incorrect inputs automatically. + +Expect TCP connection to close +.............................. + +Closing a WebSocket connection normally involves a two-way WebSocket closing +handshake. Then, regardless of whether the closure is normal or abnormal, the +server starts the four-way TCP closing handshake. If the network fails at the +wrong point, you can end up waiting until the TCP timeout, which is very long. + +To prevent dangling TCP connections when you expect the end of the data stream +but you never reach it, call :meth:`~protocol.Protocol.close_expected` +and, if it returns :obj:`True`, schedule closing the TCP connection after a +short timeout:: + + # start a new execution thread to run this code + sleep(10) + sock.close() # does nothing if the socket is already closed + +If the connection is still open when the timeout elapses, closing the socket +makes the execution thread that reads from the socket reach the end of the +data stream, possibly with an exception. + +Close TCP connection +.................... + +If you called :meth:`~protocol.Protocol.receive_eof`, close the TCP +connection now. This is a clean closure because the receive buffer is empty. + +After :meth:`~protocol.Protocol.receive_eof` signals the end of the read +stream, :meth:`~protocol.Protocol.data_to_send` always signals the end of +the write stream, unless it already ended. So, at this point, the TCP +connection is already half-closed. The only reason for closing it now is to +release resources related to the socket. + +Now you can exit the loop relaying data from the network to the application. + +Receive events +.............. + +Finally, call :meth:`~protocol.Protocol.events_received` to obtain events +parsed from the data provided to :meth:`~protocol.Protocol.receive_data`:: + + events = connection.events_received() + +The first event will be the WebSocket opening handshake request or response. +See `Opening a connection`_ above for details. + +All later events are WebSocket frames. There are two types of frames: + +* Data frames contain messages transferred over the WebSocket connections. You + should provide them to the application. See `Fragmentation`_ below for + how to reassemble messages from frames. +* Control frames provide information about the connection's state. The main + use case is to expose an abstraction over ping and pong to the application. + Keep in mind that websockets responds to ping frames and close frames + automatically. Don't duplicate this functionality! + +From the application to the network +----------------------------------- + +The connection object provides one method for each type of WebSocket frame. + +For sending a data frame: + +* :meth:`~protocol.Protocol.send_continuation` +* :meth:`~protocol.Protocol.send_text` +* :meth:`~protocol.Protocol.send_binary` + +These methods raise :exc:`~exceptions.ProtocolError` if you don't set +the :attr:`FIN <websockets.frames.Frame.fin>` bit correctly in fragmented +messages. + +For sending a control frame: + +* :meth:`~protocol.Protocol.send_close` +* :meth:`~protocol.Protocol.send_ping` +* :meth:`~protocol.Protocol.send_pong` + +:meth:`~protocol.Protocol.send_close` initiates the closing handshake. +See `Closing a connection`_ below for details. + +If you encounter an unrecoverable error and you must fail the WebSocket +connection, call :meth:`~protocol.Protocol.fail`. + +After any of the above, call :meth:`~protocol.Protocol.data_to_send` and +send its output to the network, as shown in `Send data`_ above. + +If you called :meth:`~protocol.Protocol.send_close` +or :meth:`~protocol.Protocol.fail`, you expect the end of the data +stream. You should follow the process described in `Close TCP connection`_ +above in order to prevent dangling TCP connections. + +Closing a connection +-------------------- + +Under normal circumstances, when a server wants to close the TCP connection: + +* it closes the write side; +* it reads until the end of the stream, because it expects the client to close + the read side; +* it closes the socket. + +When a client wants to close the TCP connection: + +* it reads until the end of the stream, because it expects the server to close + the read side; +* it closes the write side; +* it closes the socket. + +Applying the rules described earlier in this document gives the intended +result. As a reminder, the rules are: + +* When :meth:`~protocol.Protocol.data_to_send` returns the empty + bytestring, close the write side of the TCP connection. +* When you reach the end of the read stream, close the TCP connection. +* When :meth:`~protocol.Protocol.close_expected` returns :obj:`True`, if + you don't reach the end of the read stream quickly, close the TCP connection. + +Fragmentation +------------- + +WebSocket messages may be fragmented. Since this is a protocol-level concern, +you may choose to reassemble fragmented messages before handing them over to +the application. + +To reassemble a message, read data frames until you get a frame where +the :attr:`FIN <websockets.frames.Frame.fin>` bit is set, then concatenate +the payloads of all frames. + +You will never receive an inconsistent sequence of frames because websockets +raises a :exc:`~exceptions.ProtocolError` and fails the connection when this +happens. However, you may receive an incomplete sequence if the connection +drops in the middle of a fragmented message. + +Tips +---- + +Serialize operations +.................... + +The Sans-I/O layer expects to run sequentially. If your interact with it from +multiple threads or coroutines, you must ensure correct serialization. This +should happen automatically in a cooperative multitasking environment. + +However, you still have to make sure you don't break this property by +accident. For example, serialize writes to the network +when :meth:`~protocol.Protocol.data_to_send` returns multiple values to +prevent concurrent writes from interleaving incorrectly. + +Avoid buffers +............. + +The Sans-I/O layer doesn't do any buffering. It makes events available in +:meth:`~protocol.Protocol.events_received` as soon as they're received. + +You should make incoming messages available to the application immediately and +stop further processing until the application fetches them. This will usually +result in the best performance. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst new file mode 100644 index 0000000000..5eefc7711b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst @@ -0,0 +1,131 @@ +Deploy with Supervisor +====================== + +This guide proposes a simple way to deploy a websockets server directly on a +Linux or BSD operating system. + +We'll configure Supervisor_ to run several server processes and to restart +them if needed. + +.. _Supervisor: http://supervisord.org/ + +We'll bind all servers to the same port. The OS will take care of balancing +connections. + +Create and activate a virtualenv: + +.. code-block:: console + + $ python -m venv supervisor-websockets + $ . supervisor-websockets/bin/activate + +Install websockets and Supervisor: + +.. code-block:: console + + $ pip install websockets + $ pip install supervisor + +Save this app to a file called ``app.py``: + +.. literalinclude:: ../../example/deployment/supervisor/app.py + +This is an echo server with two features added for the purpose of this guide: + +* It shuts down gracefully when receiving a ``SIGTERM`` signal; +* It enables the ``reuse_port`` option of :meth:`~asyncio.loop.create_server`, + which in turns sets ``SO_REUSEPORT`` on the accept socket. + +Save this Supervisor configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/supervisor/supervisord.conf + +This is the minimal configuration required to keep four instances of the app +running, restarting them if they exit. + +Now start Supervisor in the foreground: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + INFO Increased RLIMIT_NOFILE limit to 1024 + INFO supervisord started with pid 43596 + INFO spawned: 'websockets-test_00' with pid 43597 + INFO spawned: 'websockets-test_01' with pid 43598 + INFO spawned: 'websockets-test_02' with pid 43599 + INFO spawned: 'websockets-test_03' with pid 43600 + INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_01 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_02 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_03 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + +In another shell, after activating the virtualenv, we can connect to the app — +press Ctrl-D to exit: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). + +Look at the pid of an instance of the app in the logs and terminate it: + +.. code-block:: console + + $ kill -TERM 43597 + +The logs show that Supervisor restarted this instance: + +.. code-block:: console + + INFO exited: websockets-test_00 (exit status 0; expected) + INFO spawned: 'websockets-test_00' with pid 43629 + INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + +Now let's check what happens when we shut down Supervisor, but first let's +establish a connection and leave it open: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > + +Look at the pid of supervisord itself in the logs and terminate it: + +.. code-block:: console + + $ kill -TERM 43596 + +The logs show that Supervisor terminated all instances of the app before +exiting: + +.. code-block:: console + + WARN received SIGTERM indicating exit request + INFO waiting for websockets-test_00, websockets-test_01, websockets-test_02, websockets-test_03 to die + INFO stopped: websockets-test_02 (exit status 0) + INFO stopped: websockets-test_03 (exit status 0) + INFO stopped: websockets-test_01 (exit status 0) + INFO stopped: websockets-test_00 (exit status 0) + +And you can see that the connection to the app was closed gracefully: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + Connection closed: 1001 (going away). + +In this example, we've been sharing the same virtualenv for supervisor and +websockets. + +In a real deployment, you would likely: + +* Install Supervisor with the package manager of the OS. +* Create a virtualenv dedicated to your application. +* Add ``environment=PATH="path/to/your/virtualenv/bin"`` in the Supervisor + configuration. Then ``python app.py`` runs in that virtualenv. + diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst new file mode 100644 index 0000000000..d9737db12a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst @@ -0,0 +1,75 @@ +websockets +========== + +|licence| |version| |pyversions| |tests| |docs| |openssf| + +.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |version| image:: https://img.shields.io/pypi/v/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests + :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml + +.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg + :target: https://websockets.readthedocs.io/ + +.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge + :target: https://bestpractices.coreinfrastructure.org/projects/6475 + +websockets is a library for building WebSocket_ servers and clients in Python +with a focus on correctness, simplicity, robustness, and performance. + +.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API + +It supports several network I/O and control flow paradigms: + +1. The default implementation builds upon :mod:`asyncio`, Python's standard + asynchronous I/O framework. It provides an elegant coroutine-based API. It's + ideal for servers that handle many clients concurrently. +2. The :mod:`threading` implementation is a good alternative for clients, + especially if you aren't familiar with :mod:`asyncio`. It may also be used + for servers that don't need to serve many clients. +3. The `Sans-I/O`_ implementation is designed for integrating in third-party + libraries, typically application servers, in addition being used internally + by websockets. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +Here's an echo server with the :mod:`asyncio` API: + +.. literalinclude:: ../example/echo.py + +Here's how a client sends and receives messages with the :mod:`threading` API: + +.. literalinclude:: ../example/hello.py + +Don't worry about the opening and closing handshakes, pings and pongs, or any +other behavior described in the WebSocket specification. websockets takes care +of this under the hood so you can focus on your application! + +Also, websockets provides an interactive client: + +.. code-block:: console + + $ python -m websockets ws://localhost:8765/ + Connected to ws://localhost:8765/. + > Hello world! + < Hello world! + Connection closed: 1000 (OK). + +Do you like it? :doc:`Let's dive in! <intro/index>` + +.. toctree:: + :hidden: + + intro/index + howto/index + faq/index + reference/index + topics/index + project/index diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst new file mode 100644 index 0000000000..095262a207 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst @@ -0,0 +1,46 @@ +Getting started +=============== + +.. currentmodule:: websockets + +Requirements +------------ + +websockets requires Python ≥ 3.8. + +.. admonition:: Use the most recent Python release + :class: tip + + For each minor version (3.x), only the latest bugfix or security release + (3.x.y) is officially supported. + +It doesn't have any dependencies. + +.. _install: + +Installation +------------ + +Install websockets with: + +.. code-block:: console + + $ pip install websockets + +Wheels are available for all platforms. + +Tutorial +-------- + +Learn how to build an real-time web application with websockets. + +.. toctree:: + + tutorial1 + tutorial2 + tutorial3 + +In a hurry? +----------- + +Look at the :doc:`quick start guide <../howto/quickstart>`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst new file mode 100644 index 0000000000..ff85003b58 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst @@ -0,0 +1,591 @@ +Part 1 - Send & receive +======================= + +.. currentmodule:: websockets + +In this tutorial, you're going to build a web-based `Connect Four`_ game. + +.. _Connect Four: https://en.wikipedia.org/wiki/Connect_Four + +The web removes the constraint of being in the same room for playing a game. +Two players can connect over of the Internet, regardless of where they are, +and play in their browsers. + +When a player makes a move, it should be reflected immediately on both sides. +This is difficult to implement over HTTP due to the request-response style of +the protocol. + +Indeed, there is no good way to be notified when the other player makes a +move. Workarounds such as polling or long-polling introduce significant +overhead. + +Enter `WebSocket <websocket>`_. + +The WebSocket protocol provides two-way communication between a browser and a +server over a persistent connection. That's exactly what you need to exchange +moves between players, via a server. + +.. admonition:: This is the first part of the tutorial. + + * In this :doc:`first part <tutorial1>`, you will create a server and + connect one browser; you can play if you share the same browser. + * In the :doc:`second part <tutorial2>`, you will connect a second + browser; you can play from different browsers on a local network. + * In the :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +Prerequisites +------------- + +This tutorial assumes basic knowledge of Python and JavaScript. + +If you're comfortable with :doc:`virtual environments <python:tutorial/venv>`, +you can use one for this tutorial. Else, don't worry: websockets doesn't have +any dependencies; it shouldn't create trouble in the default environment. + +If you haven't installed websockets yet, do it now: + +.. code-block:: console + + $ pip install websockets + +Confirm that websockets is installed: + +.. code-block:: console + + $ python -m websockets --version + +.. admonition:: This tutorial is written for websockets |release|. + :class: tip + + If you installed another version, you should switch to the corresponding + version of the documentation. + +Download the starter kit +------------------------ + +Create a directory and download these three files: +:download:`connect4.js <../../example/tutorial/start/connect4.js>`, +:download:`connect4.css <../../example/tutorial/start/connect4.css>`, +and :download:`connect4.py <../../example/tutorial/start/connect4.py>`. + +The JavaScript module, along with the CSS file, provides a web-based user +interface. Here's its API. + +.. js:module:: connect4 + +.. js:data:: PLAYER1 + + Color of the first player. + +.. js:data:: PLAYER2 + + Color of the second player. + +.. js:function:: createBoard(board) + + Draw a board. + + :param board: DOM element containing the board; must be initially empty. + +.. js:function:: playMove(board, player, column, row) + + Play a move. + + :param board: DOM element containing the board. + :param player: :js:data:`PLAYER1` or :js:data:`PLAYER2`. + :param column: between ``0`` and ``6``. + :param row: between ``0`` and ``5``. + +The Python module provides a class to record moves and tell when a player +wins. Here's its API. + +.. module:: connect4 + +.. data:: PLAYER1 + :value: "red" + + Color of the first player. + +.. data:: PLAYER2 + :value: "yellow" + + Color of the second player. + +.. class:: Connect4 + + A Connect Four game. + + .. method:: play(player, column) + + Play a move. + + :param player: :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2`. + :param column: between ``0`` and ``6``. + :returns: Row where the checker lands, between ``0`` and ``5``. + :raises RuntimeError: if the move is illegal. + + .. attribute:: moves + + List of moves played during this game, as ``(player, column, row)`` + tuples. + + .. attribute:: winner + + :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2` if they + won; :obj:`None` if the game is still ongoing. + +.. currentmodule:: websockets + +Bootstrap the web UI +-------------------- + +Create an ``index.html`` file next to ``connect4.js`` and ``connect4.css`` +with this content: + +.. literalinclude:: ../../example/tutorial/step1/index.html + :language: html + +This HTML page contains an empty ``<div>`` element where you will draw the +Connect Four board. It loads a ``main.js`` script where you will write all +your JavaScript code. + +Create a ``main.js`` file next to ``index.html``. In this script, when the +page loads, draw the board: + +.. code-block:: javascript + + import { createBoard, playMove } from "./connect4.js"; + + window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + }); + +Open a shell, navigate to the directory containing these files, and start an +HTTP server: + +.. code-block:: console + + $ python -m http.server + +Open http://localhost:8000/ in a web browser. The page displays an empty board +with seven columns and six rows. You will play moves in this board later. + +Bootstrap the server +-------------------- + +Create an ``app.py`` file next to ``connect4.py`` with this content: + +.. code-block:: python + + #!/usr/bin/env python + + import asyncio + + import websockets + + + async def handler(websocket): + while True: + message = await websocket.recv() + print(message) + + + async def main(): + async with websockets.serve(handler, "", 8001): + await asyncio.Future() # run forever + + + if __name__ == "__main__": + asyncio.run(main()) + +The entry point of this program is ``asyncio.run(main())``. It creates an +asyncio event loop, runs the ``main()`` coroutine, and shuts down the loop. + +The ``main()`` coroutine calls :func:`~server.serve` to start a websockets +server. :func:`~server.serve` takes three positional arguments: + +* ``handler`` is a coroutine that manages a connection. When a client + connects, websockets calls ``handler`` with the connection in argument. + When ``handler`` terminates, websockets closes the connection. +* The second argument defines the network interfaces where the server can be + reached. Here, the server listens on all interfaces, so that other devices + on the same local network can connect. +* The third argument is the port on which the server listens. + +Invoking :func:`~server.serve` as an asynchronous context manager, in an +``async with`` block, ensures that the server shuts down properly when +terminating the program. + +For each connection, the ``handler()`` coroutine runs an infinite loop that +receives messages from the browser and prints them. + +Open a shell, navigate to the directory containing ``app.py``, and start the +server: + +.. code-block:: console + + $ python app.py + +This doesn't display anything. Hopefully the WebSocket server is running. +Let's make sure that it works. You cannot test the WebSocket server with a +web browser like you tested the HTTP server. However, you can test it with +websockets' interactive client. + +Open another shell and run this command: + +.. code-block:: console + + $ python -m websockets ws://localhost:8001/ + +You get a prompt. Type a message and press "Enter". Switch to the shell where +the server is running and check that the server received the message. Good! + +Exit the interactive client with Ctrl-C or Ctrl-D. + +Now, if you look at the console where you started the server, you can see the +stack trace of an exception: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + File "app.py", line 22, in handler + message = await websocket.recv() + ... + websockets.exceptions.ConnectionClosedOK: received 1000 (OK); then sent 1000 (OK) + +Indeed, the server was waiting for the next message +with :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` when the client +disconnected. When this happens, websockets raises +a :exc:`~exceptions.ConnectionClosedOK` exception to let you know that you +won't receive another message on this connection. + +This exception creates noise in the server logs, making it more difficult to +spot real errors when you add functionality to the server. Catch it in the +``handler()`` coroutine: + +.. code-block:: python + + async def handler(websocket): + while True: + try: + message = await websocket.recv() + except websockets.ConnectionClosedOK: + break + print(message) + +Stop the server with Ctrl-C and start it again: + +.. code-block:: console + + $ python app.py + +.. admonition:: You must restart the WebSocket server when you make changes. + :class: tip + + The WebSocket server loads the Python code in ``app.py`` then serves every + WebSocket request with this version of the code. As a consequence, + changes to ``app.py`` aren't visible until you restart the server. + + This is unlike the HTTP server that you started earlier with ``python -m + http.server``. For every request, this HTTP server reads the target file + and sends it. That's why changes are immediately visible. + + It is possible to :doc:`restart the WebSocket server automatically + <../howto/autoreload>` but this isn't necessary for this tutorial. + +Try connecting and disconnecting the interactive client again. +The :exc:`~exceptions.ConnectionClosedOK` exception doesn't appear anymore. + +This pattern is so common that websockets provides a shortcut for iterating +over messages received on the connection until the client disconnects: + +.. code-block:: python + + async def handler(websocket): + async for message in websocket: + print(message) + +Restart the server and check with the interactive client that its behavior +didn't change. + +At this point, you bootstrapped a web application and a WebSocket server. +Let's connect them. + +Transmit from browser to server +------------------------------- + +In JavaScript, you open a WebSocket connection as follows: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://localhost:8001/"); + +Before you exchange messages with the server, you need to decide their format. +There is no universal convention for this. + +Let's use JSON objects with a ``type`` key identifying the type of the event +and the rest of the object containing properties of the event. + +Here's an event describing a move in the middle slot of the board: + +.. code-block:: javascript + + const event = {type: "play", column: 3}; + +Here's how to serialize this event to JSON and send it to the server: + +.. code-block:: javascript + + websocket.send(JSON.stringify(event)); + +Now you have all the building blocks to send moves to the server. + +Add this function to ``main.js``: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: function sendMoves + :end-before: window.addEventListener + +``sendMoves()`` registers a listener for ``click`` events on the board. The +listener figures out which column was clicked, builds a event of type +``"play"``, serializes it, and sends it to the server. + +Modify the initialization to open the WebSocket connection and call the +``sendMoves()`` function: + +.. code-block:: javascript + + window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket("ws://localhost:8001/"); + sendMoves(board, websocket); + }); + +Check that the HTTP server and the WebSocket server are still running. If you +stopped them, here are the commands to start them again: + +.. code-block:: console + + $ python -m http.server + +.. code-block:: console + + $ python app.py + +Refresh http://localhost:8000/ in your web browser. Click various columns in +the board. The server receives messages with the expected column number. + +There isn't any feedback in the board because you haven't implemented that +yet. Let's do it. + +Transmit from server to browser +------------------------------- + +In JavaScript, you receive WebSocket messages by listening to ``message`` +events. Here's how to receive a message from the server and deserialize it +from JSON: + +.. code-block:: javascript + + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + // do something with event + }); + +You're going to need three types of messages from the server to the browser: + +.. code-block:: javascript + + {type: "play", player: "red", column: 3, row: 0} + {type: "win", player: "red"} + {type: "error", message: "This slot is full."} + +The JavaScript code receiving these messages will dispatch events depending on +their type and take appropriate action. For example, it will react to an +event of type ``"play"`` by displaying the move on the board with +the :js:func:`~connect4.playMove` function. + +Add this function to ``main.js``: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: function showMessage + :end-before: function sendMoves + +.. admonition:: Why does ``showMessage`` use ``window.setTimeout``? + :class: hint + + When :js:func:`playMove` modifies the state of the board, the browser + renders changes asynchronously. Conversely, ``window.alert()`` runs + synchronously and blocks rendering while the alert is visible. + + If you called ``window.alert()`` immediately after :js:func:`playMove`, + the browser could display the alert before rendering the move. You could + get a "Player red wins!" alert without seeing red's last move. + + We're using ``window.alert()`` for simplicity in this tutorial. A real + application would display these messages in the user interface instead. + It wouldn't be vulnerable to this problem. + +Modify the initialization to call the ``receiveMoves()`` function: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: window.addEventListener + +At this point, the user interface should receive events properly. Let's test +it by modifying the server to send some events. + +Sending an event from Python is quite similar to JavaScript: + +.. code-block:: python + + event = {"type": "play", "player": "red", "column": 3, "row": 0} + await websocket.send(json.dumps(event)) + +.. admonition:: Don't forget to serialize the event with :func:`json.dumps`. + :class: tip + + Else, websockets raises ``TypeError: data is a dict-like object``. + +Modify the ``handler()`` coroutine in ``app.py`` as follows: + +.. code-block:: python + + import json + + from connect4 import PLAYER1, PLAYER2 + + async def handler(websocket): + for player, column, row in [ + (PLAYER1, 3, 0), + (PLAYER2, 3, 1), + (PLAYER1, 4, 0), + (PLAYER2, 4, 1), + (PLAYER1, 2, 0), + (PLAYER2, 1, 0), + (PLAYER1, 5, 0), + ]: + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + await asyncio.sleep(0.5) + event = { + "type": "win", + "player": PLAYER1, + } + await websocket.send(json.dumps(event)) + +Restart the WebSocket server and refresh http://localhost:8000/ in your web +browser. Seven moves appear at 0.5 second intervals. Then an alert announces +the winner. + +Good! Now you know how to communicate both ways. + +Once you plug the game engine to process moves, you will have a fully +functional game. + +Add the game logic +------------------ + +In the ``handler()`` coroutine, you're going to initialize a game: + +.. code-block:: python + + from connect4 import Connect4 + + async def handler(websocket): + # Initialize a Connect Four game. + game = Connect4() + + ... + +Then, you're going to iterate over incoming messages and take these steps: + +* parse an event of type ``"play"``, the only type of event that the user + interface sends; +* play the move in the board with the :meth:`~connect4.Connect4.play` method, + alternating between the two players; +* if :meth:`~connect4.Connect4.play` raises :exc:`RuntimeError` because the + move is illegal, send an event of type ``"error"``; +* else, send an event of type ``"play"`` to tell the user interface where the + checker lands; +* if the move won the game, send an event of type ``"win"``. + +Try to implement this by yourself! + +Keep in mind that you must restart the WebSocket server and reload the page in +the browser when you make changes. + +When it works, you can play the game from a single browser, with players +taking alternate turns. + +.. admonition:: Enable debug logs to see all messages sent and received. + :class: tip + + Here's how to enable debug logs: + + .. code-block:: python + + import logging + + logging.basicConfig(format="%(message)s", level=logging.DEBUG) + +If you're stuck, a solution is available at the bottom of this document. + +Summary +------- + +In this first part of the tutorial, you learned how to: + +* build and run a WebSocket server in Python with :func:`~server.serve`; +* receive a message in a connection handler + with :meth:`~server.WebSocketServerProtocol.recv`; +* send a message in a connection handler + with :meth:`~server.WebSocketServerProtocol.send`; +* iterate over incoming messages with ``async for + message in websocket: ...``; +* open a WebSocket connection in JavaScript with the ``WebSocket`` API; +* send messages in a browser with ``WebSocket.send()``; +* receive messages in a browser by listening to ``message`` events; +* design a set of events to be exchanged between the browser and the server. + +You can now play a Connect Four game in a browser, communicating over a +WebSocket connection with a server where the game logic resides! + +However, the two players share a browser, so the constraint of being in the +same room still applies. + +Move on to the :doc:`second part <tutorial2>` of the tutorial to break this +constraint and play from separate browsers. + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step1/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step1/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :caption: main.js + :language: js + :linenos: diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst new file mode 100644 index 0000000000..5ac4ae9dd5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst @@ -0,0 +1,565 @@ +Part 2 - Route & broadcast +========================== + +.. currentmodule:: websockets + +.. admonition:: This is the second part of the tutorial. + + * In the :doc:`first part <tutorial1>`, you created a server and + connected one browser; you could play if you shared the same browser. + * In this :doc:`second part <tutorial2>`, you will connect a second + browser; you can play from different browsers on a local network. + * In the :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +In the first part of the tutorial, you opened a WebSocket connection from a +browser to a server and exchanged events to play moves. The state of the game +was stored in an instance of the :class:`~connect4.Connect4` class, +referenced as a local variable in the connection handler coroutine. + +Now you want to open two WebSocket connections from two separate browsers, one +for each player, to the same server in order to play the same game. This +requires moving the state of the game to a place where both connections can +access it. + +Share game state +---------------- + +As long as you're running a single server process, you can share state by +storing it in a global variable. + +.. admonition:: What if you need to scale to multiple server processes? + :class: hint + + In that case, you must design a way for the process that handles a given + connection to be aware of relevant events for that client. This is often + achieved with a publish / subscribe mechanism. + +How can you make two connection handlers agree on which game they're playing? +When the first player starts a game, you give it an identifier. Then, you +communicate the identifier to the second player. When the second player joins +the game, you look it up with the identifier. + +In addition to the game itself, you need to keep track of the WebSocket +connections of the two players. Since both players receive the same events, +you don't need to treat the two connections differently; you can store both +in the same set. + +Let's sketch this in code. + +A module-level :class:`dict` enables lookups by identifier: + +.. code-block:: python + + JOIN = {} + +When the first player starts the game, initialize and store it: + +.. code-block:: python + + import secrets + + async def handler(websocket): + ... + + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access token. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + try: + + ... + + finally: + del JOIN[join_key] + +When the second player joins the game, look it up: + +.. code-block:: python + + async def handler(websocket): + ... + + join_key = ... # TODO + + # Find the Connect Four game. + game, connected = JOIN[join_key] + + # Register to receive moves from this game. + connected.add(websocket) + try: + + ... + + finally: + connected.remove(websocket) + +Notice how we're carefully cleaning up global state with ``try: ... +finally: ...`` blocks. Else, we could leave references to games or +connections in global state, which would cause a memory leak. + +In both connection handlers, you have a ``game`` pointing to the same +:class:`~connect4.Connect4` instance, so you can interact with the game, +and a ``connected`` set of connections, so you can send game events to +both players as follows: + +.. code-block:: python + + async def handler(websocket): + + ... + + for connection in connected: + await connection.send(json.dumps(event)) + + ... + +Perhaps you spotted a major piece missing from the puzzle. How does the second +player obtain ``join_key``? Let's design new events to carry this information. + +To start a game, the first player sends an ``"init"`` event: + +.. code-block:: javascript + + {type: "init"} + +The connection handler for the first player creates a game as shown above and +responds with: + +.. code-block:: javascript + + {type: "init", join: "<join_key>"} + +With this information, the user interface of the first player can create a +link to ``http://localhost:8000/?join=<join_key>``. For the sake of simplicity, +we will assume that the first player shares this link with the second player +outside of the application, for example via an instant messaging service. + +To join the game, the second player sends a different ``"init"`` event: + +.. code-block:: javascript + + {type: "init", join: "<join_key>"} + +The connection handler for the second player can look up the game with the +join key as shown above. There is no need to respond. + +Let's dive into the details of implementing this design. + +Start a game +------------ + +We'll start with the initialization sequence for the first player. + +In ``main.js``, define a function to send an initialization event when the +WebSocket connection is established, which triggers an ``open`` event: + +.. code-block:: javascript + + function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event for the first player. + const event = { type: "init" }; + websocket.send(JSON.stringify(event)); + }); + } + +Update the initialization sequence to call ``initGame()``: + +.. literalinclude:: ../../example/tutorial/step2/main.js + :language: js + :start-at: window.addEventListener + +In ``app.py``, define a new ``handler`` coroutine — keep a copy of the +previous one to reuse it later: + +.. code-block:: python + + import secrets + + + JOIN = {} + + + async def start(websocket): + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access token. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + try: + # Send the secret access token to the browser of the first player, + # where it'll be used for building a "join" link. + event = { + "type": "init", + "join": join_key, + } + await websocket.send(json.dumps(event)) + + # Temporary - for testing. + print("first player started game", id(game)) + async for message in websocket: + print("first player sent", message) + + finally: + del JOIN[join_key] + + + async def handler(websocket): + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + # First player starts a new game. + await start(websocket) + +In ``index.html``, add an ``<a>`` element to display the link to share with +the other player. + +.. code-block:: html + + <body> + <div class="actions"> + <a class="action join" href="">Join</a> + </div> + <!-- ... --> + </body> + +In ``main.js``, modify ``receiveMoves()`` to handle the ``"init"`` message and +set the target of that link: + +.. code-block:: javascript + + switch (event.type) { + case "init": + // Create link for inviting the second player. + document.querySelector(".join").href = "?join=" + event.join; + break; + // ... + } + +Restart the WebSocket server and reload http://localhost:8000/ in the browser. +There's a link labeled JOIN below the board with a target that looks like +http://localhost:8000/?join=95ftAaU5DJVP1zvb. + +The server logs say ``first player started game ...``. If you click the board, +you see ``"play"`` events. There is no feedback in the UI, though, because +you haven't restored the game logic yet. + +Before we get there, let's handle links with a ``join`` query parameter. + +Join a game +----------- + +We'll now update the initialization sequence to account for the second +player. + +In ``main.js``, update ``initGame()`` to send the join key in the ``"init"`` +message when it's in the URL: + +.. code-block:: javascript + + function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event according to who is connecting. + const params = new URLSearchParams(window.location.search); + let event = { type: "init" }; + if (params.has("join")) { + // Second player joins an existing game. + event.join = params.get("join"); + } else { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); + } + +In ``app.py``, update the ``handler`` coroutine to look for the join key in +the ``"init"`` message, then load that game: + +.. code-block:: python + + async def error(websocket, message): + event = { + "type": "error", + "message": message, + } + await websocket.send(json.dumps(event)) + + + async def join(websocket, join_key): + # Find the Connect Four game. + try: + game, connected = JOIN[join_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + + # Temporary - for testing. + print("second player joined game", id(game)) + async for message in websocket: + print("second player sent", message) + + finally: + connected.remove(websocket) + + + async def handler(websocket): + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + if "join" in event: + # Second player joins an existing game. + await join(websocket, event["join"]) + else: + # First player starts a new game. + await start(websocket) + +Restart the WebSocket server and reload http://localhost:8000/ in the browser. + +Copy the link labeled JOIN and open it in another browser. You may also open +it in another tab or another window of the same browser; however, that makes +it a bit tricky to remember which one is the first or second player. + +.. admonition:: You must start a new game when you restart the server. + :class: tip + + Since games are stored in the memory of the Python process, they're lost + when you stop the server. + + Whenever you make changes to ``app.py``, you must restart the server, + create a new game in a browser, and join it in another browser. + +The server logs say ``first player started game ...`` and ``second player +joined game ...``. The numbers match, proving that the ``game`` local +variable in both connection handlers points to same object in the memory of +the Python process. + +Click the board in either browser. The server receives ``"play"`` events from +the corresponding player. + +In the initialization sequence, you're routing connections to ``start()`` or +``join()`` depending on the first message received by the server. This is a +common pattern in servers that handle different clients. + +.. admonition:: Why not use different URIs for ``start()`` and ``join()``? + :class: hint + + Instead of sending an initialization event, you could encode the join key + in the WebSocket URI e.g. ``ws://localhost:8001/join/<join_key>``. The + WebSocket server would parse ``websocket.path`` and route the connection, + similar to how HTTP servers route requests. + + When you need to send sensitive data like authentication credentials to + the server, sending it an event is considered more secure than encoding + it in the URI because URIs end up in logs. + + For the purposes of this tutorial, both approaches are equivalent because + the join key comes from an HTTP URL. There isn't much at risk anyway! + +Now you can restore the logic for playing moves and you'll have a fully +functional two-player game. + +Add the game logic +------------------ + +Once the initialization is done, the game is symmetrical, so you can write a +single coroutine to process the moves of both players: + +.. code-block:: python + + async def play(websocket, game, player, connected): + ... + +With such a coroutine, you can replace the temporary code for testing in +``start()`` by: + +.. code-block:: python + + await play(websocket, game, PLAYER1, connected) + +and in ``join()`` by: + +.. code-block:: python + + await play(websocket, game, PLAYER2, connected) + +The ``play()`` coroutine will reuse much of the code you wrote in the first +part of the tutorial. + +Try to implement this by yourself! + +Keep in mind that you must restart the WebSocket server, reload the page to +start a new game with the first player, copy the JOIN link, and join the game +with the second player when you make changes. + +When ``play()`` works, you can play the game from two separate browsers, +possibly running on separate computers on the same local network. + +A complete solution is available at the bottom of this document. + +Watch a game +------------ + +Let's add one more feature: allow spectators to watch the game. + +The process for inviting a spectator can be the same as for inviting the +second player. You will have to duplicate all the initialization logic: + +- declare a ``WATCH`` global variable similar to ``JOIN``; +- generate a watch key when creating a game; it must be different from the + join key, or else a spectator could hijack a game by tweaking the URL; +- include the watch key in the ``"init"`` event sent to the first player; +- generate a WATCH link in the UI with a ``watch`` query parameter; +- update the ``initGame()`` function to handle such links; +- update the ``handler()`` coroutine to invoke a ``watch()`` coroutine for + spectators; +- prevent ``sendMoves()`` from sending ``"play"`` events for spectators. + +Once the initialization sequence is done, watching a game is as simple as +registering the WebSocket connection in the ``connected`` set in order to +receive game events and doing nothing until the spectator disconnects. You +can wait for a connection to terminate with +:meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`: + +.. code-block:: python + + async def watch(websocket, watch_key): + + ... + + connected.add(websocket) + try: + await websocket.wait_closed() + finally: + connected.remove(websocket) + +The connection can terminate because the ``receiveMoves()`` function closed it +explicitly after receiving a ``"win"`` event, because the spectator closed +their browser, or because the network failed. + +Again, try to implement this by yourself. + +When ``watch()`` works, you can invite spectators to watch the game from other +browsers, as long as they're on the same local network. + +As a further improvement, you may support adding spectators while a game is +already in progress. This requires replaying moves that were played before +the spectator was added to the ``connected`` set. Past moves are available in +the :attr:`~connect4.Connect4.moves` attribute of the game. + +This feature is included in the solution proposed below. + +Broadcast +--------- + +When you need to send a message to the two players and to all spectators, +you're using this pattern: + +.. code-block:: python + + async def handler(websocket): + + ... + + for connection in connected: + await connection.send(json.dumps(event)) + + ... + +Since this is a very common pattern in WebSocket servers, websockets provides +the :func:`broadcast` helper for this purpose: + +.. code-block:: python + + async def handler(websocket): + + ... + + websockets.broadcast(connected, json.dumps(event)) + + ... + +Calling :func:`broadcast` once is more efficient than +calling :meth:`~legacy.protocol.WebSocketCommonProtocol.send` in a loop. + +However, there's a subtle difference in behavior. Did you notice that there's +no ``await`` in the second version? Indeed, :func:`broadcast` is a function, +not a coroutine like :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +or :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`. + +It's quite obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` +is a coroutine. When you want to receive the next message, you have to wait +until the client sends it and the network transmits it. + +It's less obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.send` is +a coroutine. If you send many messages or large messages, you could write +data faster than the network can transmit it or the client can read it. Then, +outgoing data will pile up in buffers, which will consume memory and may +crash your application. + +To avoid this problem, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +waits until the write buffer drains. By slowing down the application as +necessary, this ensures that the server doesn't send data too quickly. This +is called backpressure and it's useful for building robust systems. + +That said, when you're sending the same messages to many clients in a loop, +applying backpressure in this way can become counterproductive. When you're +broadcasting, you don't want to slow down everyone to the pace of the slowest +clients; you want to drop clients that cannot keep up with the data stream. +That's why :func:`broadcast` doesn't wait until write buffers drain. + +For our Connect Four game, there's no difference in practice: the total amount +of data sent on a connection for a game of Connect Four is less than 64 KB, +so the write buffer never fills up and backpressure never kicks in anyway. + +Summary +------- + +In this second part of the tutorial, you learned how to: + +* configure a connection by exchanging initialization messages; +* keep track of connections within a single server process; +* wait until a client disconnects in a connection handler; +* broadcast a message to many connections efficiently. + +You can now play a Connect Four game from separate browser, communicating over +WebSocket connections with a server that synchronizes the game logic! + +However, the two players have to be on the same local network as the server, +so the constraint of being in the same place still mostly applies. + +Head over to the :doc:`third part <tutorial3>` of the tutorial to deploy the +game to the web and remove this constraint. + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step2/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step2/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step2/main.js + :caption: main.js + :language: js + :linenos: diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst new file mode 100644 index 0000000000..6fdec113b2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst @@ -0,0 +1,290 @@ +Part 3 - Deploy to the web +========================== + +.. currentmodule:: websockets + +.. admonition:: This is the third part of the tutorial. + + * In the :doc:`first part <tutorial1>`, you created a server and + connected one browser; you could play if you shared the same browser. + * In this :doc:`second part <tutorial2>`, you connected a second browser; + you could play from different browsers on a local network. + * In this :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +In the first and second parts of the tutorial, for local development, you ran +an HTTP server on ``http://localhost:8000/`` with: + +.. code-block:: console + + $ python -m http.server + +and a WebSocket server on ``ws://localhost:8001/`` with: + +.. code-block:: console + + $ python app.py + +Now you want to deploy these servers on the Internet. There's a vast range of +hosting providers to choose from. For the sake of simplicity, we'll rely on: + +* GitHub Pages for the HTTP server; +* Heroku for the WebSocket server. + +Commit project to git +--------------------- + +Perhaps you committed your work to git while you were progressing through the +tutorial. If you didn't, now is a good time, because GitHub and Heroku offer +git-based deployment workflows. + +Initialize a git repository: + +.. code-block:: console + + $ git init -b main + Initialized empty Git repository in websockets-tutorial/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) ...] Initial commit. + +Add all files and commit: + +.. code-block:: console + + $ git add . + $ git commit -m "Initial implementation of Connect Four game." + [main ...] Initial implementation of Connect Four game. + 6 files changed, 500 insertions(+) + create mode 100644 app.py + create mode 100644 connect4.css + create mode 100644 connect4.js + create mode 100644 connect4.py + create mode 100644 index.html + create mode 100644 main.js + +Prepare the WebSocket server +---------------------------- + +Before you deploy the server, you must adapt it to meet requirements of +Heroku's runtime. This involves two small changes: + +1. Heroku expects the server to `listen on a specific port`_, provided in the + ``$PORT`` environment variable. + +2. Heroku sends a ``SIGTERM`` signal when `shutting down a dyno`_, which + should trigger a clean exit. + +.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port + +.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown + +Adapt the ``main()`` coroutine accordingly: + +.. code-block:: python + + import os + import signal + +.. literalinclude:: ../../example/tutorial/step3/app.py + :pyobject: main + +To catch the ``SIGTERM`` signal, ``main()`` creates a :class:`~asyncio.Future` +called ``stop`` and registers a signal handler that sets the result of this +future. The value of the future doesn't matter; it's only for waiting for +``SIGTERM``. + +Then, by using :func:`~server.serve` as a context manager and exiting the +context when ``stop`` has a result, ``main()`` ensures that the server closes +connections cleanly and exits on ``SIGTERM``. + +The app is now fully compatible with Heroku. + +Deploy the WebSocket server +--------------------------- + +Create a ``requirements.txt`` file with this content to install ``websockets`` +when building the image: + +.. literalinclude:: ../../example/tutorial/step3/requirements.txt + :language: text + +.. admonition:: Heroku treats ``requirements.txt`` as a signal to `detect a Python app`_. + :class: tip + + That's why you don't need to declare that you need a Python runtime. + +.. _detect a Python app: https://devcenter.heroku.com/articles/python-support#recognizing-a-python-app + +Create a ``Procfile`` file with this content to configure the command for +running the server: + +.. literalinclude:: ../../example/tutorial/step3/Procfile + :language: text + +Commit your changes: + +.. code-block:: console + + $ git add . + $ git commit -m "Deploy to Heroku." + [main ...] Deploy to Heroku. + 3 files changed, 12 insertions(+), 2 deletions(-) + create mode 100644 Procfile + create mode 100644 requirements.txt + +Follow the `set-up instructions`_ to install the Heroku CLI and to log in, if +you haven't done that yet. + +.. _set-up instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up + +Create a Heroku app. You must choose a unique name and replace +``websockets-tutorial`` by this name in the following command: + +.. code-block:: console + + $ heroku create websockets-tutorial + Creating ⬢ websockets-tutorial... done + https://websockets-tutorial.herokuapp.com/ | https://git.heroku.com/websockets-tutorial.git + +If you reuse a name that someone else already uses, you will receive this +error; if this happens, try another name: + +.. code-block:: console + + $ heroku create websockets-tutorial + Creating ⬢ websockets-tutorial... ! + ▸ Name websockets-tutorial is already taken + +Deploy by pushing the code to Heroku: + +.. code-block:: console + + $ git push heroku + + ... lots of output... + + remote: Released v1 + remote: https://websockets-tutorial.herokuapp.com/ deployed to Heroku + remote: + remote: Verifying deploy... done. + To https://git.heroku.com/websockets-tutorial.git + * [new branch] main -> main + +You can test the WebSocket server with the interactive client exactly like you +did in the first part of the tutorial. Replace ``websockets-tutorial`` by the +name of your app in the following command: + +.. code-block:: console + + $ python -m websockets wss://websockets-tutorial.herokuapp.com/ + Connected to wss://websockets-tutorial.herokuapp.com/. + > {"type": "init"} + < {"type": "init", "join": "54ICxFae_Ip7TJE2", "watch": "634w44TblL5Dbd9a"} + Connection closed: 1000 (OK). + +It works! + +Prepare the web application +--------------------------- + +Before you deploy the web application, perhaps you're wondering how it will +locate the WebSocket server? Indeed, at this point, its address is hard-coded +in ``main.js``: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://localhost:8001/"); + +You can take this strategy one step further by checking the address of the +HTTP server and determining the address of the WebSocket server accordingly. + +Add this function to ``main.js``; replace ``python-websockets`` by your GitHub +username and ``websockets-tutorial`` by the name of your app on Heroku: + +.. literalinclude:: ../../example/tutorial/step3/main.js + :language: js + :start-at: function getWebSocketServer + :end-before: function initGame + +Then, update the initialization to connect to this address instead: + +.. code-block:: javascript + + const websocket = new WebSocket(getWebSocketServer()); + +Commit your changes: + +.. code-block:: console + + $ git add . + $ git commit -m "Configure WebSocket server address." + [main ...] Configure WebSocket server address. + 1 file changed, 11 insertions(+), 1 deletion(-) + +Deploy the web application +-------------------------- + +Go to GitHub and create a new repository called ``websockets-tutorial``. + +Push your code to this repository. You must replace ``python-websockets`` by +your GitHub username in the following command: + +.. code-block:: console + + $ git remote add origin git@github.com:python-websockets/websockets-tutorial.git + $ git push -u origin main + Enumerating objects: 11, done. + Counting objects: 100% (11/11), done. + Delta compression using up to 8 threads + Compressing objects: 100% (10/10), done. + Writing objects: 100% (11/11), 5.90 KiB | 2.95 MiB/s, done. + Total 11 (delta 0), reused 0 (delta 0), pack-reused 0 + To github.com:<username>/websockets-tutorial.git + * [new branch] main -> main + Branch 'main' set up to track remote branch 'main' from 'origin'. + +Go back to GitHub, open the Settings tab of the repository and select Pages in +the menu. Select the main branch as source and click Save. GitHub tells you +that your site is published. + +Follow the link and start a game! + +Summary +------- + +In this third part of the tutorial, you learned how to deploy a WebSocket +application with Heroku. + +You can start a Connect Four game, send the JOIN link to a friend, and play +over the Internet! + +Congratulations for completing the tutorial. Enjoy building real-time web +applications with websockets! + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step3/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/main.js + :caption: main.js + :language: js + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/Procfile + :caption: Procfile + :language: text + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/requirements.txt + :caption: requirements.txt + :language: text + :linenos: diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat new file mode 100644 index 0000000000..922152e96a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst new file mode 100644 index 0000000000..264e6e42d1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst @@ -0,0 +1,1230 @@ +Changelog +========= + +.. currentmodule:: websockets + +.. _backwards-compatibility policy: + +Backwards-compatibility policy +------------------------------ + +websockets is intended for production use. Therefore, stability is a goal. + +websockets also aims at providing the best API for WebSocket in Python. + +While we value stability, we value progress more. When an improvement requires +changing a public API, we make the change and document it in this changelog. + +When possible with reasonable effort, we preserve backwards-compatibility for +five years after the release that introduced the change. + +When a release contains backwards-incompatible API changes, the major version +is increased, else the minor version is increased. Patch versions are only for +fixing regressions shortly after a release. + +Only documented APIs are public. Undocumented, private APIs may change without +notice. + +12.0 +---- + +*October 21, 2023* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 12.0 requires Python ≥ 3.8. + :class: tip + + websockets 11.0 is the last version supporting Python 3.7. + +Improvements +............ + +* Made convenience imports from ``websockets`` compatible with static code + analysis tools such as auto-completion in an IDE or type checking with mypy_. + + .. _mypy: https://github.com/python/mypy + +* Accepted a plain :class:`int` where an :class:`~http.HTTPStatus` is expected. + +* Added :class:`~frames.CloseCode`. + +11.0.3 +------ + +*May 7, 2023* + +Bug fixes +......... + +* Fixed the :mod:`threading` implementation of servers on Windows. + +11.0.2 +------ + +*April 18, 2023* + +Bug fixes +......... + +* Fixed a deadlock in the :mod:`threading` implementation when closing a + connection without reading all messages. + +11.0.1 +------ + +*April 6, 2023* + +Bug fixes +......... + +* Restored the C extension in the source distribution. + +11.0 +---- + +*April 2, 2023* + +Backwards-incompatible changes +.............................. + +.. admonition:: The Sans-I/O implementation was moved. + :class: caution + + Aliases provide compatibility for all previously public APIs according to + the `backwards-compatibility policy`_. + + * The ``connection`` module was renamed to ``protocol``. + + * The ``connection.Connection``, ``server.ServerConnection``, and + ``client.ClientConnection`` classes were renamed to ``protocol.Protocol``, + ``server.ServerProtocol``, and ``client.ClientProtocol``. + +.. admonition:: Sans-I/O protocol constructors now use keyword-only arguments. + :class: caution + + If you instantiate :class:`~server.ServerProtocol` or + :class:`~client.ClientProtocol` directly, make sure you are using keyword + arguments. + +.. admonition:: Closing a connection without an empty close frame is OK. + :class: note + + Receiving an empty close frame now results in + :exc:`~exceptions.ConnectionClosedOK` instead of + :exc:`~exceptions.ConnectionClosedError`. + + As a consequence, calling ``WebSocket.close()`` without arguments in a + browser isn't reported as an error anymore. + +.. admonition:: :func:`~server.serve` times out on the opening handshake after 10 seconds by default. + :class: note + + You can adjust the timeout with the ``open_timeout`` parameter. Set it to + :obj:`None` to disable the timeout entirely. + +New features +............ + +.. admonition:: websockets 11.0 introduces a implementation on top of :mod:`threading`. + :class: important + + It may be more convenient if you don't need to manage many connections and + you're more comfortable with :mod:`threading` than :mod:`asyncio`. + + It is particularly suited to client applications that establish only one + connection. It may be used for servers handling few connections. + + See :func:`~sync.client.connect` and :func:`~sync.server.serve` for details. + +* Added ``open_timeout`` to :func:`~server.serve`. + +* Made it possible to close a server without closing existing connections. + +* Added :attr:`~server.ServerProtocol.select_subprotocol` to customize + negotiation of subprotocols in the Sans-I/O layer. + +Improvements +............ + +* Added platform-independent wheels. + +* Improved error handling in :func:`~websockets.broadcast`. + +* Set ``server_hostname`` automatically on TLS connections when providing a + ``sock`` argument to :func:`~sync.client.connect`. + +10.4 +---- + +*October 25, 2022* + +New features +............ + +* Validated compatibility with Python 3.11. + +* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` property to + protocols. + +* Changed :attr:`~legacy.protocol.WebSocketCommonProtocol.ping` to return the + latency of the connection. + +* Supported overriding or removing the ``User-Agent`` header in clients and the + ``Server`` header in servers. + +* Added deployment guides for more Platform as a Service providers. + +Improvements +............ + +* Improved FAQ. + +10.3 +---- + +*April 17, 2022* + +Backwards-incompatible changes +.............................. + +.. admonition:: The ``exception`` attribute of :class:`~http11.Request` and :class:`~http11.Response` is deprecated. + :class: note + + Use the ``handshake_exc`` attribute of :class:`~server.ServerProtocol` and + :class:`~client.ClientProtocol` instead. + + See :doc:`../howto/sansio` for details. + +Improvements +............ + +* Reduced noise in logs when :mod:`ssl` or :mod:`zlib` raise exceptions. + +10.2 +---- + +*February 21, 2022* + +Improvements +............ + +* Made compression negotiation more lax for compatibility with Firefox. + +* Improved FAQ and quick start guide. + +Bug fixes +......... + +* Fixed backwards-incompatibility in 10.1 for connection handlers created with + :func:`functools.partial`. + +* Avoided leaking open sockets when :func:`~client.connect` is canceled. + +10.1 +---- + +*November 14, 2021* + +New features +............ + +* Added a tutorial. + +* Made the second parameter of connection handlers optional. It will be + deprecated in the next major release. The request path is available in + the :attr:`~legacy.protocol.WebSocketCommonProtocol.path` attribute of + the first argument. + + If you implemented the connection handler of a server as:: + + async def handler(request, path): + ... + + You should replace it by:: + + async def handler(request): + path = request.path # if handler() uses the path argument + ... + +* Added ``python -m websockets --version``. + +Improvements +............ + +* Added wheels for Python 3.10, PyPy 3.7, and for more platforms. + +* Reverted optimization of default compression settings for clients, mainly to + avoid triggering bugs in poorly implemented servers like `AWS API Gateway`_. + + .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065 + +* Mirrored the entire :class:`~asyncio.Server` API + in :class:`~server.WebSocketServer`. + +* Improved performance for large messages on ARM processors. + +* Documented how to auto-reload on code changes in development. + +Bug fixes +......... + +* Avoided half-closing TCP connections that are already closed. + +10.0 +---- + +*September 9, 2021* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 10.0 requires Python ≥ 3.7. + :class: tip + + websockets 9.1 is the last version supporting Python 3.6. + +.. admonition:: The ``loop`` parameter is deprecated from all APIs. + :class: caution + + This reflects a decision made in Python 3.8. See the release notes of + Python 3.10 for details. + + The ``loop`` parameter is also removed + from :class:`~server.WebSocketServer`. This should be transparent. + +.. admonition:: :func:`~client.connect` times out after 10 seconds by default. + :class: note + + You can adjust the timeout with the ``open_timeout`` parameter. Set it to + :obj:`None` to disable the timeout entirely. + +.. admonition:: The ``legacy_recv`` option is deprecated. + :class: note + + See the release notes of websockets 3.0 for details. + +.. admonition:: The signature of :exc:`~exceptions.ConnectionClosed` changed. + :class: note + + If you raise :exc:`~exceptions.ConnectionClosed` or a subclass, rather + than catch them when websockets raises them, you must change your code. + +.. admonition:: A ``msg`` parameter was added to :exc:`~exceptions.InvalidURI`. + :class: note + + If you raise :exc:`~exceptions.InvalidURI`, rather than catch it when + websockets raises it, you must change your code. + +New features +............ + +.. admonition:: websockets 10.0 introduces a `Sans-I/O API + <https://sans-io.readthedocs.io/>`_ for easier integration + in third-party libraries. + :class: important + + If you're integrating websockets in a library, rather than just using it, + look at the :doc:`Sans-I/O integration guide <../howto/sansio>`. + +* Added compatibility with Python 3.10. + +* Added :func:`~websockets.broadcast` to send a message to many clients. + +* Added support for reconnecting automatically by using + :func:`~client.connect` as an asynchronous iterator. + +* Added ``open_timeout`` to :func:`~client.connect`. + +* Documented how to integrate with `Django <https://www.djangoproject.com/>`_. + +* Documented how to deploy websockets in production, with several options. + +* Documented how to authenticate connections. + +* Documented how to broadcast messages to many connections. + +Improvements +............ + +* Improved logging. See the :doc:`logging guide <../topics/logging>`. + +* Optimized default compression settings to reduce memory usage. + +* Optimized processing of client-to-server messages when the C extension isn't + available. + +* Supported relative redirects in :func:`~client.connect`. + +* Handled TCP connection drops during the opening handshake. + +* Made it easier to customize authentication with + :meth:`~auth.BasicAuthWebSocketServerProtocol.check_credentials`. + +* Provided additional information in :exc:`~exceptions.ConnectionClosed` + exceptions. + +* Clarified several exceptions or log messages. + +* Restructured documentation. + +* Improved API documentation. + +* Extended FAQ. + +Bug fixes +......... + +* Avoided a crash when receiving a ping while the connection is closing. + +9.1 +--- + +*May 27, 2021* + +Security fix +............ + +.. admonition:: websockets 9.1 fixes a security issue introduced in 8.0. + :class: important + + Version 8.0 was vulnerable to timing attacks on HTTP Basic Auth passwords + (`CVE-2021-33880`_). + + .. _CVE-2021-33880: https://nvd.nist.gov/vuln/detail/CVE-2021-33880 + +9.0.2 +----- + +*May 15, 2021* + +Bug fixes +......... + +* Restored compatibility of ``python -m websockets`` with Python < 3.9. + +* Restored compatibility with mypy. + +9.0.1 +----- + +*May 2, 2021* + +Bug fixes +......... + +* Fixed issues with the packaging of the 9.0 release. + +9.0 +--- + +*May 1, 2021* + +Backwards-incompatible changes +.............................. + +.. admonition:: Several modules are moved or deprecated. + :class: caution + + Aliases provide compatibility for all previously public APIs according to + the `backwards-compatibility policy`_ + + * :class:`~datastructures.Headers` and + :exc:`~datastructures.MultipleValuesError` are moved from + ``websockets.http`` to :mod:`websockets.datastructures`. If you're using + them, you should adjust the import path. + + * The ``client``, ``server``, ``protocol``, and ``auth`` modules were + moved from the ``websockets`` package to a ``websockets.legacy`` + sub-package. Despite the name, they're still fully supported. + + * The ``framing``, ``handshake``, ``headers``, ``http``, and ``uri`` + modules in the ``websockets`` package are deprecated. These modules + provided low-level APIs for reuse by other projects, but they didn't + reach that goal. Keeping these APIs public makes it more difficult to + improve websockets. + + These changes pave the path for a refactoring that should be a transparent + upgrade for most uses and facilitate integration by other projects. + +.. admonition:: Convenience imports from ``websockets`` are performed lazily. + :class: note + + While Python supports this, tools relying on static code analysis don't. + This breaks auto-completion in an IDE or type checking with mypy_. + + .. _mypy: https://github.com/python/mypy + + If you depend on such tools, use the real import paths, which can be found + in the API documentation, for example:: + + from websockets.client import connect + from websockets.server import serve + +New features +............ + +* Added compatibility with Python 3.9. + +Improvements +............ + +* Added support for IRIs in addition to URIs. + +* Added close codes 1012, 1013, and 1014. + +* Raised an error when passing a :class:`dict` to + :meth:`~legacy.protocol.WebSocketCommonProtocol.send`. + +* Improved error reporting. + +Bug fixes +......... + +* Fixed sending fragmented, compressed messages. + +* Fixed ``Host`` header sent when connecting to an IPv6 address. + +* Fixed creating a client or a server with an existing Unix socket. + +* Aligned maximum cookie size with popular web browsers. + +* Ensured cancellation always propagates, even on Python versions where + :exc:`~asyncio.CancelledError` inherits :exc:`Exception`. + +8.1 +--- + +*November 1, 2019* + +New features +............ + +* Added compatibility with Python 3.8. + +8.0.2 +----- + +*July 31, 2019* + +Bug fixes +......... + +* Restored the ability to pass a socket with the ``sock`` parameter of + :func:`~server.serve`. + +* Removed an incorrect assertion when a connection drops. + +8.0.1 +----- + +*July 21, 2019* + +Bug fixes +......... + +* Restored the ability to import ``WebSocketProtocolError`` from + ``websockets``. + +8.0 +--- + +*July 7, 2019* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 8.0 requires Python ≥ 3.6. + :class: tip + + websockets 7.0 is the last version supporting Python 3.4 and 3.5. + +.. admonition:: ``process_request`` is now expected to be a coroutine. + :class: note + + If you're passing a ``process_request`` argument to + :func:`~server.serve` or :class:`~server.WebSocketServerProtocol`, or if + you're overriding + :meth:`~server.WebSocketServerProtocol.process_request` in a subclass, + define it with ``async def`` instead of ``def``. Previously, both were supported. + + For backwards compatibility, functions are still accepted, but mixing + functions and coroutines won't work in some inheritance scenarios. + +.. admonition:: ``max_queue`` must be :obj:`None` to disable the limit. + :class: note + + If you were setting ``max_queue=0`` to make the queue of incoming messages + unbounded, change it to ``max_queue=None``. + +.. admonition:: The ``host``, ``port``, and ``secure`` attributes + of :class:`~legacy.protocol.WebSocketCommonProtocol` are deprecated. + :class: note + + Use :attr:`~legacy.protocol.WebSocketCommonProtocol.local_address` in + servers and + :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` in clients + instead of ``host`` and ``port``. + +.. admonition:: ``WebSocketProtocolError`` is renamed + to :exc:`~exceptions.ProtocolError`. + :class: note + + An alias provides backwards compatibility. + +.. admonition:: ``read_response()`` now returns the reason phrase. + :class: note + + If you're using this low-level API, you must change your code. + +New features +............ + +* Added :func:`~auth.basic_auth_protocol_factory` to enforce HTTP + Basic Auth on the server side. + +* :func:`~client.connect` handles redirects from the server during the + handshake. + +* :func:`~client.connect` supports overriding ``host`` and ``port``. + +* Added :func:`~client.unix_connect` for connecting to Unix sockets. + +* Added support for asynchronous generators + in :meth:`~legacy.protocol.WebSocketCommonProtocol.send` + to generate fragmented messages incrementally. + +* Enabled readline in the interactive client. + +* Added type hints (:pep:`484`). + +* Added a FAQ to the documentation. + +* Added documentation for extensions. + +* Documented how to optimize memory usage. + +Improvements +............ + +* :meth:`~legacy.protocol.WebSocketCommonProtocol.send`, + :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support bytes-like + types :class:`bytearray` and :class:`memoryview` in addition to + :class:`bytes`. + +* Added :exc:`~exceptions.ConnectionClosedOK` and + :exc:`~exceptions.ConnectionClosedError` subclasses of + :exc:`~exceptions.ConnectionClosed` to tell apart normal connection + termination from errors. + +* Changed :meth:`WebSocketServer.close() + <server.WebSocketServer.close>` to perform a proper closing handshake + instead of failing the connection. + +* Improved error messages when HTTP parsing fails. + +* Improved API documentation. + +Bug fixes +......... + +* Prevented spurious log messages about :exc:`~exceptions.ConnectionClosed` + exceptions in keepalive ping task. If you were using ``ping_timeout=None`` + as a workaround, you can remove it. + +* Avoided a crash when a ``extra_headers`` callable returns :obj:`None`. + +7.0 +--- + +*November 1, 2018* + +Backwards-incompatible changes +.............................. + +.. admonition:: Keepalive is enabled by default. + :class: important + + websockets now sends Ping frames at regular intervals and closes the + connection if it doesn't receive a matching Pong frame. + See :class:`~legacy.protocol.WebSocketCommonProtocol` for details. + +.. admonition:: Termination of connections by :meth:`WebSocketServer.close() + <server.WebSocketServer.close>` changes. + :class: caution + + Previously, connections handlers were canceled. Now, connections are + closed with close code 1001 (going away). + + From the perspective of the connection handler, this is the same as if the + remote endpoint was disconnecting. This removes the need to prepare for + :exc:`~asyncio.CancelledError` in connection handlers. + + You can restore the previous behavior by adding the following line at the + beginning of connection handlers:: + + def handler(websocket, path): + closed = asyncio.ensure_future(websocket.wait_closed()) + closed.add_done_callback(lambda task: task.cancel()) + +.. admonition:: Calling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` + concurrently raises a :exc:`RuntimeError`. + :class: note + + Concurrent calls lead to non-deterministic behavior because there are no + guarantees about which coroutine will receive which message. + +.. admonition:: The ``timeout`` argument of :func:`~server.serve` + and :func:`~client.connect` is renamed to ``close_timeout`` . + :class: note + + This prevents confusion with ``ping_timeout``. + + For backwards compatibility, ``timeout`` is still supported. + +.. admonition:: The ``origins`` argument of :func:`~server.serve` changes. + :class: note + + Include :obj:`None` in the list rather than ``''`` to allow requests that + don't contain an Origin header. + +.. admonition:: Pending pings aren't canceled when the connection is closed. + :class: note + + A ping — as in ``ping = await websocket.ping()`` — for which no pong was + received yet used to be canceled when the connection is closed, so that + ``await ping`` raised :exc:`~asyncio.CancelledError`. + + Now ``await ping`` raises :exc:`~exceptions.ConnectionClosed` like other + public APIs. + +New features +............ + +* Added ``process_request`` and ``select_subprotocol`` arguments to + :func:`~server.serve` and + :class:`~server.WebSocketServerProtocol` to facilitate customization of + :meth:`~server.WebSocketServerProtocol.process_request` and + :meth:`~server.WebSocketServerProtocol.select_subprotocol`. + +* Added support for sending fragmented messages. + +* Added the :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed` + method to protocols. + +* Added an interactive client: ``python -m websockets <uri>``. + +Improvements +............ + +* Improved handling of multiple HTTP headers with the same name. + +* Improved error messages when a required HTTP header is missing. + +Bug fixes +......... + +* Fixed a data loss bug in + :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`: + canceling it at the wrong time could result in messages being dropped. + +6.0 +--- + +*July 16, 2018* + +Backwards-incompatible changes +.............................. + +.. admonition:: The :class:`~datastructures.Headers` class is introduced and + several APIs are updated to use it. + :class: caution + + * The ``request_headers`` argument + of :meth:`~server.WebSocketServerProtocol.process_request` is now + a :class:`~datastructures.Headers` instead of + an ``http.client.HTTPMessage``. + + * The ``request_headers`` and ``response_headers`` attributes of + :class:`~legacy.protocol.WebSocketCommonProtocol` are now + :class:`~datastructures.Headers` instead of ``http.client.HTTPMessage``. + + * The ``raw_request_headers`` and ``raw_response_headers`` attributes of + :class:`~legacy.protocol.WebSocketCommonProtocol` are removed. Use + :meth:`~datastructures.Headers.raw_items` instead. + + * Functions defined in the ``handshake`` module now receive + :class:`~datastructures.Headers` in argument instead of ``get_header`` + or ``set_header`` functions. This affects libraries that rely on + low-level APIs. + + * Functions defined in the ``http`` module now return HTTP headers as + :class:`~datastructures.Headers` instead of lists of ``(name, value)`` + pairs. + + Since :class:`~datastructures.Headers` and ``http.client.HTTPMessage`` + provide similar APIs, much of the code dealing with HTTP headers won't + require changes. + +New features +............ + +* Added compatibility with Python 3.7. + +5.0.1 +----- + +*May 24, 2018* + +Bug fixes +......... + +* Fixed a regression in 5.0 that broke some invocations of + :func:`~server.serve` and :func:`~client.connect`. + +5.0 +--- + +*May 22, 2018* + +Security fix +............ + +.. admonition:: websockets 5.0 fixes a security issue introduced in 4.0. + :class: important + + Version 4.0 was vulnerable to denial of service by memory exhaustion + because it didn't enforce ``max_size`` when decompressing compressed + messages (`CVE-2018-1000518`_). + + .. _CVE-2018-1000518: https://nvd.nist.gov/vuln/detail/CVE-2018-1000518 + +Backwards-incompatible changes +.............................. + +.. admonition:: A ``user_info`` field is added to the return value of + ``parse_uri`` and ``WebSocketURI``. + :class: note + + If you're unpacking ``WebSocketURI`` into four variables, adjust your code + to account for that fifth field. + +New features +............ + +* :func:`~client.connect` performs HTTP Basic Auth when the URI contains + credentials. + +* :func:`~server.unix_serve` can be used as an asynchronous context + manager on Python ≥ 3.5.1. + +* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.closed` property + to protocols. + +* Added new examples in the documentation. + +Improvements +............ + +* Iterating on incoming messages no longer raises an exception when the + connection terminates with close code 1001 (going away). + +* A plain HTTP request now receives a 426 Upgrade Required response and + doesn't log a stack trace. + +* If a :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` doesn't receive a + pong, it's canceled when the connection is closed. + +* Reported the cause of :exc:`~exceptions.ConnectionClosed` exceptions. + +* Stopped logging stack traces when the TCP connection dies prematurely. + +* Prevented writing to a closing TCP connection during unclean shutdowns. + +* Made connection termination more robust to network congestion. + +* Prevented processing of incoming frames after failing the connection. + +* Updated documentation with new features from Python 3.6. + +* Improved several sections of the documentation. + +Bug fixes +......... + +* Prevented :exc:`TypeError` due to missing close code on connection close. + +* Fixed a race condition in the closing handshake that raised + :exc:`~exceptions.InvalidState`. + +4.0.1 +----- + +*November 2, 2017* + +Bug fixes +......... + +* Fixed issues with the packaging of the 4.0 release. + +4.0 +--- + +*November 2, 2017* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 4.0 requires Python ≥ 3.4. + :class: tip + + websockets 3.4 is the last version supporting Python 3.3. + +.. admonition:: Compression is enabled by default. + :class: important + + In August 2017, Firefox and Chrome support the permessage-deflate + extension, but not Safari and IE. + + Compression should improve performance but it increases RAM and CPU use. + + If you want to disable compression, add ``compression=None`` when calling + :func:`~server.serve` or :func:`~client.connect`. + +.. admonition:: The ``state_name`` attribute of protocols is deprecated. + :class: note + + Use ``protocol.state.name`` instead of ``protocol.state_name``. + +New features +............ + +* :class:`~legacy.protocol.WebSocketCommonProtocol` instances can be used as + asynchronous iterators on Python ≥ 3.6. They yield incoming messages. + +* Added :func:`~server.unix_serve` for listening on Unix sockets. + +* Added the :attr:`~server.WebSocketServer.sockets` attribute to the + return value of :func:`~server.serve`. + +* Allowed ``extra_headers`` to override ``Server`` and ``User-Agent`` headers. + +Improvements +............ + +* Reorganized and extended documentation. + +* Rewrote connection termination to increase robustness in edge cases. + +* Reduced verbosity of "Failing the WebSocket connection" logs. + +Bug fixes +......... + +* Aborted connections if they don't close within the configured ``timeout``. + +* Stopped leaking pending tasks when :meth:`~asyncio.Task.cancel` is called on + a connection while it's being closed. + +3.4 +--- + +*August 20, 2017* + +Backwards-incompatible changes +.............................. + +.. admonition:: ``InvalidStatus`` is replaced + by :class:`~exceptions.InvalidStatusCode`. + :class: note + + This exception is raised when :func:`~client.connect` receives an invalid + response status code from the server. + +New features +............ + +* :func:`~server.serve` can be used as an asynchronous context manager + on Python ≥ 3.5.1. + +* Added support for customizing handling of incoming connections with + :meth:`~server.WebSocketServerProtocol.process_request`. + +* Made read and write buffer sizes configurable. + +Improvements +............ + +* Renamed :func:`~server.serve` and :func:`~client.connect`'s + ``klass`` argument to ``create_protocol`` to reflect that it can also be a + callable. For backwards compatibility, ``klass`` is still supported. + +* Rewrote HTTP handling for simplicity and performance. + +* Added an optional C extension to speed up low-level operations. + +Bug fixes +......... + +* Providing a ``sock`` argument to :func:`~client.connect` no longer + crashes. + +3.3 +--- + +*March 29, 2017* + +New features +............ + +* Ensured compatibility with Python 3.6. + +Improvements +............ + +* Reduced noise in logs caused by connection resets. + +Bug fixes +......... + +* Avoided crashing on concurrent writes on slow connections. + +3.2 +--- + +*August 17, 2016* + +New features +............ + +* Added ``timeout``, ``max_size``, and ``max_queue`` arguments to + :func:`~client.connect` and :func:`~server.serve`. + +Improvements +............ + +* Made server shutdown more robust. + +3.1 +--- + +*April 21, 2016* + +New features +............ + +* Added flow control for incoming data. + +Bug fixes +......... + +* Avoided a warning when closing a connection before the opening handshake. + +3.0 +--- + +*December 25, 2015* + +Backwards-incompatible changes +.............................. + +.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` now + raises an exception when the connection is closed. + :class: caution + + :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` used to return + :obj:`None` when the connection was closed. This required checking the + return value of every call:: + + message = await websocket.recv() + if message is None: + return + + Now it raises a :exc:`~exceptions.ConnectionClosed` exception instead. + This is more Pythonic. The previous code can be simplified to:: + + message = await websocket.recv() + + When implementing a server, there's no strong reason to handle such + exceptions. Let them bubble up, terminate the handler coroutine, and the + server will simply ignore them. + + In order to avoid stranding projects built upon an earlier version, the + previous behavior can be restored by passing ``legacy_recv=True`` to + :func:`~server.serve`, :func:`~client.connect`, + :class:`~server.WebSocketServerProtocol`, or + :class:`~client.WebSocketClientProtocol`. + +New features +............ + +* :func:`~client.connect` can be used as an asynchronous context + manager on Python ≥ 3.5.1. + +* :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support data passed as + :class:`str` in addition to :class:`bytes`. + +* Made ``state_name`` attribute on protocols a public API. + +Improvements +............ + +* Updated documentation with ``await`` and ``async`` syntax from Python 3.5. + +* Worked around an :mod:`asyncio` bug affecting connection termination under + load. + +* Improved documentation. + +2.7 +--- + +*November 18, 2015* + +New features +............ + +* Added compatibility with Python 3.5. + +Improvements +............ + +* Refreshed documentation. + +2.6 +--- + +*August 18, 2015* + +New features +............ + +* Added ``local_address`` and ``remote_address`` attributes on protocols. + +* Closed open connections with code 1001 when a server shuts down. + +Bug fixes +......... + +* Avoided TCP fragmentation of small frames. + +2.5 +--- + +*July 28, 2015* + +New features +............ + +* Provided access to handshake request and response HTTP headers. + +* Allowed customizing handshake request and response HTTP headers. + +* Added support for running on a non-default event loop. + +Improvements +............ + +* Improved documentation. + +* Sent a 403 status code instead of 400 when request Origin isn't allowed. + +* Clarified that the closing handshake can be initiated by the client. + +* Set the close code and reason more consistently. + +* Strengthened connection termination. + +Bug fixes +......... + +* Canceling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` no longer + drops the next message. + +2.4 +--- + +*January 31, 2015* + +New features +............ + +* Added support for subprotocols. + +* Added ``loop`` argument to :func:`~client.connect` and + :func:`~server.serve`. + +2.3 +--- + +*November 3, 2014* + +Improvements +............ + +* Improved compliance of close codes. + +2.2 +--- + +*July 28, 2014* + +New features +............ + +* Added support for limiting message size. + +2.1 +--- + +*April 26, 2014* + +New features +............ + +* Added ``host``, ``port`` and ``secure`` attributes on protocols. + +* Added support for providing and checking Origin_. + +.. _Origin: https://www.rfc-editor.org/rfc/rfc6455.html#section-10.2 + +2.0 +--- + +*February 16, 2014* + +Backwards-incompatible changes +.............................. + +.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.send`, + :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` are now coroutines. + :class: caution + + They used to be functions. + + Instead of:: + + websocket.send(message) + + you must write:: + + await websocket.send(message) + +New features +............ + +* Added flow control for outgoing data. + +1.0 +--- + +*November 14, 2013* + +New features +............ + +* Initial public release. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst new file mode 100644 index 0000000000..020ed7ad85 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst @@ -0,0 +1,66 @@ +Contributing +============ + +Thanks for taking the time to contribute to websockets! + +Code of Conduct +--------------- + +This project and everyone participating in it is governed by the `Code of +Conduct`_. By participating, you are expected to uphold this code. Please +report inappropriate behavior to aymeric DOT augustin AT fractalideas DOT com. + +.. _Code of Conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md + +*(If I'm the person with the inappropriate behavior, please accept my +apologies. I know I can mess up. I can't expect you to tell me, but if you +choose to do so, I'll do my best to handle criticism constructively. +-- Aymeric)* + +Contributions +------------- + +Bug reports, patches and suggestions are welcome! + +Please open an issue_ or send a `pull request`_. + +Feedback about the documentation is especially valuable, as the primary author +feels more confident about writing code than writing docs :-) + +If you're wondering why things are done in a certain way, the :doc:`design +document <../topics/design>` provides lots of details about the internals of +websockets. + +.. _issue: https://github.com/python-websockets/websockets/issues/new +.. _pull request: https://github.com/python-websockets/websockets/compare/ + +Questions +--------- + +GitHub issues aren't a good medium for handling questions. There are better +places to ask questions, for example Stack Overflow. + +If you want to ask a question anyway, please make sure that: + +- it's a question about websockets and not about :mod:`asyncio`; +- it isn't answered in the documentation; +- it wasn't asked already. + +A good question can be written as a suggestion to improve the documentation. + +Cryptocurrency users +-------------------- + +websockets appears to be quite popular for interfacing with Bitcoin or other +cryptocurrency trackers. I'm strongly opposed to Bitcoin's carbon footprint. + +I'm aware of efforts to build proof-of-stake models. I'll care once the total +energy consumption of all cryptocurrencies drops to a non-bullshit level. + +You already negated all of humanity's efforts to develop renewable energy. +Please stop heating the planet where my children will have to live. + +Since websockets is released under an open-source license, you can use it for +any purpose you like. However, I won't spend any of my time to help you. + +I will summarily close issues related to Bitcoin or cryptocurrency in any way. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst new file mode 100644 index 0000000000..459146345b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst @@ -0,0 +1,12 @@ +About websockets +================ + +This is about websockets-the-project rather than websockets-the-software. + +.. toctree:: + :titlesonly: + + changelog + contributing + license + For enterprise <tidelift> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst new file mode 100644 index 0000000000..0a3b8703d5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst @@ -0,0 +1,4 @@ +License +======= + +.. include:: ../../LICENSE diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst new file mode 100644 index 0000000000..42100fade9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst @@ -0,0 +1,112 @@ +websockets for enterprise +========================= + +Available as part of the Tidelift Subscription +---------------------------------------------- + +.. image:: ../_static/tidelift.png + :height: 150px + :width: 150px + :align: left + +Tidelift is working with the maintainers of websockets and thousands of other +open source projects to deliver commercial support and maintenance for the +open source dependencies you use to build your applications. Save time, reduce +risk, and improve code health, while paying the maintainers of the exact +dependencies you use. + +.. raw:: html + + <style type="text/css"> + .tidelift-links { + display: flex; + justify-content: center; + } + @media only screen and (max-width: 600px) { + .tidelift-links { + flex-direction: column; + } + } + .tidelift-links a { + border: thin solid #f6914d; + border-radius: 0.25em; + font-family: Verdana, sans-serif; + font-size: 15px; + margin: 0.5em 2em; + padding: 0.5em 2em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + } + .tidelift-links a.tidelift-links__learn-more { + background-color: white; + color: #f6914d; + } + .tidelift-links a.tidelift-links__request-a-demo { + background-color: #f6914d; + color: white; + } + </style> + + <div class="tidelift-links"> + <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a> + <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a> + </div> + +Enterprise-ready open source software—managed for you +----------------------------------------------------- + +The Tidelift Subscription is a managed open source subscription for +application dependencies covering millions of open source projects across +JavaScript, Python, Java, PHP, Ruby, .NET, and more. + +Your subscription includes: + +* **Security updates** + + * Tidelift’s security response team coordinates patches for new breaking + security vulnerabilities and alerts immediately through a private channel, + so your software supply chain is always secure. + +* **Licensing verification and indemnification** + + * Tidelift verifies license information to enable easy policy enforcement + and adds intellectual property indemnification to cover creators and users + in case something goes wrong. You always have a 100% up-to-date bill of + materials for your dependencies to share with your legal team, customers, + or partners. + +* **Maintenance and code improvement** + + * Tidelift ensures the software you rely on keeps working as long as you + need it to work. Your managed dependencies are actively maintained and we + recruit additional maintainers where required. + +* **Package selection and version guidance** + + * We help you choose the best open source packages from the start—and then + guide you through updates to stay on the best releases as new issues + arise. + +* **Roadmap input** + + * Take a seat at the table with the creators behind the software you use. + Tidelift’s participating maintainers earn more income as their software is + used by more subscribers, so they’re interested in knowing what you need. + +* **Tooling and cloud integration** + + * Tidelift works with GitHub, GitLab, BitBucket, and more. We support every + cloud platform (and other deployment targets, too). + +The end result? All of the capabilities you expect from commercial-grade +software, for the full breadth of open source you use. That means less time +grappling with esoteric open source trivia, and more time building your own +applications—and your business. + +.. raw:: html + + <div class="tidelift-links"> + <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a> + <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a> + </div> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst new file mode 100644 index 0000000000..5086015b7b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst @@ -0,0 +1,64 @@ +Client (:mod:`asyncio`) +======================= + +.. automodule:: websockets.client + +Opening a connection +-------------------- + +.. 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) + :async: + +.. 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) + :async: + +Using a connection +------------------ + +.. 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) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst new file mode 100644 index 0000000000..dc7a54ee1a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst @@ -0,0 +1,54 @@ +:orphan: + +Both sides (:mod:`asyncio`) +=========================== + +.. automodule:: websockets.legacy.protocol + +.. 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) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst new file mode 100644 index 0000000000..1063179162 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst @@ -0,0 +1,113 @@ +Server (:mod:`asyncio`) +======================= + +.. automodule:: websockets.server + +Starting a server +----------------- + +.. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=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, **kwds) + :async: + +.. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=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, **kwds) + :async: + +Stopping a server +----------------- + +.. autoclass:: WebSocketServer + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: get_loop + + .. automethod:: is_serving + + .. automethod:: start_serving + + .. automethod:: serve_forever + + .. autoattribute:: sockets + +Using a connection +------------------ + +.. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=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) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + You can customize the opening handshake in a subclass by overriding these methods: + + .. automethod:: process_request + + .. automethod:: select_subprotocol + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + +Basic authentication +-------------------- + +.. automodule:: websockets.auth + +websockets supports HTTP Basic Authentication according to +:rfc:`7235` and :rfc:`7617`. + +.. autofunction:: basic_auth_protocol_factory + +.. autoclass:: BasicAuthWebSocketServerProtocol + + .. autoattribute:: realm + + .. autoattribute:: username + + .. automethod:: check_credentials + +Broadcast +--------- + +.. autofunction:: websockets.broadcast diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst new file mode 100644 index 0000000000..ec02d42101 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst @@ -0,0 +1,66 @@ +Data structures +=============== + +WebSocket events +---------------- + +.. automodule:: websockets.frames + + .. autoclass:: Frame + + .. autoclass:: Opcode + + .. autoattribute:: CONT + .. autoattribute:: TEXT + .. autoattribute:: BINARY + .. autoattribute:: CLOSE + .. autoattribute:: PING + .. autoattribute:: PONG + + .. autoclass:: Close + + .. autoclass:: CloseCode + + .. autoattribute:: NORMAL_CLOSURE + .. autoattribute:: GOING_AWAY + .. autoattribute:: PROTOCOL_ERROR + .. autoattribute:: UNSUPPORTED_DATA + .. autoattribute:: NO_STATUS_RCVD + .. autoattribute:: ABNORMAL_CLOSURE + .. autoattribute:: INVALID_DATA + .. autoattribute:: POLICY_VIOLATION + .. autoattribute:: MESSAGE_TOO_BIG + .. autoattribute:: MANDATORY_EXTENSION + .. autoattribute:: INTERNAL_ERROR + .. autoattribute:: SERVICE_RESTART + .. autoattribute:: TRY_AGAIN_LATER + .. autoattribute:: BAD_GATEWAY + .. autoattribute:: TLS_HANDSHAKE + +HTTP events +----------- + +.. automodule:: websockets.http11 + + .. autoclass:: Request + + .. autoclass:: Response + +.. automodule:: websockets.datastructures + + .. autoclass:: Headers + + .. automethod:: get_all + + .. automethod:: raw_items + + .. autoexception:: MultipleValuesError + +URIs +---- + +.. automodule:: websockets.uri + + .. autofunction:: parse_uri + + .. autoclass:: WebSocketURI diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst new file mode 100644 index 0000000000..907a650d20 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst @@ -0,0 +1,6 @@ +Exceptions +========== + +.. automodule:: websockets.exceptions + :members: + diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst new file mode 100644 index 0000000000..a70f1b1e58 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst @@ -0,0 +1,60 @@ +Extensions +========== + +.. currentmodule:: websockets.extensions + +The WebSocket protocol supports extensions_. + +At the time of writing, there's only one `registered extension`_ with a public +specification, WebSocket Per-Message Deflate. + +.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9 +.. _registered extension: https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name + +Per-Message Deflate +------------------- + +.. automodule:: websockets.extensions.permessage_deflate + + :mod:`websockets.extensions.permessage_deflate` implements WebSocket + Per-Message Deflate. + + This extension is specified in :rfc:`7692`. + + Refer to the :doc:`topic guide on compression <../topics/compression>` to + learn more about tuning compression settings. + + .. autoclass:: ClientPerMessageDeflateFactory + + .. autoclass:: ServerPerMessageDeflateFactory + +Base classes +------------ + +.. automodule:: websockets.extensions + + :mod:`websockets.extensions` defines base classes for implementing + extensions. + + Refer to the :doc:`how-to guide on extensions <../howto/extensions>` to + learn more about writing an extension. + + .. autoclass:: Extension + + .. autoattribute:: name + + .. automethod:: decode + + .. automethod:: encode + + .. autoclass:: ClientExtensionFactory + + .. autoattribute:: name + + .. automethod:: get_request_params + + .. automethod:: process_response_params + + .. autoclass:: ServerExtensionFactory + + .. automethod:: process_request_params diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst new file mode 100644 index 0000000000..98b3c0ddaf --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst @@ -0,0 +1,187 @@ +Features +======== + +.. currentmodule:: websockets + +Feature support matrices summarize which implementations support which features. + +.. raw:: html + + <style> + .support-matrix-table { width: 100%; } + .support-matrix-table th:first-child { text-align: left; } + .support-matrix-table th:not(:first-child) { text-align: center; width: 15%; } + .support-matrix-table td:not(:first-child) { text-align: center; } + </style> + +.. |aio| replace:: :mod:`asyncio` +.. |sync| replace:: :mod:`threading` +.. |sans| replace:: `Sans-I/O`_ +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +Both sides +---------- + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Perform the opening handshake | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Send a message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Receive a message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Iterate over received messages | ✅ | ✅ | ❌ | + +------------------------------------+--------+--------+--------+ + | Send a fragmented message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Receive a fragmented message after | ✅ | ✅ | ❌ | + | reassembly | | | | + +------------------------------------+--------+--------+--------+ + | Receive a fragmented message frame | ❌ | ✅ | ✅ | + | by frame (`#479`_) | | | | + +------------------------------------+--------+--------+--------+ + | Send a ping | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Respond to pings automatically | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Send a pong | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform the closing handshake | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Report close codes and reasons | ❌ | ✅ | ✅ | + | from both sides | | | | + +------------------------------------+--------+--------+--------+ + | Compress messages (:rfc:`7692`) | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Tune memory usage for compression | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Negotiate extensions | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Implement custom extensions | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Negotiate a subprotocol | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Enforce security limits | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Log events | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Enforce opening timeout | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Enforce closing timeout | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Keepalive | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Heartbeat | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + +.. _#479: https://github.com/python-websockets/websockets/issues/479 + +Server +------ + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Listen on a TCP socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Listen on a Unix socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Listen using a preexisting socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Encrypt connection with TLS | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close server on context exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close connection on handler exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Shut down server gracefully | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Check ``Origin`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Customize subprotocol selection | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Configure ``Server`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake request | ❌ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake response | ❌ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Basic Authentication | ✅ | ❌ | ❌ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ | + +------------------------------------+--------+--------+--------+ + | Force HTTP response | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + +Client +------ + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Connect to a TCP socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Connect to a Unix socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Connect using a preexisting socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Encrypt connection with TLS | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close connection on context exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Reconnect automatically | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Configure ``Origin`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Configure ``User-Agent`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake request | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Connect to non-ASCII IRIs | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Basic Authentication | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ | + | (`#784`_) | | | | + +------------------------------------+--------+--------+--------+ + | Follow HTTP redirects | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Connect via a HTTP proxy (`#364`_) | ❌ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Connect via a SOCKS5 proxy | ❌ | ❌ | — | + | (`#475`_) | | | | + +------------------------------------+--------+--------+--------+ + +.. _#364: https://github.com/python-websockets/websockets/issues/364 +.. _#475: https://github.com/python-websockets/websockets/issues/475 +.. _#784: https://github.com/python-websockets/websockets/issues/784 + +Known limitations +----------------- + +There is no way to control compression of outgoing frames on a per-frame basis +(`#538`_). If compression is enabled, all frames are compressed. + +.. _#538: https://github.com/python-websockets/websockets/issues/538 + +The server doesn't check the Host header and respond with a HTTP 400 Bad Request +if it is missing or invalid (`#1246`). + +.. _#1246: https://github.com/python-websockets/websockets/issues/1246 + +The client API doesn't attempt to guarantee that there is no more than one +connection to a given IP address in a CONNECTING state. This behavior is +`mandated by RFC 6455`_. However, :func:`~client.connect()` isn't the right +layer for enforcing this constraint. It's the caller's responsibility. + +.. _mandated by RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-4.1 diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst new file mode 100644 index 0000000000..0b80f087a1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst @@ -0,0 +1,90 @@ +API reference +============= + +.. currentmodule:: websockets + +Features +-------- + +Check which implementations support which features and known limitations. + +.. toctree:: + :titlesonly: + + features + + +:mod:`asyncio` +-------------- + +This is the default implementation. It's ideal for servers that handle many +clients concurrently. + +.. toctree:: + :titlesonly: + + asyncio/server + asyncio/client + +:mod:`threading` +---------------- + +This alternative implementation can be a good choice for clients. + +.. toctree:: + :titlesonly: + + sync/server + sync/client + +`Sans-I/O`_ +----------- + +This layer is designed for integrating in third-party libraries, typically +application servers. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. toctree:: + :titlesonly: + + sansio/server + sansio/client + +Extensions +---------- + +The Per-Message Deflate extension is built in. You may also define custom +extensions. + +.. toctree:: + :titlesonly: + + extensions + +Shared +------ + +These low-level APIs are shared by all implementations. + +.. toctree:: + :titlesonly: + + datastructures + exceptions + types + +API stability +------------- + +Public APIs documented in this API reference are subject to the +:ref:`backwards-compatibility policy <backwards-compatibility policy>`. + +Anything that isn't listed in the API reference is a private API. There's no +guarantees of behavior or backwards-compatibility for private APIs. + +Convenience imports +------------------- + +For convenience, many public APIs can be imported directly from the +``websockets`` package. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst new file mode 100644 index 0000000000..09bafc7455 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst @@ -0,0 +1,58 @@ +Client (`Sans-I/O`_) +==================== + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. currentmodule:: websockets.client + +.. autoclass:: ClientProtocol(wsuri, origin=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: connect + + .. automethod:: send_request + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + WebSocket protocol objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: handshake_exc + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst new file mode 100644 index 0000000000..cd1ef3c63a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst @@ -0,0 +1,64 @@ +:orphan: + +Both sides (`Sans-I/O`_) +========================= + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. automodule:: websockets.protocol + +.. autoclass:: Protocol(side, state=State.OPEN, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc + +.. autoclass:: Side + + .. autoattribute:: SERVER + + .. autoattribute:: CLIENT + +.. autoclass:: State + + .. autoattribute:: CONNECTING + + .. autoattribute:: OPEN + + .. autoattribute:: CLOSING + + .. autoattribute:: CLOSED + +.. autodata:: SEND_EOF diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst new file mode 100644 index 0000000000..d70df6277a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst @@ -0,0 +1,62 @@ +Server (`Sans-I/O`_) +==================== + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. currentmodule:: websockets.server + +.. autoclass:: ServerProtocol(origins=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: accept + + .. automethod:: select_subprotocol + + .. automethod:: reject + + .. automethod:: send_response + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + WebSocket protocol objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: handshake_exc + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst new file mode 100644 index 0000000000..6cccd6ec48 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst @@ -0,0 +1,49 @@ +Client (:mod:`threading`) +========================= + +.. automodule:: websockets.sync.client + +Opening a connection +-------------------- + +.. autofunction:: connect(uri, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +.. autofunction:: unix_connect(path, uri=None, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +Using a connection +------------------ + +.. autoclass:: ClientConnection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst new file mode 100644 index 0000000000..3dc6d4a509 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst @@ -0,0 +1,41 @@ +:orphan: + +Both sides (:mod:`threading`) +============================= + +.. automodule:: websockets.sync.connection + +.. autoclass:: Connection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst new file mode 100644 index 0000000000..35c112046a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst @@ -0,0 +1,60 @@ +Server (:mod:`threading`) +========================= + +.. automodule:: websockets.sync.server + +Creating a server +----------------- + +.. autofunction:: serve(handler, host=None, port=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +.. autofunction:: unix_serve(handler, path=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +Running a server +---------------- + +.. autoclass:: WebSocketServer + + .. automethod:: serve_forever + + .. automethod:: shutdown + + .. automethod:: fileno + +Using a connection +------------------ + +.. autoclass:: ServerConnection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst new file mode 100644 index 0000000000..9d3aa8310b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst @@ -0,0 +1,24 @@ +Types +===== + +.. automodule:: websockets.typing + + .. autodata:: Data + + .. autodata:: LoggerLike + + .. autodata:: StatusLike + + .. autodata:: Origin + + .. autodata:: Subprotocol + + .. autodata:: ExtensionName + + .. autodata:: ExtensionParameter + +.. autodata:: websockets.protocol.Event + +.. autodata:: websockets.datastructures.HeadersLike + +.. autodata:: websockets.datastructures.SupportsKeysAndGetItem diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt new file mode 100644 index 0000000000..bcd1d71143 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt @@ -0,0 +1,8 @@ +furo +sphinx +sphinx-autobuild +sphinx-copybutton +sphinx-inline-tabs +sphinxcontrib-spelling +sphinxcontrib-trio +sphinxext-opengraph diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt new file mode 100644 index 0000000000..dfa7065e79 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt @@ -0,0 +1,85 @@ +augustin +auth +autoscaler +aymeric +backend +backoff +backpressure +balancer +balancers +bottlenecked +bufferbloat +bugfix +buildpack +bytestring +bytestrings +changelog +coroutine +coroutines +cryptocurrencies +cryptocurrency +css +ctrl +deserialize +django +dev +Dockerfile +dyno +formatter +fractalideas +gunicorn +healthz +html +hypercorn +iframe +IPv +istio +iterable +js +keepalive +KiB +kubernetes +lifecycle +linkerd +liveness +lookups +MiB +mutex +mypy +nginx +Paketo +permessage +pid +procfile +proxying +py +pythonic +reconnection +redis +redistributions +retransmit +runtime +scalable +stateful +subclasses +subclassing +submodule +subpackages +subprotocol +subprotocols +supervisord +tidelift +tls +tox +txt +unregister +uple +uvicorn +uvloop +virtualenv +WebSocket +websocket +websockets +ws +wsgi +www diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst new file mode 100644 index 0000000000..31bc8e6da8 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst @@ -0,0 +1,348 @@ +Authentication +============== + +The WebSocket protocol was designed for creating web applications that need +bidirectional communication between clients running in browsers and servers. + +In most practical use cases, WebSocket servers need to authenticate clients in +order to route communications appropriately and securely. + +:rfc:`6455` stays elusive when it comes to authentication: + + This protocol doesn't prescribe any particular way that servers can + authenticate clients during the WebSocket handshake. The WebSocket + server can use any client authentication mechanism available to a + generic HTTP server, such as cookies, HTTP authentication, or TLS + authentication. + +None of these three mechanisms works well in practice. Using cookies is +cumbersome, HTTP authentication isn't supported by all mainstream browsers, +and TLS authentication in a browser is an esoteric user experience. + +Fortunately, there are better alternatives! Let's discuss them. + +System design +------------- + +Consider a setup where the WebSocket server is separate from the HTTP server. + +Most servers built with websockets to complement a web application adopt this +design because websockets doesn't aim at supporting HTTP. + +The following diagram illustrates the authentication flow. + +.. image:: authentication.svg + +Assuming the current user is authenticated with the HTTP server (1), the +application needs to obtain credentials from the HTTP server (2) in order to +send them to the WebSocket server (3), who can check them against the database +of user accounts (4). + +Usernames and passwords aren't a good choice of credentials here, if only +because passwords aren't available in clear text in the database. + +Tokens linked to user accounts are a better choice. These tokens must be +impossible to forge by an attacker. For additional security, they can be +short-lived or even single-use. + +Sending credentials +------------------- + +Assume the web application obtained authentication credentials, likely a +token, from the HTTP server. There's four options for passing them to the +WebSocket server. + +1. **Sending credentials as the first message in the WebSocket connection.** + + This is fully reliable and the most secure mechanism in this discussion. It + has two minor downsides: + + * Authentication is performed at the application layer. Ideally, it would + be managed at the protocol layer. + + * Authentication is performed after the WebSocket handshake, making it + impossible to monitor authentication failures with HTTP response codes. + +2. **Adding credentials to the WebSocket URI in a query parameter.** + + This is also fully reliable but less secure. Indeed, it has a major + downside: + + * URIs end up in logs, which leaks credentials. Even if that risk could be + lowered with single-use tokens, it is usually considered unacceptable. + + Authentication is still performed at the application layer but it can + happen before the WebSocket handshake, which improves separation of + concerns and enables responding to authentication failures with HTTP 401. + +3. **Setting a cookie on the domain of the WebSocket URI.** + + Cookies are undoubtedly the most common and hardened mechanism for sending + credentials from a web application to a server. In an HTTP application, + credentials would be a session identifier or a serialized, signed session. + + Unfortunately, when the WebSocket server runs on a different domain from + the web application, this idea bumps into the `Same-Origin Policy`_. For + security reasons, setting a cookie on a different origin is impossible. + + The proper workaround consists in: + + * creating a hidden iframe_ served from the domain of the WebSocket server + * sending the token to the iframe with postMessage_ + * setting the cookie in the iframe + + before opening the WebSocket connection. + + Sharing a parent domain (e.g. example.com) between the HTTP server (e.g. + www.example.com) and the WebSocket server (e.g. ws.example.com) and setting + the cookie on that parent domain would work too. + + However, the cookie would be shared with all subdomains of the parent + domain. For a cookie containing credentials, this is unacceptable. + +.. _Same-Origin Policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy +.. _iframe: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe +.. _postMessage: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage + +4. **Adding credentials to the WebSocket URI in user information.** + + Letting the browser perform HTTP Basic Auth is a nice idea in theory. + + In practice it doesn't work due to poor support in browsers. + + As of May 2021: + + * Chrome 90 behaves as expected. + + * Firefox 88 caches credentials too aggressively. + + When connecting again to the same server with new credentials, it reuses + the old credentials, which may be expired, resulting in an HTTP 401. Then + the next connection succeeds. Perhaps errors clear the cache. + + When tokens are short-lived or single-use, this bug produces an + interesting effect: every other WebSocket connection fails. + + * Safari 14 ignores credentials entirely. + +Two other options are off the table: + +1. **Setting a custom HTTP header** + + This would be the most elegant mechanism, solving all issues with the options + discussed above. + + Unfortunately, it doesn't work because the `WebSocket API`_ doesn't support + `setting custom headers`_. + +.. _WebSocket API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API +.. _setting custom headers: https://github.com/whatwg/html/issues/3062 + +2. **Authenticating with a TLS certificate** + + While this is suggested by the RFC, installing a TLS certificate is too far + from the mainstream experience of browser users. This could make sense in + high security contexts. I hope developers working on such projects don't + take security advice from the documentation of random open source projects. + +Let's experiment! +----------------- + +The `experiments/authentication`_ directory demonstrates these techniques. + +Run the experiment in an environment where websockets is installed: + +.. _experiments/authentication: https://github.com/python-websockets/websockets/tree/main/experiments/authentication + +.. code-block:: console + + $ python experiments/authentication/app.py + Running on http://localhost:8000/ + +When you browse to the HTTP server at http://localhost:8000/ and you submit a +username, the server creates a token and returns a testing web page. + +This page opens WebSocket connections to four WebSocket servers running on +four different origins. It attempts to authenticate with the token in four +different ways. + +First message +............. + +As soon as the connection is open, the client sends a message containing the +token: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://.../"); + websocket.onopen = () => websocket.send(token); + + // ... + +At the beginning of the connection handler, the server receives this message +and authenticates the user. If authentication fails, the server closes the +connection: + +.. code-block:: python + + async def first_message_handler(websocket): + token = await websocket.recv() + user = get_user(token) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + ... + +Query parameter +............... + +The client adds the token to the WebSocket URI in a query parameter before +opening the connection: + +.. code-block:: javascript + + const uri = `ws://.../?token=${token}`; + const websocket = new WebSocket(uri); + + // ... + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class QueryParamProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + token = get_query_parameter(path, "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + async def query_param_handler(websocket): + user = websocket.user + + ... + +Cookie +...... + +The client sets a cookie containing the token before opening the connection. + +The cookie must be set by an iframe loaded from the same origin as the +WebSocket server. This requires passing the token to this iframe. + +.. code-block:: javascript + + // in main window + iframe.contentWindow.postMessage(token, "http://..."); + + // in iframe + document.cookie = `token=${data}; SameSite=Strict`; + + // in main window + const websocket = new WebSocket("ws://.../"); + + // ... + +This sequence must be synchronized between the main window and the iframe. +This involves several events. Look at the full implementation for details. + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class CookieProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + # Serve iframe on non-WebSocket requests + ... + + token = get_cookie(headers.get("Cookie", ""), "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + async def cookie_handler(websocket): + user = websocket.user + + ... + +User information +................ + +The client adds the token to the WebSocket URI in user information before +opening the connection: + +.. code-block:: javascript + + const uri = `ws://token:${token}@.../`; + const websocket = new WebSocket(uri); + + // ... + +Since HTTP Basic Auth is designed to accept a username and a password rather +than a token, we send ``token`` as username and the token as password. + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + if username != "token": + return False + + user = get_user(password) + if user is None: + return False + + self.user = user + return True + + async def user_info_handler(websocket): + user = websocket.user + + ... + +Machine-to-machine authentication +--------------------------------- + +When the WebSocket client is a standalone program rather than a script running +in a browser, there are far fewer constraints. HTTP Authentication is the best +solution in this scenario. + +To authenticate a websockets client with HTTP Basic Authentication +(:rfc:`7617`), include the credentials in the URI: + +.. code-block:: python + + async with websockets.connect( + f"wss://{username}:{password}@example.com", + ) as websocket: + ... + +(You must :func:`~urllib.parse.quote` ``username`` and ``password`` if they +contain unsafe characters.) + +To authenticate a websockets client with HTTP Bearer Authentication +(:rfc:`6750`), add a suitable ``Authorization`` header: + +.. code-block:: python + + async with websockets.connect( + "wss://example.com", + extra_headers={"Authorization": f"Bearer {token}"} + ) as websocket: + ... diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg new file mode 100644 index 0000000000..79b5fb8d56 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-514 74 644 380" width="644px" height="380px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-504" y="228" width="192" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="228" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-504" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-492" font-family="DIN Next, sans-serif" height="48"><tspan x="-408" y="258" text-anchor="middle" style="white-space:pre;">HTTP</tspan><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-72" y="228" width="192" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="228" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-72" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-60" font-family="DIN Next, sans-serif" height="48"><tspan x="24" y="258" text-anchor="middle" style="white-space:pre;">WebSocket</tspan><tspan x="24" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-288" y="84" width="192" height="72" rx="3" ry="3" fill="#d9dde0"/><rect y="84" rx="3" stroke="#4B5C6B" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-288" ry="3" height="72"/><text y="96" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="48"><tspan x="-192" y="114" text-anchor="middle" style="white-space:pre;">web app</tspan><tspan x="-192" y="138" text-anchor="middle" style="white-space:pre;">in browser</tspan></text></g></g><g><g><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="#e2cdf2"/><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="none" stroke="#730FC3" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="396" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="24"><tspan x="-192" y="414" text-anchor="middle" style="white-space:pre;">user accounts</tspan></text></g></g><g><g><path d="M-400.46606,302.69069l37.26606,13.30931M-284.8,344l33.4991,11.96396" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-252.34924,349.70583 -247.53394,357.30931 -256.07648,360.14213" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-404" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="336" text-anchor="middle" style="white-space:pre;">(1) authenticate user</tspan></text></g></g><g><g><path d="M-247.35316,159.15135l-43.98017,18.84865M-356.66667,206l-40.30359,17.27297" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-391.94549,227.14787 -400.64684,224.84865 -396.31086,216.96199" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="164" xml:space="preserve" x="-406" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="198" text-anchor="middle" style="white-space:pre;">(2) obtain credentials</tspan></text></g></g><g><g><path d="M-136.64684,159.15135l43.98017,18.84865M-27.33333,206l40.30359,17.27297" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="12.31086,216.96199 16.64684,224.84865 7.94549,227.14787" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="153" xml:space="preserve" x="-136.5" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="198" text-anchor="middle" style="white-space:pre;">(3) send credentials</tspan></text></g></g><g><g><path d="M16.46606,302.69069l-37.26606,13.30931M-99.2,344l-33.4991,11.96396" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-127.92352,360.14213 -136.46606,357.30931 -131.65076,349.70583" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-140" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="336" text-anchor="middle" style="white-space:pre;">(4) authenticate user</tspan></text></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst new file mode 100644 index 0000000000..1acb372d4f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst @@ -0,0 +1,348 @@ +Broadcasting messages +===================== + +.. currentmodule:: websockets + + +.. admonition:: If you just want to send a message to all connected clients, + use :func:`broadcast`. + :class: tip + + If you want to learn about its design in depth, continue reading this + document. + +WebSocket servers often send the same message to all connected clients or to a +subset of clients for which the message is relevant. + +Let's explore options for broadcasting a message, explain the design +of :func:`broadcast`, and discuss alternatives. + +For each option, we'll provide a connection handler called ``handler()`` and a +function or coroutine called ``broadcast()`` that sends a message to all +connected clients. + +Integrating them is left as an exercise for the reader. You could start with:: + + import asyncio + import websockets + + async def handler(websocket): + ... + + async def broadcast(message): + ... + + async def broadcast_messages(): + while True: + await asyncio.sleep(1) + message = ... # your application logic goes here + await broadcast(message) + + async def main(): + async with websockets.serve(handler, "localhost", 8765): + await broadcast_messages() # runs forever + + if __name__ == "__main__": + asyncio.run(main()) + +``broadcast_messages()`` must yield control to the event loop between each +message, or else it will never let the server run. That's why it includes +``await asyncio.sleep(1)``. + +A complete example is available in the `experiments/broadcast`_ directory. + +.. _experiments/broadcast: https://github.com/python-websockets/websockets/tree/main/experiments/broadcast + +The naive way +------------- + +The most obvious way to send a message to all connected clients consists in +keeping track of them and sending the message to each of them. + +Here's a connection handler that registers clients in a global variable:: + + CLIENTS = set() + + async def handler(websocket): + CLIENTS.add(websocket) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(websocket) + +This implementation assumes that the client will never send any messages. If +you'd rather not make this assumption, you can change:: + + await websocket.wait_closed() + +to:: + + async for _ in websocket: + pass + +Here's a coroutine that broadcasts a message to all clients:: + + async def broadcast(message): + for websocket in CLIENTS.copy(): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + +There are two tricks in this version of ``broadcast()``. + +First, it makes a copy of ``CLIENTS`` before iterating it. Else, if a client +connects or disconnects while ``broadcast()`` is running, the loop would fail +with:: + + RuntimeError: Set changed size during iteration + +Second, it ignores :exc:`~exceptions.ConnectionClosed` exceptions because a +client could disconnect between the moment ``broadcast()`` makes a copy of +``CLIENTS`` and the moment it sends a message to this client. This is fine: a +client that disconnected doesn't belongs to "all connected clients" anymore. + +The naive way can be very fast. Indeed, if all connections have enough free +space in their write buffers, ``await websocket.send(message)`` writes the +message and returns immediately, as it doesn't need to wait for the buffer to +drain. In this case, ``broadcast()`` doesn't yield control to the event loop, +which minimizes overhead. + +The naive way can also fail badly. If the write buffer of a connection reaches +``write_limit``, ``broadcast()`` waits for the buffer to drain before sending +the message to other clients. This can cause a massive drop in performance. + +As a consequence, this pattern works only when write buffers never fill up, +which is usually outside of the control of the server. + +If you know for sure that you will never write more than ``write_limit`` bytes +within ``ping_interval + ping_timeout``, then websockets will terminate slow +connections before the write buffer has time to fill up. + +Don't set extreme ``write_limit``, ``ping_interval``, and ``ping_timeout`` +values to ensure that this condition holds. Set reasonable values and use the +built-in :func:`broadcast` function instead. + +The concurrent way +------------------ + +The naive way didn't work well because it serialized writes, while the whole +point of asynchronous I/O is to perform I/O concurrently. + +Let's modify ``broadcast()`` to send messages concurrently:: + + async def send(websocket, message): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + + def broadcast(message): + for websocket in CLIENTS: + asyncio.create_task(send(websocket, message)) + +We move the error handling logic in a new coroutine and we schedule +a :class:`~asyncio.Task` to run it instead of executing it immediately. + +Since ``broadcast()`` no longer awaits coroutines, we can make it a function +rather than a coroutine and do away with the copy of ``CLIENTS``. + +This version of ``broadcast()`` makes clients independent from one another: a +slow client won't block others. As a side effect, it makes messages +independent from one another. + +If you broadcast several messages, there is no strong guarantee that they will +be sent in the expected order. Fortunately, the event loop runs tasks in the +order in which they are created, so the order is correct in practice. + +Technically, this is an implementation detail of the event loop. However, it +seems unlikely for an event loop to run tasks in an order other than FIFO. + +If you wanted to enforce the order without relying this implementation detail, +you could be tempted to wait until all clients have received the message:: + + async def broadcast(message): + if CLIENTS: # asyncio.wait doesn't accept an empty list + await asyncio.wait([ + asyncio.create_task(send(websocket, message)) + for websocket in CLIENTS + ]) + +However, this doesn't really work in practice. Quite often, it will block +until the slowest client times out. + +Backpressure meets broadcast +---------------------------- + +At this point, it becomes apparent that backpressure, usually a good practice, +doesn't work well when broadcasting a message to thousands of clients. + +When you're sending messages to a single client, you don't want to send them +faster than the network can transfer them and the client accept them. This is +why :meth:`~server.WebSocketServerProtocol.send` checks if the write buffer +is full and, if it is, waits until it drain, giving the network and the +client time to catch up. This provides backpressure. + +Without backpressure, you could pile up data in the write buffer until the +server process runs out of memory and the operating system kills it. + +The :meth:`~server.WebSocketServerProtocol.send` API is designed to enforce +backpressure by default. This helps users of websockets write robust programs +even if they never heard about backpressure. + +For comparison, :class:`asyncio.StreamWriter` requires users to understand +backpressure and to await :meth:`~asyncio.StreamWriter.drain` explicitly +after each :meth:`~asyncio.StreamWriter.write`. + +When broadcasting messages, backpressure consists in slowing down all clients +in an attempt to let the slowest client catch up. With thousands of clients, +the slowest one is probably timing out and isn't going to receive the message +anyway. So it doesn't make sense to synchronize with the slowest client. + +How do we avoid running out of memory when slow clients can't keep up with the +broadcast rate, then? The most straightforward option is to disconnect them. + +If a client gets too far behind, eventually it reaches the limit defined by +``ping_timeout`` and websockets terminates the connection. You can read the +discussion of :doc:`keepalive and timeouts <./timeouts>` for details. + +How :func:`broadcast` works +--------------------------- + +The built-in :func:`broadcast` function is similar to the naive way. The main +difference is that it doesn't apply backpressure. + +This provides the best performance by avoiding the overhead of scheduling and +running one task per client. + +Also, when sending text messages, encoding to UTF-8 happens only once rather +than once per client, providing a small performance gain. + +Per-client queues +----------------- + +At this point, we deal with slow clients rather brutally: we disconnect then. + +Can we do better? For example, we could decide to skip or to batch messages, +depending on how far behind a client is. + +To implement this logic, we can create a queue of messages for each client and +run a task that gets messages from the queue and sends them to the client:: + + import asyncio + + CLIENTS = set() + + async def relay(queue, websocket): + while True: + # Implement custom logic based on queue.qsize() and + # websocket.transport.get_write_buffer_size() here. + message = await queue.get() + await websocket.send(message) + + async def handler(websocket): + queue = asyncio.Queue() + relay_task = asyncio.create_task(relay(queue, websocket)) + CLIENTS.add(queue) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(queue) + relay_task.cancel() + +Then we can broadcast a message by pushing it to all queues:: + + def broadcast(message): + for queue in CLIENTS: + queue.put_nowait(message) + +The queues provide an additional buffer between the ``broadcast()`` function +and clients. This makes it easier to support slow clients without excessive +memory usage because queued messages aren't duplicated to write buffers +until ``relay()`` processes them. + +Publish–subscribe +----------------- + +Can we avoid centralizing the list of connected clients in a global variable? + +If each client subscribes to a stream a messages, then broadcasting becomes as +simple as publishing a message to the stream. + +Here's a message stream that supports multiple consumers:: + + class PubSub: + def __init__(self): + self.waiter = asyncio.Future() + + def publish(self, value): + waiter, self.waiter = self.waiter, asyncio.Future() + waiter.set_result((value, self.waiter)) + + async def subscribe(self): + waiter = self.waiter + while True: + value, waiter = await waiter + yield value + + __aiter__ = subscribe + + PUBSUB = PubSub() + +The stream is implemented as a linked list of futures. It isn't necessary to +synchronize consumers. They can read the stream at their own pace, +independently from one another. Once all consumers read a message, there are +no references left, therefore the garbage collector deletes it. + +The connection handler subscribes to the stream and sends messages:: + + async def handler(websocket): + async for message in PUBSUB: + await websocket.send(message) + +The broadcast function publishes to the stream:: + + def broadcast(message): + PUBSUB.publish(message) + +Like per-client queues, this version supports slow clients with limited memory +usage. Unlike per-client queues, it makes it difficult to tell how far behind +a client is. The ``PubSub`` class could be extended or refactored to provide +this information. + +The ``for`` loop is gone from this version of the ``broadcast()`` function. +However, there's still a ``for`` loop iterating on all clients hidden deep +inside :mod:`asyncio`. When ``publish()`` sets the result of the ``waiter`` +future, :mod:`asyncio` loops on callbacks registered with this future and +schedules them. This is how connection handlers receive the next value from +the asynchronous iterator returned by ``subscribe()``. + +Performance considerations +-------------------------- + +The built-in :func:`broadcast` function sends all messages without yielding +control to the event loop. So does the naive way when the network and clients +are fast and reliable. + +For each client, a WebSocket frame is prepared and sent to the network. This +is the minimum amount of work required to broadcast a message. + +It would be tempting to prepare a frame and reuse it for all connections. +However, this isn't possible in general for two reasons: + +* Clients can negotiate different extensions. You would have to enforce the + same extensions with the same parameters. For example, you would have to + select some compression settings and reject clients that cannot support + these settings. + +* Extensions can be stateful, producing different encodings of the same + message depending on previous messages. For example, you would have to + disable context takeover to make compression stateless, resulting in poor + compression rates. + +All other patterns discussed above yield control to the event loop once per +client because messages are sent by different tasks. This makes them slower +than the built-in :func:`broadcast` function. + +There is no major difference between the performance of per-client queues and +publish–subscribe. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst new file mode 100644 index 0000000000..eaf99070db --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst @@ -0,0 +1,222 @@ +Compression +=========== + +.. currentmodule:: websockets.extensions.permessage_deflate + +Most WebSocket servers exchange JSON messages because they're convenient to +parse and serialize in a browser. These messages contain text data and tend to +be repetitive. + +This makes the stream of messages highly compressible. Enabling compression +can reduce network traffic by more than 80%. + +There's a standard for compressing messages. :rfc:`7692` defines WebSocket +Per-Message Deflate, a compression extension based on the Deflate_ algorithm. + +.. _Deflate: https://en.wikipedia.org/wiki/Deflate + +Configuring compression +----------------------- + +:func:`~websockets.client.connect` and :func:`~websockets.server.serve` enable +compression by default because the reduction in network bandwidth is usually +worth the additional memory and CPU cost. + +If you want to disable compression, set ``compression=None``:: + + import websockets + + websockets.connect(..., compression=None) + + websockets.serve(..., compression=None) + +If you want to customize compression settings, you can enable the Per-Message +Deflate extension explicitly with :class:`ClientPerMessageDeflateFactory` or +:class:`ServerPerMessageDeflateFactory`:: + + import websockets + from websockets.extensions import permessage_deflate + + websockets.connect( + ..., + extensions=[ + permessage_deflate.ClientPerMessageDeflateFactory( + server_max_window_bits=11, + client_max_window_bits=11, + compress_settings={"memLevel": 4}, + ), + ], + ) + + websockets.serve( + ..., + extensions=[ + permessage_deflate.ServerPerMessageDeflateFactory( + server_max_window_bits=11, + client_max_window_bits=11, + compress_settings={"memLevel": 4}, + ), + ], + ) + +The Window Bits and Memory Level values in these examples reduce memory usage +at the expense of compression rate. + +Compression settings +-------------------- + +When a client and a server enable the Per-Message Deflate extension, they +negotiate two parameters to guarantee compatibility between compression and +decompression. These parameters affect the trade-off between compression rate +and memory usage for both sides. + +* **Context Takeover** means that the compression context is retained between + messages. In other words, compression is applied to the stream of messages + rather than to each message individually. + + Context takeover should remain enabled to get good performance on + applications that send a stream of messages with similar structure, + that is, most applications. + + This requires retaining the compression context and state between messages, + which increases the memory footprint of a connection. + +* **Window Bits** controls the size of the compression context. It must be + an integer between 9 (lowest memory usage) and 15 (best compression). + Setting it to 8 is possible but rejected by some versions of zlib. + + On the server side, websockets defaults to 12. Specifically, the compression + window size (server to client) is always 12 while the decompression window + (client to server) size may be 12 or 15 depending on whether the client + supports configuring it. + + On the client side, websockets lets the server pick a suitable value, which + has the same effect as defaulting to 15. + +:mod:`zlib` offers additional parameters for tuning compression. They control +the trade-off between compression rate, memory usage, and CPU usage only for +compressing. They're transparent for decompressing. Unless mentioned +otherwise, websockets inherits defaults of :func:`~zlib.compressobj`. + +* **Memory Level** controls the size of the compression state. It must be an + integer between 1 (lowest memory usage) and 9 (best compression). + + websockets defaults to 5. This is lower than zlib's default of 8. Not only + does a lower memory level reduce memory usage, but it can also increase + speed thanks to memory locality. + +* **Compression Level** controls the effort to optimize compression. It must + be an integer between 1 (lowest CPU usage) and 9 (best compression). + +* **Strategy** selects the compression strategy. The best choice depends on + the type of data being compressed. + + +Tuning compression +------------------ + +For servers +........... + +By default, websockets enables compression with conservative settings that +optimize memory usage at the cost of a slightly worse compression rate: +Window Bits = 12 and Memory Level = 5. This strikes a good balance for small +messages that are typical of WebSocket servers. + +Here's how various compression settings affect memory usage of a single +connection on a 64-bit system, as well a benchmark of compressed size and +compression time for a corpus of small JSON documents. + +=========== ============ ============ ================ ================ +Window Bits Memory Level Memory usage Size vs. default Time vs. default +=========== ============ ============ ================ ================ +15 8 322 KiB -4.0% +15% +14 7 178 KiB -2.6% +10% +13 6 106 KiB -1.4% +5% +**12** **5** **70 KiB** **=** **=** +11 4 52 KiB +3.7% -5% +10 3 43 KiB +90% +50% +9 2 39 KiB +160% +100% +— — 19 KiB +452% — +=========== ============ ============ ================ ================ + +Window Bits and Memory Level don't have to move in lockstep. However, other +combinations don't yield significantly better results than those shown above. + +Compressed size and compression time depend heavily on the kind of messages +exchanged by the application so this example may not apply to your use case. + +You can adapt `compression/benchmark.py`_ by creating a list of typical +messages and passing it to the ``_run`` function. + +Window Bits = 11 and Memory Level = 4 looks like the sweet spot in this table. + +websockets defaults to Window Bits = 12 and Memory Level = 5 to stay away from +Window Bits = 10 or Memory Level = 3 where performance craters, raising doubts +on what could happen at Window Bits = 11 and Memory Level = 4 on a different +corpus. + +Defaults must be safe for all applications, hence a more conservative choice. + +.. _compression/benchmark.py: https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py + +The benchmark focuses on compression because it's more expensive than +decompression. Indeed, leaving aside small allocations, theoretical memory +usage is: + +* ``(1 << (windowBits + 2)) + (1 << (memLevel + 9))`` for compression; +* ``1 << windowBits`` for decompression. + +CPU usage is also higher for compression than decompression. + +While it's always possible for a server to use a smaller window size for +compressing outgoing messages, using a smaller window size for decompressing +incoming messages requires collaboration from clients. + +When a client doesn't support configuring the size of its compression window, +websockets enables compression with the largest possible decompression window. +In most use cases, this is more efficient than disabling compression both ways. + +If you are very sensitive to memory usage, you can reverse this behavior by +setting the ``require_client_max_window_bits`` parameter of +:class:`ServerPerMessageDeflateFactory` to ``True``. + +For clients +........... + +By default, websockets enables compression with Memory Level = 5 but leaves +the Window Bits setting up to the server. + +There's two good reasons and one bad reason for not optimizing the client side +like the server side: + +1. If the maintainers of a server configured some optimized settings, we don't + want to override them with more restrictive settings. + +2. Optimizing memory usage doesn't matter very much for clients because it's + uncommon to open thousands of client connections in a program. + +3. On a more pragmatic note, some servers misbehave badly when a client + configures compression settings. `AWS API Gateway`_ is the worst offender. + + .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065 + + Unfortunately, even though websockets is right and AWS is wrong, many users + jump to the conclusion that websockets doesn't work. + + Until the ecosystem levels up, interoperability with buggy servers seems + more valuable than optimizing memory usage. + + +Further reading +--------------- + +This `blog post by Ilya Grigorik`_ provides more details about how compression +settings affect memory usage and how to optimize them. + +.. _blog post by Ilya Grigorik: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/ + +This `experiment by Peter Thorson`_ recommends Window Bits = 11 and Memory +Level = 4 for optimizing memory usage. + +.. _experiment by Peter Thorson: https://mailarchive.ietf.org/arch/msg/hybi/F9t4uPufVEy8KBLuL36cZjCmM_Y/ diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg new file mode 100644 index 0000000000..22a198ed83 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-382 -214 500 464" width="500px" height="464px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-372" y="-12" width="480" height="72" rx="3" ry="3" fill="#f6d8dd"/><rect y="-12" rx="3" stroke="#D3455B" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="30" text-anchor="middle" style="white-space:pre;">Integration layer</tspan></text></g></g><g><g><rect x="-372" y="168" width="480" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="168" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="192" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="210" text-anchor="middle" style="white-space:pre;">Sans-I/O layer</tspan></text></g></g><g><g><rect x="-372" y="-192" width="216" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="-192" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="216" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-264" y="-150" text-anchor="middle" style="white-space:pre;">Application</tspan></text></g></g><g><g><path d="M-324,-20v-20M-324,-92v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,-104.9079 -324,-112 -318.45905,-104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-363.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="-72" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M-204,-112l0,20M-204,-40v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,-27.0921 -204,-20 -209.54095,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-243.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="#e3e6e9"/><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-96" font-family="DIN Next, sans-serif" height="24"><tspan x="0" y="-150" text-anchor="middle" style="white-space:pre;">Network</tspan></text></g></g><g><g><path d="M-60,-20v-20M-60,-92v-15.53727" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,-104.44518 -60,-111.53727 -54.45905,-104.44518" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="38" xml:space="preserve" x="-79" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="-48" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,-108.78722v18.78722M60,-38v14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,-27.0921 60,-20 54.45905,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-88" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="-70" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="-46" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,68v20M60,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,152.9079 60,160 54.45905,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="108" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-60,160v-20M-60,88v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,75.0921 -60,68 -54.45905,75.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="42" xml:space="preserve" x="-81" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-212,42" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-212,42 -212,42 -212,42" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><path d="M-204,68v20M-204,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,152.9079 -204,160 -209.54095,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="52" xml:space="preserve" x="-230" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="132" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g><g><g><path d="M-324,160l0,-18M-324,90v-14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,79.0921 -324,72 -318.45905,79.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="92" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="-352" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="110" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="134" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst new file mode 100644 index 0000000000..2a1fe9a785 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst @@ -0,0 +1,181 @@ +Deployment +========== + +.. currentmodule:: websockets + +When you deploy your websockets server to production, at a high level, your +architecture will almost certainly look like the following diagram: + +.. image:: deployment.svg + +The basic unit for scaling a websockets server is "one server process". Each +blue box in the diagram represents one server process. + +There's more variation in routing. While the routing layer is shown as one big +box, it is likely to involve several subsystems. + +When you design a deployment, your should consider two questions: + +1. How will I run the appropriate number of server processes? +2. How will I route incoming connections to these processes? + +These questions are strongly related. There's a wide range of acceptable +answers, depending on your goals and your constraints. + +You can find a few concrete examples in the :ref:`deployment how-to guides +<deployment-howto>`. + +Running server processes +------------------------ + +How many processes do I need? +............................. + +Typically, one server process will manage a few hundreds or thousands +connections, depending on the frequency of messages and the amount of work +they require. + +CPU and memory usage increase with the number of connections to the server. + +Often CPU is the limiting factor. If a server process goes to 100% CPU, then +you reached the limit. How much headroom you want to keep is up to you. + +Once you know how many connections a server process can manage and how many +connections you need to handle, you can calculate how many processes to run. + +You can also automate this calculation by configuring an autoscaler to keep +CPU usage or connection count within acceptable limits. + +Don't scale with threads. Threads doesn't make sense for a server built with +:mod:`asyncio`. + +How do I run processes? +....................... + +Most solutions for running multiple instances of a server process fall into +one of these three buckets: + +1. Running N processes on a platform: + + * a Kubernetes Deployment + + * its equivalent on a Platform as a Service provider + +2. Running N servers: + + * an AWS Auto Scaling group, a GCP Managed instance group, etc. + + * a fixed set of long-lived servers + +3. Running N processes on a server: + + * preferably via a process manager or supervisor + +Option 1 is easiest of you have access to such a platform. + +Option 2 almost always combines with option 3. + +How do I start a process? +......................... + +Run a Python program that invokes :func:`~server.serve`. That's it. + +Don't run an ASGI server such as Uvicorn, Hypercorn, or Daphne. They're +alternatives to websockets, not complements. + +Don't run a WSGI server such as Gunicorn, Waitress, or mod_wsgi. They aren't +designed to run WebSocket applications. + +Applications servers handle network connections and expose a Python API. You +don't need one because websockets handles network connections directly. + +How do I stop a process? +........................ + +Process managers send the SIGTERM signal to terminate processes. Catch this +signal and exit the server to ensure a graceful shutdown. + +Here's an example: + +.. literalinclude:: ../../example/faq/shutdown_server.py + :emphasize-lines: 12-15,18 + +When exiting the context manager, :func:`~server.serve` closes all connections +with code 1001 (going away). As a consequence: + +* If the connection handler is awaiting + :meth:`~server.WebSocketServerProtocol.recv`, it receives a + :exc:`~exceptions.ConnectionClosedOK` exception. It can catch the exception + and clean up before exiting. + +* Otherwise, it should be waiting on + :meth:`~server.WebSocketServerProtocol.wait_closed`, so it can receive the + :exc:`~exceptions.ConnectionClosedOK` exception and exit. + +This example is easily adapted to handle other signals. + +If you override the default signal handler for SIGINT, which raises +:exc:`KeyboardInterrupt`, be aware that you won't be able to interrupt a +program with Ctrl-C anymore when it's stuck in a loop. + +Routing connections +------------------- + +What does routing involve? +.......................... + +Since the routing layer is directly exposed to the Internet, it should provide +appropriate protection against threats ranging from Internet background noise +to targeted attacks. + +You should always secure WebSocket connections with TLS. Since the routing +layer carries the public domain name, it should terminate TLS connections. + +Finally, it must route connections to the server processes, balancing new +connections across them. + +How do I route connections? +........................... + +Here are typical solutions for load balancing, matched to ways of running +processes: + +1. If you're running on a platform, it comes with a routing layer: + + * a Kubernetes Ingress and Service + + * a service mesh: Istio, Consul, Linkerd, etc. + + * the routing mesh of a Platform as a Service + +2. If you're running N servers, you may load balance with: + + * a cloud load balancer: AWS Elastic Load Balancing, GCP Cloud Load + Balancing, etc. + + * A software load balancer: HAProxy, NGINX, etc. + +3. If you're running N processes on a server, you may load balance with: + + * A software load balancer: HAProxy, NGINX, etc. + + * The operating system — all processes listen on the same port + +You may trust the load balancer to handle encryption and to provide security. +You may add another layer in front of the load balancer for these purposes. + +There are many possibilities. Don't add layers that you don't need, though. + +How do I implement a health check? +.................................. + +Load balancers need a way to check whether server processes are up and running +to avoid routing connections to a non-functional backend. + +websockets provide minimal support for responding to HTTP requests with the +:meth:`~server.WebSocketServerProtocol.process_request` hook. + +Here's an example: + +.. literalinclude:: ../../example/faq/health_check_server.py + :emphasize-lines: 7-9,18 diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg new file mode 100644 index 0000000000..cb948d8d6b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-490 -34 644 356" width="644px" height="356px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="#e3e6e9"/><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="30" text-anchor="middle" style="white-space:pre;">Internet</tspan></text></g></g><g><g><rect x="-480" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-312" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-312" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-300" font-family="DIN Next, sans-serif" height="24"><tspan x="-240" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="0" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="0" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="12" font-family="DIN Next, sans-serif" height="24"><tspan x="72" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-144" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-144" ry="1.5" height="36"/></g></g><g><g><rect x="-48" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-48" ry="1.5" height="36"/></g></g><g><g><rect x="-96" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-96" ry="1.5" height="36"/></g></g><g><g><rect x="-480" y="120" width="624" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="120" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="624" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="144" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="162" text-anchor="middle" style="white-space:pre;">routing</tspan></text></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-240" y2="227.99999999999994" x2="-240"/><polygon points="-234.45905,224.9079 -240,232 -245.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-407.99999999999994" y2="227.99999999999994" x2="-407.99999999999994"/><polygon points="-402.45905,224.9079 -408,232 -413.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="200.00000000000003" stroke-linecap="round" stroke-width="4" x1="72" y2="228" x2="72"/><polygon points="77.54095,224.9079 72,232 66.45905,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="79.92376615249583" stroke-linecap="round" stroke-width="4" x1="-168" y2="107.99999999999994" x2="-168"/><polygon points="-162.45905,104.9079 -168,112 -173.54095,104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst new file mode 100644 index 0000000000..f164d29905 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst @@ -0,0 +1,572 @@ +Design +====== + +.. currentmodule:: websockets + +This document describes the design of websockets. It assumes familiarity with +the specification of the WebSocket protocol in :rfc:`6455`. + +It's primarily intended at maintainers. It may also be useful for users who +wish to understand what happens under the hood. + +.. warning:: + + Internals described in this document may change at any time. + + Backwards compatibility is only guaranteed for :doc:`public APIs + <../reference/index>`. + +Lifecycle +--------- + +State +..... + +WebSocket connections go through a trivial state machine: + +- ``CONNECTING``: initial state, +- ``OPEN``: when the opening handshake is complete, +- ``CLOSING``: when the closing handshake is started, +- ``CLOSED``: when the TCP connection is closed. + +Transitions happen in the following places: + +- ``CONNECTING -> OPEN``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` which runs when + the :ref:`opening handshake <opening-handshake>` completes and the WebSocket + connection is established — not to be confused with + :meth:`~asyncio.BaseProtocol.connection_made` which runs when the TCP connection + is established; +- ``OPEN -> CLOSING``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.write_frame` immediately before + sending a close frame; since receiving a close frame triggers sending a + close frame, this does the right thing regardless of which side started the + :ref:`closing handshake <closing-handshake>`; also in + :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` which duplicates + a few lines of code from ``write_close_frame()`` and ``write_frame()``; +- ``* -> CLOSED``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_lost` which is always + called exactly once when the TCP connection is closed. + +Coroutines +.......... + +The following diagram shows which coroutines are running at each stage of the +connection lifecycle on the client side. + +.. image:: lifecycle.svg + :target: _images/lifecycle.svg + +The lifecycle is identical on the server side, except inversion of control +makes the equivalent of :meth:`~client.connect` implicit. + +Coroutines shown in green are called by the application. Multiple coroutines +may interact with the WebSocket connection concurrently. + +Coroutines shown in gray manage the connection. When the opening handshake +succeeds, :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` starts +two tasks: + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.transfer_data` which handles + incoming data and lets :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` + consume it. It may be canceled to terminate the connection. It never exits + with an exception other than :exc:`~asyncio.CancelledError`. See :ref:`data + transfer <data-transfer>` below. + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping` which sends Ping + frames at regular intervals and ensures that corresponding Pong frames are + received. It is canceled when the connection terminates. It never exits + with an exception other than :exc:`~asyncio.CancelledError`. + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.close_connection` which waits for + the data transfer to terminate, then takes care of closing the TCP + connection. It must not be canceled. It never exits with an exception. See + :ref:`connection termination <connection-termination>` below. + +Besides, :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` starts +the same :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` when +the opening handshake fails, in order to close the TCP connection. + +Splitting the responsibilities between two tasks makes it easier to guarantee +that websockets can terminate connections: + +- within a fixed timeout, +- without leaking pending tasks, +- without leaking open TCP connections, + +regardless of whether the connection terminates normally or abnormally. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` completes when no +more data will be received on the connection. Under normal circumstances, it +exits after exchanging close frames. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` completes when +the TCP connection is closed. + + +.. _opening-handshake: + +Opening handshake +----------------- + +websockets performs the opening handshake when establishing a WebSocket +connection. On the client side, :meth:`~client.connect` executes it +before returning the protocol to the caller. On the server side, it's executed +before passing the protocol to the ``ws_handler`` coroutine handling the +connection. + +While the opening handshake is asymmetrical — the client sends an HTTP Upgrade +request and the server replies with an HTTP Switching Protocols response — +websockets aims at keeping the implementation of both sides consistent with +one another. + +On the client side, :meth:`~client.WebSocketClientProtocol.handshake`: + +- builds an HTTP request based on the ``uri`` and parameters passed to + :meth:`~client.connect`; +- writes the HTTP request to the network; +- reads an HTTP response from the network; +- checks the HTTP response, validates ``extensions`` and ``subprotocol``, and + configures the protocol accordingly; +- moves to the ``OPEN`` state. + +On the server side, :meth:`~server.WebSocketServerProtocol.handshake`: + +- reads an HTTP request from the network; +- calls :meth:`~server.WebSocketServerProtocol.process_request` which may + abort the WebSocket handshake and return an HTTP response instead; this + hook only makes sense on the server side; +- checks the HTTP request, negotiates ``extensions`` and ``subprotocol``, and + configures the protocol accordingly; +- builds an HTTP response based on the above and parameters passed to + :meth:`~server.serve`; +- writes the HTTP response to the network; +- moves to the ``OPEN`` state; +- returns the ``path`` part of the ``uri``. + +The most significant asymmetry between the two sides of the opening handshake +lies in the negotiation of extensions and, to a lesser extent, of the +subprotocol. The server knows everything about both sides and decides what the +parameters should be for the connection. The client merely applies them. + +If anything goes wrong during the opening handshake, websockets :ref:`fails +the connection <connection-failure>`. + + +.. _data-transfer: + +Data transfer +------------- + +Symmetry +........ + +Once the opening handshake has completed, the WebSocket protocol enters the +data transfer phase. This part is almost symmetrical. There are only two +differences between a server and a client: + +- `client-to-server masking`_: the client masks outgoing frames; the server + unmasks incoming frames; +- `closing the TCP connection`_: the server closes the connection immediately; + the client waits for the server to do it. + +.. _client-to-server masking: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3 +.. _closing the TCP connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.1 + +These differences are so minor that all the logic for `data framing`_, for +`sending and receiving data`_ and for `closing the connection`_ is implemented +in the same class, :class:`~legacy.protocol.WebSocketCommonProtocol`. + +.. _data framing: https://www.rfc-editor.org/rfc/rfc6455.html#section-5 +.. _sending and receiving data: https://www.rfc-editor.org/rfc/rfc6455.html#section-6 +.. _closing the connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-7 + +The :attr:`~legacy.protocol.WebSocketCommonProtocol.is_client` attribute tells which +side a protocol instance is managing. This attribute is defined on the +:attr:`~server.WebSocketServerProtocol` and +:attr:`~client.WebSocketClientProtocol` classes. + +Data flow +......... + +The following diagram shows how data flows between an application built on top +of websockets and a remote endpoint. It applies regardless of which side is +the server or the client. + +.. image:: protocol.svg + :target: _images/protocol.svg + +Public methods are shown in green, private methods in yellow, and buffers in +orange. Methods related to connection termination are omitted; connection +termination is discussed in another section below. + +Receiving data +.............. + +The left side of the diagram shows how websockets receives data. + +Incoming data is written to a :class:`~asyncio.StreamReader` in order to +implement flow control and provide backpressure on the TCP connection. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, which is started +when the WebSocket connection is established, processes this data. + +When it receives data frames, it reassembles fragments and puts the resulting +messages in the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue. + +When it encounters a control frame: + +- if it's a close frame, it starts the closing handshake; +- if it's a ping frame, it answers with a pong frame; +- if it's a pong frame, it acknowledges the corresponding ping (unless it's an + unsolicited pong). + +Running this process in a task guarantees that control frames are processed +promptly. Without such a task, websockets would depend on the application to +drive the connection by having exactly one coroutine awaiting +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` at any time. While this +happens naturally in many use cases, it cannot be relied upon. + +Then :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` fetches the next message +from the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue, with some +complexity added for handling backpressure and termination correctly. + +Sending data +............ + +The right side of the diagram shows how websockets sends data. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.send` writes one or several data +frames containing the message. While sending a fragmented message, concurrent +calls to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` are put on hold until +all fragments are sent. This makes concurrent calls safe. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping` writes a ping frame and +yields a :class:`~asyncio.Future` which will be completed when a matching pong +frame is received. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` writes a pong frame. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` writes a close frame and +waits for the TCP connection to terminate. + +Outgoing data is written to a :class:`~asyncio.StreamWriter` in order to +implement flow control and provide backpressure from the TCP connection. + +.. _closing-handshake: + +Closing handshake +................. + +When the other side of the connection initiates the closing handshake, +:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives a close +frame while in the ``OPEN`` state. It moves to the ``CLOSING`` state, sends a +close frame, and returns :obj:`None`, causing +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate. + +When this side of the connection initiates the closing handshake with +:meth:`~legacy.protocol.WebSocketCommonProtocol.close`, it moves to the ``CLOSING`` +state and sends a close frame. When the other side sends a close frame, +:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives it in the +``CLOSING`` state and returns :obj:`None`, also causing +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate. + +If the other side doesn't send a close frame within the connection's close +timeout, websockets :ref:`fails the connection <connection-failure>`. + +The closing handshake can take up to ``2 * close_timeout``: one +``close_timeout`` to write a close frame and one ``close_timeout`` to receive +a close frame. + +Then websockets terminates the TCP connection. + + +.. _connection-termination: + +Connection termination +---------------------- + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which is +started when the WebSocket connection is established, is responsible for +eventually closing the TCP connection. + +First :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` waits +for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate, +which may happen as a result of: + +- a successful closing handshake: as explained above, this exits the infinite + loop in :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`; +- a timeout while waiting for the closing handshake to complete: this cancels + :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`; +- a protocol error, including connection errors: depending on the exception, + :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` :ref:`fails the + connection <connection-failure>` with a suitable code and exits. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` is separate +from :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to make it +easier to implement the timeout on the closing handshake. Canceling +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` creates no risk +of canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` +and failing to close the TCP connection, thus leaking resources. + +Then :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` cancels +:meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no +protocol compliance responsibilities. Terminating it to avoid leaking it is +the only concern. + +Terminating the TCP connection can take up to ``2 * close_timeout`` on the +server side and ``3 * close_timeout`` on the client side. Clients start by +waiting for the server to close the connection, hence the extra +``close_timeout``. Then both sides go through the following steps until the +TCP connection is lost: half-closing the connection (only for non-TLS +connections), closing the connection, aborting the connection. At this point +the connection drops regardless of what happens on the network. + + +.. _connection-failure: + +Connection failure +------------------ + +If the opening handshake doesn't complete successfully, websockets fails the +connection by closing the TCP connection. + +Once the opening handshake has completed, websockets fails the connection by +canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +and sending a close frame if appropriate. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` exits, unblocking +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which closes +the TCP connection. + + +.. _server-shutdown: + +Server shutdown +--------------- + +:class:`~websockets.server.WebSocketServer` closes asynchronously like +:class:`asyncio.Server`. The shutdown happen in two steps: + +1. Stop listening and accepting new connections; +2. Close established connections with close code 1001 (going away) or, if + the opening handshake is still in progress, with HTTP status code 503 + (Service Unavailable). + +The first call to :class:`~websockets.server.WebSocketServer.close` starts a +task that performs this sequence. Further calls are ignored. This is the +easiest way to make :class:`~websockets.server.WebSocketServer.close` and +:class:`~websockets.server.WebSocketServer.wait_closed` idempotent. + + +.. _cancellation: + +Cancellation +------------ + +User code +......... + +websockets provides a WebSocket application server. It manages connections and +passes them to user-provided connection handlers. This is an *inversion of +control* scenario: library code calls user code. + +If a connection drops, the corresponding handler should terminate. If the +server shuts down, all connection handlers must terminate. Canceling +connection handlers would terminate them. + +However, using cancellation for this purpose would require all connection +handlers to handle it properly. For example, if a connection handler starts +some tasks, it should catch :exc:`~asyncio.CancelledError`, terminate or +cancel these tasks, and then re-raise the exception. + +Cancellation is tricky in :mod:`asyncio` applications, especially when it +interacts with finalization logic. In the example above, what if a handler +gets interrupted with :exc:`~asyncio.CancelledError` while it's finalizing +the tasks it started, after detecting that the connection dropped? + +websockets considers that cancellation may only be triggered by the caller of +a coroutine when it doesn't care about the results of that coroutine anymore. +(Source: `Guido van Rossum <https://groups.google.com/forum/#!msg +/python-tulip/LZQe38CR3bg/7qZ1p_q5yycJ>`_). Since connection handlers run +arbitrary user code, websockets has no way of deciding whether that code is +still doing something worth caring about. + +For these reasons, websockets never cancels connection handlers. Instead it +expects them to detect when the connection is closed, execute finalization +logic if needed, and exit. + +Conversely, cancellation isn't a concern for WebSocket clients because they +don't involve inversion of control. + +Library +....... + +Most :doc:`public APIs <../reference/index>` of websockets are coroutines. +They may be canceled, for example if the user starts a task that calls these +coroutines and cancels the task later. websockets must handle this situation. + +Cancellation during the opening handshake is handled like any other exception: +the TCP connection is closed and the exception is re-raised. This can only +happen on the client side. On the server side, the opening handshake is +managed by websockets and nothing results in a cancellation. + +Once the WebSocket connection is established, internal tasks +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` and +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` mustn't get +accidentally canceled if a coroutine that awaits them is canceled. In other +words, they must be shielded from cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` waits for the next message in +the queue or for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +to terminate, whichever comes first. It relies on :func:`~asyncio.wait` for +waiting on two futures in parallel. As a consequence, even though it's waiting +on a :class:`~asyncio.Future` signaling the next message and on +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, it doesn't +propagate cancellation to them. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.ensure_open` is called by +:meth:`~legacy.protocol.WebSocketCommonProtocol.send`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong`. When the connection state is +``CLOSING``, it waits for +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` but shields it to +prevent cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` waits for the data transfer +task to terminate with :func:`~asyncio.timeout`. If it's canceled or if the +timeout elapses, :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +is canceled, which is correct at this point. +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` then waits for +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` but shields it +to prevent cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` and +:meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` are the only +places where :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` may +be canceled. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` starts by +waiting for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`. It +catches :exc:`~asyncio.CancelledError` to prevent a cancellation of +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` from propagating +to :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`. + +.. _backpressure: + +Backpressure +------------ + +.. note:: + + This section discusses backpressure from the perspective of a server but + the concept applies to clients symmetrically. + +With a naive implementation, if a server receives inputs faster than it can +process them, or if it generates outputs faster than it can send them, data +accumulates in buffers, eventually causing the server to run out of memory and +crash. + +The solution to this problem is backpressure. Any part of the server that +receives inputs faster than it can process them and send the outputs +must propagate that information back to the previous part in the chain. + +websockets is designed to make it easy to get backpressure right. + +For incoming data, websockets builds upon :class:`~asyncio.StreamReader` which +propagates backpressure to its own buffer and to the TCP stream. Frames are +parsed from the input stream and added to a bounded queue. If the queue fills +up, parsing halts until the application reads a frame. + +For outgoing data, websockets builds upon :class:`~asyncio.StreamWriter` which +implements flow control. If the output buffers grow too large, it waits until +they're drained. That's why all APIs that write frames are asynchronous. + +Of course, it's still possible for an application to create its own unbounded +buffers and break the backpressure. Be careful with queues. + + +.. _buffers: + +Buffers +------- + +.. note:: + + This section discusses buffers from the perspective of a server but it + applies to clients as well. + +An asynchronous systems works best when its buffers are almost always empty. + +For example, if a client sends data too fast for a server, the queue of +incoming messages will be constantly full. The server will always be 32 +messages (by default) behind the client. This consumes memory and increases +latency for no good reason. The problem is called bufferbloat. + +If buffers are almost always full and that problem cannot be solved by adding +capacity — typically because the system is bottlenecked by the output and +constantly regulated by backpressure — reducing the size of buffers minimizes +negative consequences. + +By default websockets has rather high limits. You can decrease them according +to your application's characteristics. + +Bufferbloat can happen at every level in the stack where there is a buffer. +For each connection, the receiving side contains these buffers: + +- OS buffers: tuning them is an advanced optimization. +- :class:`~asyncio.StreamReader` bytes buffer: the default limit is 64 KiB. + You can set another limit by passing a ``read_limit`` keyword argument to + :func:`~client.connect()` or :func:`~server.serve`. +- Incoming messages :class:`~collections.deque`: its size depends both on + the size and the number of messages it contains. By default the maximum + UTF-8 encoded size is 1 MiB and the maximum number is 32. In the worst case, + after UTF-8 decoding, a single message could take up to 4 MiB of memory and + the overall memory consumption could reach 128 MiB. You should adjust these + limits by setting the ``max_size`` and ``max_queue`` keyword arguments of + :func:`~client.connect()` or :func:`~server.serve` according to your + application's requirements. + +For each connection, the sending side contains these buffers: + +- :class:`~asyncio.StreamWriter` bytes buffer: the default size is 64 KiB. + You can set another limit by passing a ``write_limit`` keyword argument to + :func:`~client.connect()` or :func:`~server.serve`. +- OS buffers: tuning them is an advanced optimization. + +Concurrency +----------- + +Awaiting any combination of :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.send`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, or +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` concurrently is safe, including +multiple calls to the same method, with one exception and one limitation. + +* **Only one coroutine can receive messages at a time.** This constraint + avoids non-deterministic behavior (and simplifies the implementation). If a + coroutine is awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, + awaiting it again in another coroutine raises :exc:`RuntimeError`. + +* **Sending a fragmented message forces serialization.** Indeed, the WebSocket + protocol doesn't support multiplexing messages. If a coroutine is awaiting + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to send a fragmented message, + awaiting it again in another coroutine waits until the first call completes. + This will be transparent in many cases. It may be a concern if the + fragmented message is generated slowly by an asynchronous iterator. + +Receiving frames is independent from sending frames. This isolates +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, which receives frames, from +the other methods, which send frames. + +While the connection is open, each frame is sent with a single write. Combined +with the concurrency model of :mod:`asyncio`, this enforces serialization. The +only other requirement is to prevent interleaving other data frames in the +middle of a fragmented message. + +After the connection is closed, sending a frame raises +:exc:`~websockets.exceptions.ConnectionClosed`, which is safe. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst new file mode 100644 index 0000000000..120a3dd327 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst @@ -0,0 +1,18 @@ +Topic guides +============ + +Get a deeper understanding of how websockets is built and why. + +.. toctree:: + :titlesonly: + + deployment + logging + authentication + broadcast + compression + timeouts + design + memory + security + performance diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle new file mode 100644 index 0000000000..bd888dcf31 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle @@ -0,0 +1,25 @@ +��][W�8~~���}�G�%��a�.�0KC��N�����J���i�i���ߒs�5n����cUI咾������}틌b/~Y�:Z�d���~Y}��u�Z�����?v����j#ߋ����������j�G#_�Z;���`��� �Vk�pU[=O�ћV���R���CE���p$���2[�M�U(f�{A��zN����ƅ��l;��E�W2�\�u���B�$r �M�њ]�X�lϱ��t��Ɏ"[]��'<�&H����D�x�������}�7ZS�5�B'�OL�9ɑl�fYOD'a��s'�h,[��-۹PE.7:��9��٧d�"������ +�i�zM�&_�L,���*�mߎ������2K,�;�L�$�� Y'��eF9�x�Y�Yj.y��ٗ�X�`�Gyq@�sE̮���c�1�K��m;�b�G�7�0Se��|Ef�/Y��k����a���AP� WeJ�;#ہ,�����M��"=�0K��V�0c�?�m��IE<�����:��o(~cp�����*�0*2��=@�U�*��x�ȷ�:��W��¡9C�m~]��WϗݫQ����i�f�:��2��%�N�w��M +�����<^ k`2}�0H��~���+��F�'�/2�T�s��\kS��*WO7��<&&{�v��s��(�yF[x�������J�%&Eq���Ց�jXX:%ƅ!���f`�[#�-D-$�y]����&���)Lr;wf�kM[E��톗yMe��m�}���pV�V�ݎ���a�fu1M͗3a=�۹*��m?NҜ�k{���Q]�<79/POZ��rͶ�l�f����`�=�LkM#��[Y&�%8��*ܤ��s�X)������a��0Ma0 +6�2�B�JKb@i���i"!8����u�;��H�����py2:��ǝwݥ��nz=(�P�������E����������֫�w�b�����QlC�)&$g��n�n1*A�Ж���{sF��,؊��٤���O����7�X�b� Ģ&C�04�ʽ�ۻ�0 +dtb��8�� �Bp�X��h��&Q~M7��K��%*�]�%Sd/J��g���8�&���� m��8����g�#���K����>����+���w��(� ���d}+��wݟ���Q`��{�t�+�����8���)տz���\����~odGn/��8�_�Iz�:Bӓ���0RC�$��$�$��N�$Pc(�;G�����78OdзU�C++�1�z��P����v!����/��Tx}[�� +���:���GQr�����H���O��g�=u�i�V�})3tF +Q +���%«��c/�u�gO���O�t�>^ǂ�����.�͏a�wLę���R�D��#F����I��@J���dfE>��s�X����t�(� �C0�"��V)8���aA�+h��kL˄��&LsMc\�b�b!L�xAn��D�z`��D|W��'�OH���"Ne�ځ��#�,np��֒�ӗQo��p��������v�Cӹ�w�'Øb�#bppO��2���� +�Y�c�YF:�����j`������ �:�A�V +� �nzD܂S�8�X�B��qk6�mp��q�d�0O�ʦ^L��s����*���7��h�=�0�i>'a�k(�қ�kZE�;��rKW>àG�$�ʑ�����2lQ]�hnRaLM�b�*_�|�E���e��(*d��2�����%4�c�sv���z����)���H:_�����ӊR���4���4��ҁ/�=b��b� +u�`����]�jʔ���] +n\�]] +rW�WӸ��b:������R�fg�� +E:f&5-�H�ݯ�7��� +Ͳ��[4��zՑ����-r��O�o��&�P�u�b���~-�9�H+���P�z�1�[��5�7��q=��iV��t5[��I-{���{�[^ۂ�C�'M��.sS��U�_��o�{O��I������O���{#ǰt�N̤�p<��rj��4�yA���ENi�eYwANw��9/9�z̽�GNw��97��Vq",A*M�~����N��"���}!������,��Ŀ�a��&��:�;�8�Zywy9�>��Z������/���o��1] �0��Fj�nh�ԗ�ԣ���DŽ)�q��9�T�� +N_�G=:<���.�TK�W��<7?ZSܓZ���-����V�YWy����0�e�w�Q�n���e��*{Y��۩la� �Q� +��/n'�v�2>���&.����{g�}�KŤ#!��@ƕ���[��#܍��E��R��aq7����dDEm�ߺ�z�,�ɭ�����S�h�J~վ�Z��i���#���8����.j� m8N7�v� +s�k/$%u�� �y�Nn���X�Uj�)mGv��K�5KRz?s7xAU7L*�jOo5{ �An�jV�l)xOD���R�)�L.Ñ�}l ��"�T�6]u���9��UT��?GŻ����O,�F�ZG�:fo{c��m�'�;�~&r�':�$�`C&�/��2M[PM�� +͋qs�� +�$����V����Y�}?��"����Y�I {a���^W��;��T��ޭ-h���������#��-���^ئ�o����=o��G�}��w���=z�?����mH��e�.��������a���4�~�/���,���'Q�2*7�1mT��5�ON����/����ZLa���4���o��7�aj�5S��[ +U��D����H���?QA�S�7=E�E��V�Q ��Xw�H�= +���m�Ix��G3����������w��j�&�(*�!ԩ�[��n��%���Yv��;���c��5ox���� +/�I����f5�^]�Ŧ|{E��T��8�&Lߝ��>MT���2�;�� +�)�*}���)�k�4k�j�z���l��y����<r���̾-�8��9[T����o��I��Rd��i�O�`��J�T���L�±���\Ө)��p���ɩ�Ŝ��Х�k�F��w +�Β���01�٭�����ס6W���Ǵj diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg new file mode 100644 index 0000000000..0a9818d293 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-14.3464565 112.653543 624.6929 372.69291" width="624.6929pt" height="372.69291pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2018-07-29 15:25:34 +0000</dc:date></metadata><defs><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><text transform="translate(19.173228 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="1.5138254" y="10" textLength="72.01172">CONNECTING</tspan></text><text transform="translate(160.90551 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="23.117341" y="10" textLength="28.804688">OPEN</tspan></text><text transform="translate(359.3307 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">CLOSING</tspan></text><text transform="translate(501.063 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="15.916169" y="10" textLength="43.20703">CLOSED</tspan></text><line x1="198.4252" y1="170.07874" x2="198.4252" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="396.8504" y1="170.07874" x2="396.8504" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="538.58267" y1="170.07874" x2="538.58267" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="56.692913" y1="170.07874" x2="56.692913" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" fill="#dadada"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 248.11811)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="52.40498" y="10" textLength="93.615234">transfer_data</tspan></text><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" fill="#dadada"/><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 361.50393)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="107.469364" y="10" textLength="115.21875">close_connection</tspan></text><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" fill="#6f6"/><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">connect</tspan></text><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" fill="#6f6"/><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="17.912947" y="10" textLength="100.816406">recv / send / </tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="118.72935" y="10" textLength="93.615234">ping / pong /</tspan><tspan font-family="Courier New" font-size="12" font-weight="bold" x="212.34459" y="10" textLength="50.408203"> close </tspan></text><path d="M 170.07874 198.4252 L 183.97874 198.4252 L 198.4252 198.4252 L 198.4252 283.46457 L 198.4252 368.50393 L 212.87165 368.50393 L 215.37165 368.50393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(75.86614 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="27.760296" y="12" textLength="52.083984">opening </tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.164593" y="27" textLength="58.02539">handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="77.072796" y="27" textLength="7.1484375">e</tspan></text><text transform="translate(416.02362 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.182171" y="12" textLength="65.021484">connection</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="16.861858" y="27" textLength="69.66211">termination</tspan></text><text transform="translate(217.59842 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="41.03058" y="12" textLength="40.6875">data tr</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="81.507143" y="12" textLength="37.541016">ansfer</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="18.211245" y="27" textLength="116.625">& closing handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="134.71906" y="27" textLength="7.1484375">e</tspan></text><path d="M 425.19685 255.11811 L 439.09685 255.11811 L 453.5433 255.11811 L 453.5433 342.9307" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" fill="#dadada"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 304.81102)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="48.804395" y="10" textLength="100.816406">keepalive_ping</tspan></text><line x1="198.4252" y1="255.11811" x2="214.62165" y2="255.11811" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="198.4252" y1="311.81102" x2="215.37165" y2="311.81102" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst new file mode 100644 index 0000000000..e7abd96ce5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst @@ -0,0 +1,245 @@ +Logging +======= + +.. currentmodule:: websockets + +Logs contents +------------- + +When you run a WebSocket client, your code calls coroutines provided by +websockets. + +If an error occurs, websockets tells you by raising an exception. For example, +it raises a :exc:`~exceptions.ConnectionClosed` exception if the other side +closes the connection. + +When you run a WebSocket server, websockets accepts connections, performs the +opening handshake, runs the connection handler coroutine that you provided, +and performs the closing handshake. + +Given this `inversion of control`_, if an error happens in the opening +handshake or if the connection handler crashes, there is no way to raise an +exception that you can handle. + +.. _inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control + +Logs tell you about these errors. + +Besides errors, you may want to record the activity of the server. + +In a request/response protocol such as HTTP, there's an obvious way to record +activity: log one event per request/response. Unfortunately, this solution +doesn't work well for a bidirectional protocol such as WebSocket. + +Instead, when running as a server, websockets logs one event when a +`connection is established`_ and another event when a `connection is +closed`_. + +.. _connection is established: https://www.rfc-editor.org/rfc/rfc6455.html#section-4 +.. _connection is closed: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.4 + +By default, websockets doesn't log an event for every message. That would be +excessive for many applications exchanging small messages at a fast rate. If +you need this level of detail, you could add logging in your own code. + +Finally, you can enable debug logs to get details about everything websockets +is doing. This can be useful when developing clients as well as servers. + +See :ref:`log levels <log-levels>` below for a list of events logged by +websockets logs at each log level. + +Configure logging +----------------- + +websockets relies on the :mod:`logging` module from the standard library in +order to maximize compatibility and integrate nicely with other libraries:: + + import logging + +websockets logs to the ``"websockets.client"`` and ``"websockets.server"`` +loggers. + +websockets doesn't provide a default logging configuration because +requirements vary a lot depending on the environment. + +Here's a basic configuration for a server in production:: + + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.INFO, + ) + +Here's how to enable debug logs for development:: + + logging.basicConfig( + format="%(message)s", + level=logging.DEBUG, + ) + +Furthermore, websockets adds a ``websocket`` attribute to log records, so you +can include additional information about the current connection in logs. + +You could attempt to add information with a formatter:: + + # this doesn't work! + logging.basicConfig( + format="{asctime} {websocket.id} {websocket.remote_address[0]} {message}", + level=logging.INFO, + style="{", + ) + +However, this technique runs into two problems: + +* The formatter applies to all records. It will crash if it receives a record + without a ``websocket`` attribute. For example, this happens when logging + that the server starts because there is no current connection. + +* Even with :meth:`str.format` style, you're restricted to attribute and index + lookups, which isn't enough to implement some fairly simple requirements. + +There's a better way. :func:`~client.connect` and :func:`~server.serve` accept +a ``logger`` argument to override the default :class:`~logging.Logger`. You +can set ``logger`` to a :class:`~logging.LoggerAdapter` that enriches logs. + +For example, if the server is behind a reverse +proxy, :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` gives +the IP address of the proxy, which isn't useful. IP addresses of clients are +provided in an HTTP header set by the proxy. + +Here's how to include them in logs, assuming they're in the +``X-Forwarded-For`` header:: + + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.INFO, + ) + + class LoggerAdapter(logging.LoggerAdapter): + """Add connection ID and client IP address to websockets logs.""" + def process(self, msg, kwargs): + try: + websocket = kwargs["extra"]["websocket"] + except KeyError: + return msg, kwargs + xff = websocket.request_headers.get("X-Forwarded-For") + return f"{websocket.id} {xff} {msg}", kwargs + + async with websockets.serve( + ..., + # Python < 3.10 requires passing None as the second argument. + logger=LoggerAdapter(logging.getLogger("websockets.server"), None), + ): + ... + +Logging to JSON +--------------- + +Even though :mod:`logging` predates structured logging, it's still possible to +output logs as JSON with a bit of effort. + +First, we need a :class:`~logging.Formatter` that renders JSON: + +.. literalinclude:: ../../example/logging/json_log_formatter.py + +Then, we configure logging to apply this formatter:: + + handler = logging.StreamHandler() + handler.setFormatter(formatter) + + logger = logging.getLogger() + logger.addHandler(handler) + logger.setLevel(logging.INFO) + +Finally, we populate the ``event_data`` custom attribute in log records with +a :class:`~logging.LoggerAdapter`:: + + class LoggerAdapter(logging.LoggerAdapter): + """Add connection ID and client IP address to websockets logs.""" + def process(self, msg, kwargs): + try: + websocket = kwargs["extra"]["websocket"] + except KeyError: + return msg, kwargs + kwargs["extra"]["event_data"] = { + "connection_id": str(websocket.id), + "remote_addr": websocket.request_headers.get("X-Forwarded-For"), + } + return msg, kwargs + + async with websockets.serve( + ..., + # Python < 3.10 requires passing None as the second argument. + logger=LoggerAdapter(logging.getLogger("websockets.server"), None), + ): + ... + +Disable logging +--------------- + +If your application doesn't configure :mod:`logging`, Python outputs messages +of severity ``WARNING`` and higher to :data:`~sys.stderr`. As a consequence, +you will see a message and a stack trace if a connection handler coroutine +crashes or if you hit a bug in websockets. + +If you want to disable this behavior for websockets, you can add +a :class:`~logging.NullHandler`:: + + logging.getLogger("websockets").addHandler(logging.NullHandler()) + +Additionally, if your application configures :mod:`logging`, you must disable +propagation to the root logger, or else its handlers could output logs:: + + logging.getLogger("websockets").propagate = False + +Alternatively, you could set the log level to ``CRITICAL`` for the +``"websockets"`` logger, as the highest level currently used is ``ERROR``:: + + logging.getLogger("websockets").setLevel(logging.CRITICAL) + +Or you could configure a filter to drop all messages:: + + logging.getLogger("websockets").addFilter(lambda record: None) + +.. _log-levels: + +Log levels +---------- + +Here's what websockets logs at each level. + +``ERROR`` +......... + +* Exceptions raised by connection handler coroutines in servers +* Exceptions resulting from bugs in websockets + +``WARNING`` +........... + +* Failures in :func:`~websockets.broadcast` + +``INFO`` +........ + +* Server starting and stopping +* Server establishing and closing connections +* Client reconnecting automatically + +``DEBUG`` +......... + +* Changes to the state of connections +* Handshake requests and responses +* All frames sent and received +* Steps to close a connection +* Keepalive pings and pongs +* Errors handled transparently + +Debug messages have cute prefixes that make logs easier to scan: + +* ``>`` - send something +* ``<`` - receive something +* ``=`` - set connection state +* ``x`` - shut down connection +* ``%`` - manage pings and pongs +* ``!`` - handle errors and timeouts diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst new file mode 100644 index 0000000000..e44247a77c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst @@ -0,0 +1,48 @@ +Memory usage +============ + +.. currentmodule:: websockets + +In most cases, memory usage of a WebSocket server is proportional to the +number of open connections. When a server handles thousands of connections, +memory usage can become a bottleneck. + +Memory usage of a single connection is the sum of: + +1. the baseline amount of memory websockets requires for each connection, +2. the amount of data held in buffers before the application processes it, +3. any additional memory allocated by the application itself. + +Baseline +-------- + +Compression settings are the main factor affecting the baseline amount of +memory used by each connection. + +With websockets' defaults, on the server side, a single connections uses +70 KiB of memory. + +Refer to the :doc:`topic guide on compression <../topics/compression>` to +learn more about tuning compression settings. + +Buffers +------- + +Under normal circumstances, buffers are almost always empty. + +Under high load, if a server receives more messages than it can process, +bufferbloat can result in excessive memory usage. + +By default websockets has generous limits. It is strongly recommended to adapt +them to your application. When you call :func:`~server.serve`: + +- Set ``max_size`` (default: 1 MiB, UTF-8 encoded) to the maximum size of + messages your application generates. +- Set ``max_queue`` (default: 32) to the maximum number of messages your + application expects to receive faster than it can process them. The queue + provides burst tolerance without slowing down the TCP connection. + +Furthermore, you can lower ``read_limit`` and ``write_limit`` (default: +64 KiB) to reduce the size of buffers for incoming and outgoing data. + +The design document provides :ref:`more details about buffers <buffers>`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst new file mode 100644 index 0000000000..45e23b2390 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst @@ -0,0 +1,20 @@ +Performance +=========== + +Here are tips to optimize performance. + +uvloop +------ + +You can make a websockets application faster by running it with uvloop_. + +(This advice isn't specific to websockets. It applies to any :mod:`asyncio` +application.) + +.. _uvloop: https://github.com/MagicStack/uvloop + +broadcast +--------- + +:func:`~websockets.broadcast` is the most efficient way to send a message to +many clients. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle new file mode 100644 index 0000000000..04a9e7acb5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle @@ -0,0 +1,37 @@ +��]ks�6���� +���[Ӹ��q�t�n�xc'�v<ӡ%X�"U���f���J�x-J���ۉl�A��\q��_FQ�N�0�|�=����~2�Ꮟ���#���'�����ѯ/z�(�����g�_������Ǒ��}~��w����Q���}��q��Y����=??�C����!�v�d����5t� +�A>x���^|;���G�=��/�����~\��U<�_��o�b�z�ӧ���寗M��a?ȡ�Ӟ獂4 +�/�=���)��KFq8L���{��+ +NO#-���HJ�X(�P>��D�d����&y�7��$���Ӊ��,�2��p��Y؟�͞}Ff(��k�a|����o���b���=����2`�b? +�f��$ +w�5y�|A9��<�S���"���1O/B�ɧ�������ip��b`oޖ�3:�����L�0ӿ%ɨÊ���A�6 +��b-\Nf1��W�hq����9o�J��(ދ�a�n���>}�bN�� zJ����'\*�d��p��0��y�7H�w��A����@E��~�5�$i����H�a��7��5Vm�<��Qpq��ƽ�����~���w�S飋q�qLS�-��?�8�s�|jIcng<3�c��ꊭ2�|<��X[e�I���O��un(,����>���猯J��|W�r�t�w_��.�C���R$�U�)�Qʐ���|I)�Xq�}�J� �ǂbd\}� +��5#��JH�+�S/o�N+�4f5;�yy�v|i��\ڑ���K�Ҵ2���[�4�Z�Ϣi�gJo��W<���A��B|DY!C�DGA��ن�1�g�! ϯ���Κ@\g���* *Ʃ���1��+*��1�=)�/��1�2#�$�UT(.a�()a��Q&��>RJ�oߚ��¾�g�X6����$ +u�F��rԙ���\,,\��;N�(�&U��������g�5>�+�%o�劀/�2T}c3� +�� +�h�q�A�i~���8���!&���~�K������=�z| ++7?��O���(�4>>�i�sԫ��|�~%)4��q����a�ul~9�&>���/������?��8���1&Ň������ U c �M[�����7L� ��Ad����Y��ӠZz��x����q��N�`��o~������"��+7t�J�K"|��`R��t��:t���"�v�{p�@��4?K�IDς�xlF`S�K�/]66����'9<��WT16b�OC>�o) �p����y�Խ|�\~��f�Ad�������Ǖd�*�K�|�f�ՍB_�#5�n� �M�P�kZm�N��ᵲ��������a +.���f�̸=���*v,�����٤?�a�!BV�c_����?gYk��y��"�2���P�J���&�4f�/<N�""���Ķ�9����V�Oa*�|)�pG��BQ�)�P��%yO� �6}����Xs�ږkY��$�� +�><�DǏn�U��� tS&�5fQS{��a�����H�F�X>�v=��f����ĩ�v6�-�p+��5L8·�*�j]��%l-�� )@G�m�ȅ�eqΰ5|ykǍ���8Y�;B�M���"&����S����֎;F\��M +��� +#.i��������Xu:)�ڛggnqS淍��``mb)}čJ�6�m��$W0H�"1_��P�,� +��W��!{� +�}���m��l�����j{ɖ�@1,i��1�@n;\|�!_�Ta!�tr�H;ϒh�=�TŤNhl��c��WV�����O�|m�yqUY$�7�� +��(�$'��sՄ�鏘}V�_��CKKl�K�w�ӎ1V�_h5�[��������D�f�wwCH��bE'&Z�fѢ(���e[�և\�4� +wr��X�'VA�1���@�v�mp�F3 +�I'�K:�b��A�p?%�B.��xx�b�h�XI��¤s)�:���rb��'�X�W,��{>%DA%gJ:�L�2��A���<`�L���ʀ��B�|ݐ?S�C�(�e�0 +r�OT�d�A-K�z����IM�@��w&u����6�4��+�u�,�c�����1]�����XpJ|�̨X��,��n3�$�c�{�+�Z�Y#������ԝ�-����4�hl��uO�ϖ.�+��yɱ�}���t'י�u���N�ui}���x�\殺 +��|A}_ N)�;�V.Q�"�3��8ج +��[����1Y���pmKҟW�߅7��\_��n]���E�\t�E�pt-����][K����!7�{�{F�T�3�8�fG�:��<�b�>�Ew`|��G�+0�[���[�GZ��:�sK�����;�|P +0����_V=�/���=�L�Fj��Jj�M�E�=�VqYر� +��x;͗������u�4�e�j����F�Z3�m�f���:��1t�S{���HgY0���MB�6,||R�h9�L��u,A�\��f���Q`N}�*��AA�|y �Z�H*�m�B�7��#�杫���*�I�R��0��u�8�i]�u�]���ǹ��sm���:L��Xv=a��B��u�,���&���7��]�C���YE��ݺ�D�#��SX��_�xh`[U�z��OX��Y\_$����֊{��\�H��O���RM 偙!1�����C�mtmR�pv5�� +:o�c=�����L��:�r�iy$�P����0��<��HR%WBZ�6�=�'8��J�"���d���,�R��И�������3.��/�X��0'sm���m[����N�� ,u�»�ݩ�ypC'�wǻ�;�aw��u���t�=�Y�9�s@瀮#Н\䷈s������}�܄m`G\P�&tn�/1�>K�ޠ�Њ�ͤV��m8�v�;�u8��p��47��Am=�i��HK�:�uH����&���g��N�a��ʠ,�ԫ"*PJ�h�� +�x������Aׇ��jٕ%��Ʒd��[��5��y�8_#�f �#�j�N+8vi���� s�.���0���W�1\�a`>G�pj�bk|�mk�8���xz��J�y�~��cE��\wJ��$#�4)@�Y�;�!��*��G�Ŵ��IX�n��曳��7߶ +��7L|SHh+�v�Hh�2,���ۥ���p +�Զ�n��"ugQ��,|eJ�տj�Rw�r����?�-aXT`�HܽJY���-����O�X��A8ɚ 3u胪L�l�c�<��A{/�w���&v�ᙧ0��:���� �4+lֻ��j�#U�`�j���G����s����}��[�X�µ���� ��˟��R)����q�e�e�) +�q)�,�����X��T�LF��M�n�,�ܒKzsn�N��݉�eq ��afw�3�aƲ�#��:��*��� ��WU�KeA��G�K0�i���1��qRA����D�l5��&����ܮ&���}MB�٘+ՏH�%y��R},F�*u�qJ�`0�(�}��"�8��D��N$߁�ֻ+���s"��H����̷��(��ڜ���\���b��Zw�$ҫ���}]��]r��q0o@�%ð���6�lV�i�JKm�-�J�VK7�C��8�i +%��>ɒ�'0�����G}rX|���FI|�&9�at�I5\���]T8����K�Ψp��=N�s*�}B�T��\�t<�l�� +���8�<ʠ31���������dՖ],����Ijm>��|�ܓ�̴Z�۳���q0Ύ��/�jt�ڈ�yC+��?�Yx�����X��9�C�P;BP�۴A����z|����(����L��B��;���5�h|�odJ�&Y���mz�I~�Z�uX����Y�a|���n�8�6���K|/GA���+2W�a�O~ ��a��ء|�ւ�P�V]VZA�|ø9�A�7� O�ވ�:�����6���9'��nX�_a��y2;�W� +����{��6�Ѭ@����R`g���\�K2OgF��қ�� �v�������Y�nf�q�����.�]�t�{{��$�ø�� +�s�'z����9��v���ӹsM��5}�:�nn�7�ϒ<OF��0,-�RTk��i�y��g�3\��bM��2I�?ͮ.w��]�F}���=�����9��#F��Ϣ�^�L������ዳ�����'���}�{��=�����h���GG��?~������b�����Kt������Z��ez�\���i�-�O?����N��1l�MŒ�ݦ�n��W�����%\H0�TuÜ�f��ӟA�iK܇����m������ut_���E������Ä�����rLy��W*�^��iMu�J��4g*#`�Q�7�o?�?YEQ���N]ø���^�ī:hwEc�q_���}�1������q�WM������0#���'i +3Q�dSO���/f&���).�q�;�X�xZ��2@��"�' �������A�)��X<��n-�r�^�I��rռ]\dyz`��(8y� +�� Jkj�L4��� +z-�|<n�"�G�ݝ�)�4��$��V��!���Y����ba��,��|/e0j��s�<}������� diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg new file mode 100644 index 0000000000..51bfd982be --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 624.34646 822.34646" width="624.34646pt" height="822.34646pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2019-07-07 08:38:24 +0000</dc:date></metadata><defs><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="10" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><radialGradient cx="0" cy="0" r="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#a5a5a5"/></radialGradient><radialGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(311.81102 708.6614) scale(145.75703)"/><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_2" viewBox="-1 -6 14 12" markerWidth="14" markerHeight="12" color="black"><g><path d="M 12 0 L 0 0 M 0 -4.5 L 12 0 L 0 4.5" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_3" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.9253333 0 L 0 0 M 0 -2.222 L 5.9253333 0 L 0 2.222" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><rect fill="white" width="1314" height="1698"/><g><title>Layer 1</title><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" fill="#6cf"/><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 772.02755)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="228.50753" y="12" textLength="99.91406">remote endpoint</tspan></text><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" fill="white"/><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 90.03937)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="243.79171" y="12" textLength="51.333984">websock</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="295.00851" y="12" textLength="18.128906">ets</tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="195.65109" y="25" textLength="165.62695">WebSocketCommonProtocol</tspan></text><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" fill="#6f6"/><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 35.019685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="230.0046" y="12" textLength="96.91992">application logic</tspan></text><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165" fill="#fc6"/><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165 M 238.11023 586.77165 C 228.72189 586.77165 221.10236 596.93102 221.10236 609.4488 C 221.10236 621.9666 228.72189 632.12598 238.11023 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(125.33071 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">reader</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamReader</tspan></text><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165" fill="#fc6"/><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165 M 521.5748 586.77165 C 512.18645 586.77165 504.56693 596.93102 504.56693 609.4488 C 504.56693 621.9666 512.18645 632.12598 521.5748 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(408.79527 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">writer</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamWriter</tspan></text><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756" fill="#fecc66"/><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756 M 481.88976 374.17323 C 481.88976 377.30267 469.19055 379.84252 453.5433 379.84252 C 437.89606 379.84252 425.19685 377.30267 425.19685 374.17323" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(429.19685 387.18504)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="6.343527" y="10" textLength="36.00586">pings</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="12.3445034" y="22" textLength="24.003906">dict</tspan></text><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" fill="#dadada"/><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(61.692913 288.46457)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="43.57528" y="10" textLength="129.62109">transfer_data_task</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="96.383873" y="22" textLength="24.003906">Task</tspan></text><path d="M 297.6378 765.35433 L 297.6378 609.4488 L 255.11811 609.4488 L 269.01811 609.4488 L 266.51811 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 368.50393 609.4488 L 354.60393 609.4488 L 325.98425 609.4488 L 325.98425 753.95433" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" fill="url(#Obj_Gradient)"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(217.59842 701.1614)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="69.81416" y="12" textLength="48.796875">network</tspan></text><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 460.71653)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="44.03351" y="10" textLength="72.01172">read_frame</tspan></text><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 404.02362)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="26.03058" y="10" textLength="108.01758">read_data_frame</tspan></text><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 347.3307)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="36.832338" y="10" textLength="86.41406">read_message</tspan></text><text transform="translate(178.07874 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(178.07874 433.87008)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><text transform="translate(178.07874 371.67716)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="24.003906">data</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="19" textLength="36.00586">frames</tspan></text><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" fill="#ff6"/><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 517.40945)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="40.432924" y="10" textLength="79.21289">write_frame</tspan></text><path d="M 85.03937 609.4488 L 71.13937 609.4488 L 56.692913 609.4488 L 56.692913 595.2756 L 56.692913 566.92913 L 113.385826 566.92913 L 170.07874 566.92913 L 170.07874 495.78976 L 170.07874 494.03976" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 453.5433 539.33267 L 453.5433 552.48267 L 453.5433 566.92913 L 510.23622 566.92913 L 569.76378 566.92913 L 569.76378 595.2756 L 569.76378 609.4488 L 552.48267 609.4488 L 549.98267 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="453.5433" x2="170.07874" y2="437.34685" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="396.8504" x2="170.07874" y2="380.65393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449" fill="#fc6"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449 M 238.11023 204.09449 C 228.72189 204.09449 221.10236 214.25386 221.10236 226.77165 C 221.10236 239.28945 228.72189 249.44882 238.11023 249.44882" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(132.33071 214.27165)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x=".1953125" y="10" textLength="57.609375">messages</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="13.997559" y="22" textLength="30.004883">deque</tspan></text><path d="M 255.11811 354.3307 L 269.01811 354.3307 L 297.6378 354.3307 L 297.6378 328.8189 L 297.6378 226.77165 L 269.01811 226.77165 L 266.51811 226.77165" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">recv</tspan></text><path d="M 85.03937 226.77165 L 71.13937 226.77165 L 42.519685 226.77165 L 42.519685 209.76378 L 42.519685 155.90551 L 71.13937 155.90551 L 73.63937 155.90551" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="141.73228" x2="170.07874" y2="68.092913" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="453.5433" y1="56.692913" x2="453.5433" y2="130.33228" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="467.71653" y1="56.692913" x2="467.71653" y2="187.8752" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="481.88976" y1="56.692913" x2="481.88976" y2="244.56811" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="496.063" y1="56.692913" x2="496.063" y2="300.32302" marker-end="url(#StickArrow_Marker_3)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">send</tspan></text><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 205.59842)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">ping</tspan></text><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 262.29134)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">pong</tspan></text><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 318.98425)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="62.03644" y="10" textLength="36.00586">close</tspan></text><path d="M 538.58267 155.90551 L 552.48267 155.90551 L 566.92913 155.90551 L 566.92913 481.88976 L 453.5433 481.88976 L 453.5433 496.33622 L 453.5433 498.08622" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="538.58267" y1="212.59842" x2="566.92913" y2="212.59842" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="269.29134" x2="566.92913" y2="269.29134" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="325.98425" x2="566.92913" y2="325.98425" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 255.86811 411.02362 L 262.61811 411.02362 L 340.15748 411.02362 L 340.15748 481.88976 L 453.5433 481.88976" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(291.94527 399.02362)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="42.006836">control</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="21" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="411.02362" x2="414.64685" y2="411.02362" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><path d="M 368.50393 212.59842 L 361.75393 212.59842 L 340.15748 212.59842 L 340.15748 340.15748 L 340.15748 382.67716" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(461.5433 547.2559)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(461.5433 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="382.67716" x2="414.64685" y2="382.67716" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst new file mode 100644 index 0000000000..d3dec21bd1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst @@ -0,0 +1,41 @@ +Security +======== + +Encryption +---------- + +For production use, a server should require encrypted connections. + +See this example of :ref:`encrypting connections with TLS +<secure-server-example>`. + +Memory usage +------------ + +.. warning:: + + An attacker who can open an arbitrary number of connections will be able + to perform a denial of service by memory exhaustion. If you're concerned + by denial of service attacks, you must reject suspicious connections + before they reach websockets, typically in a reverse proxy. + +With the default settings, opening a connection uses 70 KiB of memory. + +Sending some highly compressed messages could use up to 128 MiB of memory with +an amplification factor of 1000 between network traffic and memory usage. + +Configuring a server to :doc:`optimize memory usage <memory>` will improve +security in addition to improving performance. + +Other limits +------------ + +websockets implements additional limits on the amount of data it accepts in +order to minimize exposure to security vulnerabilities. + +In the opening handshake, websockets limits the number of HTTP headers to 256 +and the size of an individual header to 4096 bytes. These limits are 10 to 20 +times larger than what's expected in standard use cases. They're hard-coded. + +If you need to change these limits, you can monkey-patch the constants in +``websockets.http11``. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst new file mode 100644 index 0000000000..633fc1ab43 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst @@ -0,0 +1,116 @@ +Timeouts +======== + +.. currentmodule:: websockets + +Long-lived connections +---------------------- + +Since the WebSocket protocol is intended for real-time communications over +long-lived connections, it is desirable to ensure that connections don't +break, and if they do, to report the problem quickly. + +Connections can drop as a consequence of temporary network connectivity issues, +which are very common, even within data centers. + +Furthermore, WebSocket builds on top of HTTP/1.1 where connections are +short-lived, even with ``Connection: keep-alive``. Typically, HTTP/1.1 +infrastructure closes idle connections after 30 to 120 seconds. + +As a consequence, proxies may terminate WebSocket connections prematurely when +no message was exchanged in 30 seconds. + +.. _keepalive: + +Keepalive in websockets +----------------------- + +To avoid these problems, websockets runs a keepalive and heartbeat mechanism +based on WebSocket Ping_ and Pong_ frames, which are designed for this purpose. + +.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 +.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + +It loops through these steps: + +1. Wait 20 seconds. +2. Send a Ping frame. +3. Receive a corresponding Pong frame within 20 seconds. + +If the Pong frame isn't received, websockets considers the connection broken and +closes it. + +This mechanism serves two purposes: + +1. It creates a trickle of traffic so that the TCP connection isn't idle and + network infrastructure along the path keeps it open ("keepalive"). +2. It detects if the connection drops or becomes so slow that it's unusable in + practice ("heartbeat"). In that case, it terminates the connection and your + application gets a :exc:`~exceptions.ConnectionClosed` exception. + +Timings are configurable with the ``ping_interval`` and ``ping_timeout`` +arguments of :func:`~client.connect` and :func:`~server.serve`. Shorter values +will detect connection drops faster but they will increase network traffic and +they will be more sensitive to latency. + +Setting ``ping_interval`` to :obj:`None` disables the whole keepalive and +heartbeat mechanism. + +Setting ``ping_timeout`` to :obj:`None` disables only timeouts. This enables +keepalive, to keep idle connections open, and disables heartbeat, to support large +latency spikes. + +.. admonition:: Why doesn't websockets rely on TCP keepalive? + :class: hint + + TCP keepalive is disabled by default on most operating systems. When + enabled, the default interval is two hours or more, which is far too much. + +Keepalive in browsers +--------------------- + +Browsers don't enable a keepalive mechanism like websockets by default. As a +consequence, they can fail to notice that a WebSocket connection is broken for +an extended period of time, until the TCP connection times out. + +In this scenario, the ``WebSocket`` object in the browser doesn't fire a +``close`` event. If you have a reconnection mechanism, it doesn't kick in +because it believes that the connection is still working. + +If your browser-based app mysteriously and randomly fails to receive events, +this is a likely cause. You need a keepalive mechanism in the browser to avoid +this scenario. + +Unfortunately, the WebSocket API in browsers doesn't expose the native Ping and +Pong functionality in the WebSocket protocol. You have to roll your own in the +application layer. + +Latency issues +-------------- + +Latency between a client and a server may increase for two reasons: + +* Network connectivity is poor. When network packets are lost, TCP attempts to + retransmit them, which manifests as latency. Excessive packet loss makes + the connection unusable in practice. At some point, timing out is a + reasonable choice. + +* Traffic is high. For example, if a client sends messages on the connection + faster than a server can process them, this manifests as latency as well, + because data is waiting in flight, mostly in OS buffers. + + If the server is more than 20 seconds behind, it doesn't see the Pong before + the default timeout elapses. As a consequence, it closes the connection. + This is a reasonable choice to prevent overload. + + If traffic spikes cause unwanted timeouts and you're confident that the server + will catch up eventually, you can increase ``ping_timeout`` or you can set it + to :obj:`None` to disable heartbeat entirely. + + The same reasoning applies to situations where the server sends more traffic + than the client can accept. + +The latency measured during the last exchange of Ping and Pong frames is +available in the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` +attribute. Alternatively, you can measure the latency at any time with the +:attr:`~legacy.protocol.WebSocketCommonProtocol.ping` method. diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/Procfile b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/Procfile new file mode 100644 index 0000000000..2e35818f67 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/Procfile @@ -0,0 +1 @@ +web: python app.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/app.py new file mode 100644 index 0000000000..4ca34d23bb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/app.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +import asyncio +import http +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=8080, + process_request=health_check, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/fly.toml b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/fly.toml new file mode 100644 index 0000000000..5290072ed2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/fly.toml @@ -0,0 +1,16 @@ +app = "websockets-echo" +kill_signal = "SIGTERM" + +[build] + builder = "paketobuildpacks/builder:base" + +[[services]] + internal_port = 8080 + protocol = "tcp" + + [[services.http_checks]] + path = "/healthz" + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt new file mode 100644 index 0000000000..14774b465e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/app.py new file mode 100644 index 0000000000..360479b8eb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/app.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio +import os +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="localhost", + port=8000 + int(os.environ["SUPERVISOR_PROCESS_NAME"][-2:]), + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg new file mode 100644 index 0000000000..e63727d1c0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg @@ -0,0 +1,17 @@ +defaults + mode http + timeout connect 10s + timeout client 30s + timeout server 30s + +frontend websocket + bind localhost:8080 + default_backend websocket + +backend websocket + balance leastconn + server websockets-test_00 localhost:8000 + server websockets-test_01 localhost:8001 + server websockets-test_02 localhost:8002 + server websockets-test_03 localhost:8003 + diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf new file mode 100644 index 0000000000..76a664d91b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf @@ -0,0 +1,7 @@ +[supervisord] + +[program:websockets-test] +command = python app.py +process_name = %(program_name)s_%(process_num)02d +numprocs = 4 +autorestart = true diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/Procfile b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/Procfile new file mode 100644 index 0000000000..2e35818f67 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/Procfile @@ -0,0 +1 @@ +web: python app.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/app.py new file mode 100644 index 0000000000..d4ba3edb51 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/app.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio +import signal +import os + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=int(os.environ["PORT"]), + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt new file mode 100644 index 0000000000..14774b465e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile new file mode 100644 index 0000000000..83ed8722c0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.9-alpine + +RUN pip install websockets + +COPY app.py . + +CMD ["python", "app.py"] diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py new file mode 100644 index 0000000000..a8bcef6881 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import asyncio +import http +import signal +import sys +import time + +import websockets + + +async def slow_echo(websocket): + async for message in websocket: + # Block the event loop! This allows saturating a single asyncio + # process without opening an impractical number of connections. + time.sleep(0.1) # 100ms + await websocket.send(message) + + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + if path == "/inemuri": + loop = asyncio.get_running_loop() + loop.call_later(1, time.sleep, 10) + return http.HTTPStatus.OK, [], b"Sleeping for 10s\n" + if path == "/seppuku": + loop = asyncio.get_running_loop() + loop.call_later(1, sys.exit, 69) + return http.HTTPStatus.OK, [], b"Terminating\n" + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + slow_echo, + host="", + port=80, + process_request=health_check, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py new file mode 100644 index 0000000000..22ee4c5bd7 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import asyncio +import sys +import websockets + + +URI = "ws://localhost:32080" + + +async def run(client_id, messages): + async with websockets.connect(URI) as websocket: + for message_id in range(messages): + await websocket.send(f"{client_id}:{message_id}") + await websocket.recv() + + +async def benchmark(clients, messages): + await asyncio.wait([ + asyncio.create_task(run(client_id, messages)) + for client_id in range(clients) + ]) + + +if __name__ == "__main__": + clients, messages = int(sys.argv[1]), int(sys.argv[2]) + asyncio.run(benchmark(clients, messages)) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml new file mode 100644 index 0000000000..ba58dd62bf --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Service +metadata: + name: websockets-test +spec: + type: NodePort + ports: + - port: 80 + nodePort: 32080 + selector: + app: websockets-test +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: websockets-test +spec: + selector: + matchLabels: + app: websockets-test + template: + metadata: + labels: + app: websockets-test + spec: + containers: + - name: websockets-test + image: websockets-test:1.0 + livenessProbe: + httpGet: + path: /healthz + port: 80 + periodSeconds: 1 + ports: + - containerPort: 80 diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/app.py new file mode 100644 index 0000000000..24e6089756 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/app.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +import asyncio +import os +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.unix_serve( + echo, + path=f"{os.environ['SUPERVISOR_PROCESS_NAME']}.sock", + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf new file mode 100644 index 0000000000..67aa0086d5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf @@ -0,0 +1,25 @@ +daemon off; + +events { +} + +http { + server { + listen localhost:8080; + + location / { + proxy_http_version 1.1; + proxy_pass http://websocket; + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; + } + } + + upstream websocket { + least_conn; + server unix:websockets-test_00.sock; + server unix:websockets-test_01.sock; + server unix:websockets-test_02.sock; + server unix:websockets-test_03.sock; + } +} diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf new file mode 100644 index 0000000000..76a664d91b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf @@ -0,0 +1,7 @@ +[supervisord] + +[program:websockets-test] +command = python app.py +process_name = %(program_name)s_%(process_num)02d +numprocs = 4 +autorestart = true diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/app.py new file mode 100644 index 0000000000..4ca34d23bb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/app.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +import asyncio +import http +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=8080, + process_request=health_check, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/requirements.txt new file mode 100644 index 0000000000..14774b465e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/render/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/app.py new file mode 100644 index 0000000000..bf61983ef7 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/app.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=8080, + reuse_port=True, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf new file mode 100644 index 0000000000..76a664d91b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf @@ -0,0 +1,7 @@ +[supervisord] + +[program:websockets-test] +command = python app.py +process_name = %(program_name)s_%(process_num)02d +numprocs = 4 +autorestart = true diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/django/authentication.py b/testing/web-platform/tests/tools/third_party/websockets/example/django/authentication.py new file mode 100644 index 0000000000..f6dad0f55e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/django/authentication.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio + +import django +import websockets + +django.setup() + +from sesame.utils import get_user +from websockets.frames import CloseCode + + +async def handler(websocket): + sesame = await websocket.recv() + user = await asyncio.to_thread(get_user, sesame) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + await websocket.send(f"Hello {user}!") + + +async def main(): + async with websockets.serve(handler, "localhost", 8888): + await asyncio.Future() # run forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/django/notifications.py b/testing/web-platform/tests/tools/third_party/websockets/example/django/notifications.py new file mode 100644 index 0000000000..3a9ed10cf0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/django/notifications.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import asyncio +import json + +import aioredis +import django +import websockets + +django.setup() + +from django.contrib.contenttypes.models import ContentType +from sesame.utils import get_user +from websockets.frames import CloseCode + + +CONNECTIONS = {} + + +def get_content_types(user): + """Return the set of IDs of content types visible by user.""" + # This does only three database queries because Django caches + # all permissions on the first call to user.has_perm(...). + return { + ct.id + for ct in ContentType.objects.all() + if user.has_perm(f"{ct.app_label}.view_{ct.model}") + or user.has_perm(f"{ct.app_label}.change_{ct.model}") + } + + +async def handler(websocket): + """Authenticate user and register connection in CONNECTIONS.""" + sesame = await websocket.recv() + user = await asyncio.to_thread(get_user, sesame) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + ct_ids = await asyncio.to_thread(get_content_types, user) + CONNECTIONS[websocket] = {"content_type_ids": ct_ids} + try: + await websocket.wait_closed() + finally: + del CONNECTIONS[websocket] + + +async def process_events(): + """Listen to events in Redis and process them.""" + redis = aioredis.from_url("redis://127.0.0.1:6379/1") + pubsub = redis.pubsub() + await pubsub.subscribe("events") + async for message in pubsub.listen(): + if message["type"] != "message": + continue + payload = message["data"].decode() + # Broadcast event to all users who have permissions to see it. + event = json.loads(payload) + recipients = ( + websocket + for websocket, connection in CONNECTIONS.items() + if event["content_type_id"] in connection["content_type_ids"] + ) + websockets.broadcast(recipients, payload) + + +async def main(): + async with websockets.serve(handler, "localhost", 8888): + await process_events() # runs forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/django/signals.py b/testing/web-platform/tests/tools/third_party/websockets/example/django/signals.py new file mode 100644 index 0000000000..6dc827f72d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/django/signals.py @@ -0,0 +1,23 @@ +import json + +from django.contrib.admin.models import LogEntry +from django.db.models.signals import post_save +from django.dispatch import receiver + +from django_redis import get_redis_connection + + +@receiver(post_save, sender=LogEntry) +def publish_event(instance, **kwargs): + event = { + "model": instance.content_type.name, + "object": instance.object_repr, + "message": instance.get_change_message(), + "timestamp": instance.action_time.isoformat(), + "user": str(instance.user), + "content_type_id": instance.content_type_id, + "object_id": instance.object_id, + } + connection = get_redis_connection("default") + payload = json.dumps(event) + connection.publish("events", payload) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/echo.py b/testing/web-platform/tests/tools/third_party/websockets/example/echo.py new file mode 100644 index 0000000000..2e47e52d94 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/echo.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import asyncio +from websockets.server import serve + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + +async def main(): + async with serve(echo, "localhost", 8765): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/faq/health_check_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/faq/health_check_server.py new file mode 100644 index 0000000000..7b8bded772 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/faq/health_check_server.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +import asyncio +import http +import websockets + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + +async def main(): + async with websockets.serve( + echo, "localhost", 8765, + process_request=health_check, + ): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_client.py new file mode 100644 index 0000000000..539dd0304a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_client.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import asyncio +import signal +import websockets + +async def client(): + uri = "ws://localhost:8765" + async with websockets.connect(uri) as websocket: + # Close the connection when receiving SIGTERM. + loop = asyncio.get_running_loop() + loop.add_signal_handler( + signal.SIGTERM, loop.create_task, websocket.close()) + + # Process messages received on the connection. + async for message in websocket: + ... + +asyncio.run(client()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_server.py new file mode 100644 index 0000000000..1bcc9c90ba --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/faq/shutdown_server.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import asyncio +import signal +import websockets + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + +async def server(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve(echo, "localhost", 8765): + await stop + +asyncio.run(server()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/hello.py b/testing/web-platform/tests/tools/third_party/websockets/example/hello.py new file mode 100644 index 0000000000..a3ce0699ee --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/hello.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +import asyncio +from websockets.sync.client import connect + +def hello(): + with connect("ws://localhost:8765") as websocket: + websocket.send("Hello world!") + message = websocket.recv() + print(f"Received: {message}") + +hello() diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py new file mode 100644 index 0000000000..164732152f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +# WS client example with HTTP Basic Authentication + +import asyncio +import websockets + +async def hello(): + uri = "ws://mary:p@ssw0rd@localhost:8765" + async with websockets.connect(uri) as websocket: + greeting = await websocket.recv() + print(greeting) + +asyncio.run(hello()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py new file mode 100644 index 0000000000..d2efeb7e53 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# Server example with HTTP Basic Authentication over TLS + +import asyncio +import websockets + +async def hello(websocket): + greeting = f"Hello {websocket.username}!" + await websocket.send(greeting) + +async def main(): + async with websockets.serve( + hello, "localhost", 8765, + create_protocol=websockets.basic_auth_protocol_factory( + realm="example", credentials=("mary", "p@ssw0rd") + ), + ): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_client.py new file mode 100644 index 0000000000..9261567303 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_client.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# WS client example connecting to a Unix socket + +import asyncio +import os.path +import websockets + +async def hello(): + socket_path = os.path.join(os.path.dirname(__file__), "socket") + async with websockets.unix_connect(socket_path) as websocket: + name = input("What's your name? ") + await websocket.send(name) + print(f">>> {name}") + + greeting = await websocket.recv() + print(f"<<< {greeting}") + +asyncio.run(hello()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_server.py new file mode 100644 index 0000000000..335039c351 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/legacy/unix_server.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# WS server example listening on a Unix socket + +import asyncio +import os.path +import websockets + +async def hello(websocket): + name = await websocket.recv() + print(f"<<< {name}") + + greeting = f"Hello {name}!" + + await websocket.send(greeting) + print(f">>> {greeting}") + +async def main(): + socket_path = os.path.join(os.path.dirname(__file__), "socket") + async with websockets.unix_serve(hello, socket_path): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/logging/json_log_formatter.py b/testing/web-platform/tests/tools/third_party/websockets/example/logging/json_log_formatter.py new file mode 100644 index 0000000000..b8fc8d6dc9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/logging/json_log_formatter.py @@ -0,0 +1,33 @@ +import json +import logging +import datetime + +class JSONFormatter(logging.Formatter): + """ + Render logs as JSON. + + To add details to a log record, store them in a ``event_data`` + custom attribute. This dict is merged into the event. + + """ + def __init__(self): + pass # override logging.Formatter constructor + + def format(self, record): + event = { + "timestamp": self.getTimestamp(record.created), + "message": record.getMessage(), + "level": record.levelname, + "logger": record.name, + } + event_data = getattr(record, "event_data", None) + if event_data: + event.update(event_data) + if record.exc_info: + event["exc_info"] = self.formatException(record.exc_info) + if record.stack_info: + event["stack_info"] = self.formatStack(record.stack_info) + return json.dumps(event) + + def getTimestamp(self, created): + return datetime.datetime.utcfromtimestamp(created).isoformat() diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client.py new file mode 100644 index 0000000000..8d588c2b0e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import asyncio +import websockets + +async def hello(): + uri = "ws://localhost:8765" + async with websockets.connect(uri) as websocket: + name = input("What's your name? ") + + await websocket.send(name) + print(f">>> {name}") + + greeting = await websocket.recv() + print(f"<<< {greeting}") + +if __name__ == "__main__": + asyncio.run(hello()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client_secure.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client_secure.py new file mode 100644 index 0000000000..f4b39f2b83 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/client_secure.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import asyncio +import pathlib +import ssl +import websockets + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +localhost_pem = pathlib.Path(__file__).with_name("localhost.pem") +ssl_context.load_verify_locations(localhost_pem) + +async def hello(): + uri = "wss://localhost:8765" + async with websockets.connect(uri, ssl=ssl_context) as websocket: + name = input("What's your name? ") + + await websocket.send(name) + print(f">>> {name}") + + greeting = await websocket.recv() + print(f"<<< {greeting}") + +if __name__ == "__main__": + asyncio.run(hello()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.css b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.css new file mode 100644 index 0000000000..e1f4b77148 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.css @@ -0,0 +1,33 @@ +body { + font-family: "Courier New", sans-serif; + text-align: center; +} +.buttons { + font-size: 4em; + display: flex; + justify-content: center; +} +.button, .value { + line-height: 1; + padding: 2rem; + margin: 2rem; + border: medium solid; + min-height: 1em; + min-width: 1em; +} +.button { + cursor: pointer; + user-select: none; +} +.minus { + color: red; +} +.plus { + color: green; +} +.value { + min-width: 2em; +} +.state { + font-size: 2em; +} diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.html b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.html new file mode 100644 index 0000000000..2e3433bd21 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket demo</title> + <link href="counter.css" rel="stylesheet"> + </head> + <body> + <div class="buttons"> + <div class="minus button">-</div> + <div class="value">?</div> + <div class="plus button">+</div> + </div> + <div class="state"> + <span class="users">?</span> online + </div> + <script src="counter.js"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.js b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.js new file mode 100644 index 0000000000..37d892a28b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.js @@ -0,0 +1,26 @@ +window.addEventListener("DOMContentLoaded", () => { + const websocket = new WebSocket("ws://localhost:6789/"); + + document.querySelector(".minus").addEventListener("click", () => { + websocket.send(JSON.stringify({ action: "minus" })); + }); + + document.querySelector(".plus").addEventListener("click", () => { + websocket.send(JSON.stringify({ action: "plus" })); + }); + + websocket.onmessage = ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "value": + document.querySelector(".value").textContent = event.value; + break; + case "users": + const users = `${event.count} user${event.count == 1 ? "" : "s"}`; + document.querySelector(".users").textContent = users; + break; + default: + console.error("unsupported event", event); + } + }; +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.py new file mode 100644 index 0000000000..566e12965e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/counter.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import asyncio +import json +import logging +import websockets + +logging.basicConfig() + +USERS = set() + +VALUE = 0 + +def users_event(): + return json.dumps({"type": "users", "count": len(USERS)}) + +def value_event(): + return json.dumps({"type": "value", "value": VALUE}) + +async def counter(websocket): + global USERS, VALUE + try: + # Register user + USERS.add(websocket) + websockets.broadcast(USERS, users_event()) + # Send current state to user + await websocket.send(value_event()) + # Manage state changes + async for message in websocket: + event = json.loads(message) + if event["action"] == "minus": + VALUE -= 1 + websockets.broadcast(USERS, value_event()) + elif event["action"] == "plus": + VALUE += 1 + websockets.broadcast(USERS, value_event()) + else: + logging.error("unsupported event: %s", event) + finally: + # Unregister user + USERS.remove(websocket) + websockets.broadcast(USERS, users_event()) + +async def main(): + async with websockets.serve(counter, "localhost", 6789): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/localhost.pem b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/localhost.pem new file mode 100644 index 0000000000..f9a30ba8f6 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/localhost.pem @@ -0,0 +1,48 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDG8iDak4UBpurI +TWjSfqJ0YVG/S56nhswehupCaIzu0xQ8wqPSs36h5t1jMexJPZfvwyvFjcV+hYpj +LMM0wMJPx9oBQEe0bsmlC66e8aF0UpSQw1aVfYoxA9BejgEyrFNE7cRbQNYFEb/5 +3HfqZKdEQA2fgQSlZ0RTRmLrD+l72iO5o2xl5bttXpqYZB2XOkyO79j/xWdu9zFE +sgZJ5ysWbqoRAGgnxjdYYr9DARd8bIE/hN3SW7mDt5v4LqCIhGn1VmrwtT3d5AuG +QPz4YEbm0t6GOlmFjIMYH5Y7pALRVfoJKRj6DGNIR1JicL+wqLV66kcVnj8WKbla +20i7fR7NAgMBAAECggEAG5yvgqbG5xvLqlFUIyMAWTbIqcxNEONcoUAIc38fUGZr +gKNjKXNQOBha0dG0AdZSqCxmftzWdGEEfA9SaJf4YCpUz6ekTB60Tfv5GIZg6kwr +4ou6ELWD4Jmu6fC7qdTRGdgGUMQG8F0uT/eRjS67KHXbbi/x/SMAEK7MO+PRfCbj ++JGzS9Ym9mUweINPotgjHdDGwwd039VWYS+9A+QuNK27p3zq4hrWRb4wshSC8fKy +oLoe4OQt81aowpX9k6mAU6N8vOmP8/EcQHYC+yFIIDZB2EmDP07R1LUEH3KJnzo7 +plCK1/kYPhX0a05cEdTpXdKa74AlvSRkS11sGqfUAQKBgQDj1SRv0AUGsHSA0LWx +a0NT1ZLEXCG0uqgdgh0sTqIeirQsPROw3ky4lH5MbjkfReArFkhHu3M6KoywEPxE +wanSRh/t1qcNjNNZUvFoUzAKVpb33RLkJppOTVEWPt+wtyDlfz1ZAXzMV66tACrx +H2a3v0ZWUz6J+x/dESH5TTNL4QKBgQDfirmknp408pwBE+bulngKy0QvU09En8H0 +uvqr8q4jCXqJ1tXon4wsHg2yF4Fa37SCpSmvONIDwJvVWkkYLyBHKOns/fWCkW3n +hIcYx0q2jgcoOLU0uoaM9ArRXhIxoWqV/KGkQzN+3xXC1/MxZ5OhyxBxfPCPIYIN +YN3M1t/QbQKBgDImhsC+D30rdlmsl3IYZFed2ZKznQ/FTqBANd+8517FtWdPgnga +VtUCitKUKKrDnNafLwXrMzAIkbNn6b/QyWrp2Lln2JnY9+TfpxgJx7de3BhvZ2sl +PC4kQsccy+yAQxOBcKWY+Dmay251bP5qpRepWPhDlq6UwqzMyqev4KzBAoGAWDMi +IEO9ZGK9DufNXCHeZ1PgKVQTmJ34JxmHQkTUVFqvEKfFaq1Y3ydUfAouLa7KSCnm +ko42vuhGFB41bOdbMvh/o9RoBAZheNGfhDVN002ioUoOpSlbYU4A3q7hOtfXeCpf +lLI3JT3cFi6ic8HMTDAU4tJLEA5GhATOPr4hPNkCgYB8jTYGcLvoeFaLEveg0kS2 +cz6ZXGLJx5m1AOQy5g9FwGaW+10lr8TF2k3AldwoiwX0R6sHAf/945aGU83ms5v9 +PB9/x66AYtSRUos9MwB4y1ur4g6FiXZUBgTJUqzz2nehPCyGjYhh49WucjszqcjX +chS1bKZOY+1knWq8xj5Qyg== +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDTTCCAjWgAwIBAgIJAOjte6l+03jvMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV +BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp +bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MDUwNTE2NTkyOVoYDzIwNjAwNTA0 +MTY1OTI5WjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM +EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMbyINqThQGm6shNaNJ+onRhUb9LnqeGzB6G +6kJojO7TFDzCo9KzfqHm3WMx7Ek9l+/DK8WNxX6FimMswzTAwk/H2gFAR7RuyaUL +rp7xoXRSlJDDVpV9ijED0F6OATKsU0TtxFtA1gURv/ncd+pkp0RADZ+BBKVnRFNG +YusP6XvaI7mjbGXlu21emphkHZc6TI7v2P/FZ273MUSyBknnKxZuqhEAaCfGN1hi +v0MBF3xsgT+E3dJbuYO3m/guoIiEafVWavC1Pd3kC4ZA/PhgRubS3oY6WYWMgxgf +ljukAtFV+gkpGPoMY0hHUmJwv7CotXrqRxWePxYpuVrbSLt9Hs0CAwEAAaMwMC4w +LAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G +CSqGSIb3DQEBCwUAA4IBAQC9TsTxTEvqHPUS6sfvF77eG0D6HLOONVN91J+L7LiX +v3bFeS1xbUS6/wIxZi5EnAt/te5vaHk/5Q1UvznQP4j2gNoM6lH/DRkSARvRitVc +H0qN4Xp2Yk1R9VEx4ZgArcyMpI+GhE4vJRx1LE/hsuAzw7BAdsTt9zicscNg2fxO +3ao/eBcdaC6n9aFYdE6CADMpB1lCX2oWNVdj6IavQLu7VMc+WJ3RKncwC9th+5OP +ISPvkVZWf25rR2STmvvb0qEm3CZjk4Xd7N+gxbKKUvzEgPjrLSWzKKJAWHjCLugI +/kQqhpjWVlTbtKzWz5bViqCjSbrIPpU2MgG9AUV9y3iV +-----END CERTIFICATE----- diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server.py new file mode 100644 index 0000000000..31b1829729 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import asyncio +import websockets + +async def hello(websocket): + name = await websocket.recv() + print(f"<<< {name}") + + greeting = f"Hello {name}!" + + await websocket.send(greeting) + print(f">>> {greeting}") + +async def main(): + async with websockets.serve(hello, "localhost", 8765): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server_secure.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server_secure.py new file mode 100644 index 0000000000..de41d30dc0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/server_secure.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import asyncio +import pathlib +import ssl +import websockets + +async def hello(websocket): + name = await websocket.recv() + print(f"<<< {name}") + + greeting = f"Hello {name}!" + + await websocket.send(greeting) + print(f">>> {greeting}") + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +localhost_pem = pathlib.Path(__file__).with_name("localhost.pem") +ssl_context.load_cert_chain(localhost_pem) + +async def main(): + async with websockets.serve(hello, "localhost", 8765, ssl=ssl_context): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.html b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.html new file mode 100644 index 0000000000..b1c93b141d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket demo</title> + </head> + <body> + <script src="show_time.js"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.js b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.js new file mode 100644 index 0000000000..26bed7ec9e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.js @@ -0,0 +1,12 @@ +window.addEventListener("DOMContentLoaded", () => { + const messages = document.createElement("ul"); + document.body.appendChild(messages); + + const websocket = new WebSocket("ws://localhost:5678/"); + websocket.onmessage = ({ data }) => { + const message = document.createElement("li"); + const content = document.createTextNode(data); + message.appendChild(content); + messages.appendChild(message); + }; +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.py new file mode 100644 index 0000000000..a83078e8a9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import asyncio +import datetime +import random +import websockets + +async def show_time(websocket): + while True: + message = datetime.datetime.utcnow().isoformat() + "Z" + await websocket.send(message) + await asyncio.sleep(random.random() * 2 + 1) + +async def main(): + async with websockets.serve(show_time, "localhost", 5678): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time_2.py b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time_2.py new file mode 100644 index 0000000000..08e87f5931 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/quickstart/show_time_2.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +import asyncio +import datetime +import random +import websockets + +CONNECTIONS = set() + +async def register(websocket): + CONNECTIONS.add(websocket) + try: + await websocket.wait_closed() + finally: + CONNECTIONS.remove(websocket) + +async def show_time(): + while True: + message = datetime.datetime.utcnow().isoformat() + "Z" + websockets.broadcast(CONNECTIONS, message) + await asyncio.sleep(random.random() * 2 + 1) + +async def main(): + async with websockets.serve(register, "localhost", 5678): + await show_time() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css new file mode 100644 index 0000000000..27f0baf6e4 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css @@ -0,0 +1,105 @@ +/* General layout */ + +body { + background-color: white; + display: flex; + flex-direction: column-reverse; + justify-content: center; + align-items: center; + margin: 0; + min-height: 100vh; +} + +/* Action buttons */ + +.actions { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: flex-end; + width: 720px; + height: 100px; +} + +.action { + color: darkgray; + font-family: "Helvetica Neue", sans-serif; + font-size: 20px; + line-height: 20px; + font-weight: 300; + text-align: center; + text-decoration: none; + text-transform: uppercase; + padding: 20px; + width: 120px; +} + +.action:hover { + background-color: darkgray; + color: white; + font-weight: 700; +} + +.action[href=""] { + display: none; +} + +/* Connect Four board */ + +.board { + background-color: blue; + display: flex; + flex-direction: row; + padding: 0 10px; + position: relative; +} + +.board::before, +.board::after { + background-color: blue; + content: ""; + height: 720px; + width: 20px; + position: absolute; +} + +.board::before { + left: -20px; +} + +.board::after { + right: -20px; +} + +.column { + display: flex; + flex-direction: column-reverse; + padding: 10px; +} + +.cell { + border-radius: 50%; + width: 80px; + height: 80px; + margin: 10px 0; +} + +.empty { + background-color: white; +} + +.column:hover .empty { + background-color: lightgray; +} + +.column:hover .empty ~ .empty { + background-color: white; +} + +.red { + background-color: red; +} + +.yellow { + background-color: yellow; +} diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js new file mode 100644 index 0000000000..cb5eb9fa27 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js @@ -0,0 +1,45 @@ +const PLAYER1 = "red"; + +const PLAYER2 = "yellow"; + +function createBoard(board) { + // Inject stylesheet. + const linkElement = document.createElement("link"); + linkElement.href = import.meta.url.replace(".js", ".css"); + linkElement.rel = "stylesheet"; + document.head.append(linkElement); + // Generate board. + for (let column = 0; column < 7; column++) { + const columnElement = document.createElement("div"); + columnElement.className = "column"; + columnElement.dataset.column = column; + for (let row = 0; row < 6; row++) { + const cellElement = document.createElement("div"); + cellElement.className = "cell empty"; + cellElement.dataset.column = column; + columnElement.append(cellElement); + } + board.append(columnElement); + } +} + +function playMove(board, player, column, row) { + // Check values of arguments. + if (player !== PLAYER1 && player !== PLAYER2) { + throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`); + } + const columnElement = board.querySelectorAll(".column")[column]; + if (columnElement === undefined) { + throw new RangeError("column must be between 0 and 6."); + } + const cellElement = columnElement.querySelectorAll(".cell")[row]; + if (cellElement === undefined) { + throw new RangeError("row must be between 0 and 5."); + } + // Place checker in cell. + if (!cellElement.classList.replace("empty", player)) { + throw new Error("cell must be empty."); + } +} + +export { PLAYER1, PLAYER2, createBoard, playMove }; diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py new file mode 100644 index 0000000000..0a61e7c7ee --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py @@ -0,0 +1,62 @@ +__all__ = ["PLAYER1", "PLAYER2", "Connect4"] + +PLAYER1, PLAYER2 = "red", "yellow" + + +class Connect4: + """ + A Connect Four game. + + Play moves with :meth:`play`. + + Get past moves with :attr:`moves`. + + Check for a victory with :attr:`winner`. + + """ + + def __init__(self): + self.moves = [] + self.top = [0 for _ in range(7)] + self.winner = None + + @property + def last_player(self): + """ + Player who played the last move. + + """ + return PLAYER1 if len(self.moves) % 2 else PLAYER2 + + @property + def last_player_won(self): + """ + Whether the last move is winning. + + """ + b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2]) + return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9]) + + def play(self, player, column): + """ + Play a move in a column. + + Returns the row where the checker lands. + + Raises :exc:`RuntimeError` if the move is illegal. + + """ + if player == self.last_player: + raise RuntimeError("It isn't your turn.") + + row = self.top[column] + if row == 6: + raise RuntimeError("This slot is full.") + + self.moves.append((player, column, row)) + self.top[column] += 1 + + if self.winner is None and self.last_player_won: + self.winner = self.last_player + + return row diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico new file mode 100644 index 0000000000..602c14e4eb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico @@ -0,0 +1,2 @@ + h& ��( >��-=��v=��`@��B��U=���;���;���;���B��#J��VE���A���=���<���;���<���R��WN���I���E���E��%>���;���;���;���=��*\��VU���Q���M���N��$K��cE���A���=���D���`����q9 ^���]���Z���U��$R��dM���I���F���g��j�j1��i0��k1Cf��b���_��#[��eU���Q���N���R���t8w�k2��i0��k19d��J]���Y���U���R���|>s�v:��r7��k3�����k2u�j50f��da���]���^����Gk�~@��z>��w<Ҧs@�l2��q��zE�h��;p��ēN��I���C��Bѳ�@�x<��o5��M��o7�ȔOJ��M���J���F(�A��z=��v:��p5ɪw3ϘS%ŒP���L���JﳂC��~A��{>ɪwDʖS�ŒP���M���J���CŻ�D͗UBƔPyÏMY̙f����������������( @ <��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��U��U��U��U��U��U��U��U��B��<���<���<���;���<���<��M<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��A��3<���;���;���;���;���;���;���;���U��U��U��U��U��U��U��U��U��U��U��U��U��U��U��>��>��>��>��>��>��>��F��3A���?���>���;���;���;���;���;���;���<���<��<��<��<��<��<��<��<��<��<��<��<��<��<��=��=��=��=��=��=��J��4F���D���B���?���>���<���;���;���;���;���;���>��->��>��>��>��>��>��>��>��>��>��>��>��>��<��<��<��<��<��M��5J���G���E���C���B���@���=���<���;���;���;���;���=���=��=��=��=��=��=��=��=��=��=��=��=��=��U��U��U��U��R��5N���K���I���G���E���C���B���B��A��'<���;���;���;���;���;���<��Y<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��6S���Q���N���M���K���I���H���H��H��B��U@���>���<���;���;���;���;���<���U��U��U��U��U��U��U��U��U��U���j2�j2Z��6V���T���R���P���O���M���L���L��L��H��JE���A���@���=���<���;���;���;���;���<���<��<��<��<��<��<��<��<��<���k2^��1Z���X���V���T���S���P���P���P��P��K��KH���F���C���B���@���=���<���@���a���}�k��zOŜj2$�j2�j2�j2�j2�j2�j2�j2�j2�i1_���^���\���Z���X���V���V���V��V��M��LL���I���G���F���C���B���@����m��i1��i0��i0��i0��k2u�k2�k2�k2�k2�k2�k2�k2�k2�k1b���_���]���[���Z���Z���Z��Z��T��LR���N���M���J���I���H���G��sG���r6/�j0��i0��i0��i0��i1��i1�i1�i1�i1�i1�i1�i1�i1�k1d���a���`���]���]���]��]��V��MU���R���Q���N���M���K���J��rJ��J���r7p�l2��j1��i0��i0��k1��k1�k1�k1�k1�k1�k1�k1�k1��U���f��sa���a��_a��a��\��NW���V���T���R���P���O���M��qM��M���t8e�q6��o4��l2��j1��i0��k1X�k1�k1�k1�k1�k1�k1�k1�k1��U��U��U��U��U��Ub��F^���\���Z���X���W���T���T��pT��T���{<f�w;��s8��q6��o5��k2��j1�q9 �q9�q9�j2W��U��U��U��U��U�i0�i0�i0�i0�i0�i0a���_���^���[���Z���X���W��pW��W���~@g�z>��x<��v:��s8��r7��p5��l2B�l2�l2�j1}�i0��j0���U��U��U��U�i0�i0�i0�i0�i0���d���a���`���^���[���\��o\��\����Eg��B��~@��z=��y<��v:��t9��r7S�r7�r7�k3~�i0��|J��i0��i0��i0�i0�i0�j1�j1�j1�j1�j1�j1g���c���a���_���_��n_��_����JS��E���C���B��|?��z=��x<��v;R�v;�v;�n4�l2���\�����è���i0�i0�i0�i0�k1�k1�k1�k1�k1�k1���i��ff���d��Bd��d��ĝN +��I���H���E���C��A��|?��~?Q�~?�~?�t8�q6��n4��t=�Ѽ���N��j1љj1�j1�j1�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5M`��J���H���G���D���C���BQ��B��B�z>��t9��s8��q6��m3��l3��k1��k19�k1�k1�k1�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5ÐO���M���J���H���G���G^��G��G�}?��z=��y<��u9��s8��o5��o4��o5>�o5�o5�o5�o5�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;ƑO�ÐO���L���J���I���G2��G��C���B��|?��z=��v:��u9��t7��s5>�s5�s5�s5�s5�s5�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?ʕTtőO�ÐN���M���I���I귆Gʵ�D���C��~@��|?��z=��x<�u;=�u;�u;�u;�u;�u;�u;��D��D��D��D��D��D��D��D��D��D��D��D̙W#ɖS�ŒP�ÐN���K���J���H���F���D���C��}@��|?�}?=�}?�}?�}?�}?�}?�}?�}?��I��I��I��I��I��I��I��I��I��I��I��I��I̘T�ǓQ�őO�ÏN���L���J���I���F���D���C�D<��D��D��D��D��D��D��D��DUUUUUUUUUUUUU��U̗T�ǔQ�ŒP�ÏN���K���J���H���F鶄I8��I��I��I��I��I��I��I��I��I˘WOȓQ�œP�ÑN���Lھ�L�UUUUUUUUUUU�����������������?��?����������0��`������@��������������0����0�� ��?����������������������� diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py new file mode 100644 index 0000000000..3b0fbd7868 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +import asyncio +import itertools +import json + +import websockets + +from connect4 import PLAYER1, PLAYER2, Connect4 + + +async def handler(websocket): + # Initialize a Connect Four game. + game = Connect4() + + # Players take alternate turns, using the same browser. + turns = itertools.cycle([PLAYER1, PLAYER2]) + player = next(turns) + + async for message in websocket: + # Parse a "play" event from the UI. + event = json.loads(message) + assert event["type"] == "play" + column = event["column"] + + try: + # Play the move. + row = game.play(player, column) + except RuntimeError as exc: + # Send an "error" event if the move was illegal. + event = { + "type": "error", + "message": str(exc), + } + await websocket.send(json.dumps(event)) + continue + + # Send a "play" event to update the UI. + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + + # If move is winning, send a "win" event. + if game.winner is not None: + event = { + "type": "win", + "player": game.winner, + } + await websocket.send(json.dumps(event)) + + # Alternate turns. + player = next(turns) + + +async def main(): + async with websockets.serve(handler, "", 8001): + await asyncio.Future() # run forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html new file mode 100644 index 0000000000..8e38e89922 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Connect Four</title> + </head> + <body> + <div class="board"></div> + <script src="main.js" type="module"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js new file mode 100644 index 0000000000..dd28f9a6a8 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js @@ -0,0 +1,53 @@ +import { createBoard, playMove } from "./connect4.js"; + +function showMessage(message) { + window.setTimeout(() => window.alert(message), 50); +} + +function receiveMoves(board, websocket) { + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "play": + // Update the UI with the move. + playMove(board, event.player, event.column, event.row); + break; + case "win": + showMessage(`Player ${event.player} wins!`); + // No further messages are expected; close the WebSocket connection. + websocket.close(1000); + break; + case "error": + showMessage(event.message); + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +} + +function sendMoves(board, websocket) { + // When clicking a column, send a "play" event for a move in that column. + board.addEventListener("click", ({ target }) => { + const column = target.dataset.column; + // Ignore clicks outside a column. + if (column === undefined) { + return; + } + const event = { + type: "play", + column: parseInt(column, 10), + }; + websocket.send(JSON.stringify(event)); + }); +} + +window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket("ws://localhost:8001/"); + receiveMoves(board, websocket); + sendMoves(board, websocket); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py new file mode 100644 index 0000000000..2693d4304d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python + +import asyncio +import json +import secrets + +import websockets + +from connect4 import PLAYER1, PLAYER2, Connect4 + + +JOIN = {} + +WATCH = {} + + +async def error(websocket, message): + """ + Send an error message. + + """ + event = { + "type": "error", + "message": message, + } + await websocket.send(json.dumps(event)) + + +async def replay(websocket, game): + """ + Send previous moves. + + """ + # Make a copy to avoid an exception if game.moves changes while iteration + # is in progress. If a move is played while replay is running, moves will + # be sent out of order but each move will be sent once and eventually the + # UI will be consistent. + for player, column, row in game.moves.copy(): + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + + +async def play(websocket, game, player, connected): + """ + Receive and process moves from a player. + + """ + async for message in websocket: + # Parse a "play" event from the UI. + event = json.loads(message) + assert event["type"] == "play" + column = event["column"] + + try: + # Play the move. + row = game.play(player, column) + except RuntimeError as exc: + # Send an "error" event if the move was illegal. + await error(websocket, str(exc)) + continue + + # Send a "play" event to update the UI. + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + websockets.broadcast(connected, json.dumps(event)) + + # If move is winning, send a "win" event. + if game.winner is not None: + event = { + "type": "win", + "player": game.winner, + } + websockets.broadcast(connected, json.dumps(event)) + + +async def start(websocket): + """ + Handle a connection from the first player: start a new game. + + """ + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access tokens. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + watch_key = secrets.token_urlsafe(12) + WATCH[watch_key] = game, connected + + try: + # Send the secret access tokens to the browser of the first player, + # where they'll be used for building "join" and "watch" links. + event = { + "type": "init", + "join": join_key, + "watch": watch_key, + } + await websocket.send(json.dumps(event)) + # Receive and process moves from the first player. + await play(websocket, game, PLAYER1, connected) + finally: + del JOIN[join_key] + del WATCH[watch_key] + + +async def join(websocket, join_key): + """ + Handle a connection from the second player: join an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = JOIN[join_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send the first move, in case the first player already played it. + await replay(websocket, game) + # Receive and process moves from the second player. + await play(websocket, game, PLAYER2, connected) + finally: + connected.remove(websocket) + + +async def watch(websocket, watch_key): + """ + Handle a connection from a spectator: watch an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = WATCH[watch_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send previous moves, in case the game already started. + await replay(websocket, game) + # Keep the connection open, but don't receive any messages. + await websocket.wait_closed() + finally: + connected.remove(websocket) + + +async def handler(websocket): + """ + Handle a connection and dispatch it according to who is connecting. + + """ + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + if "join" in event: + # Second player joins an existing game. + await join(websocket, event["join"]) + elif "watch" in event: + # Spectator watches an existing game. + await watch(websocket, event["watch"]) + else: + # First player starts a new game. + await start(websocket) + + +async def main(): + async with websockets.serve(handler, "", 8001): + await asyncio.Future() # run forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html new file mode 100644 index 0000000000..1a16f72a25 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Connect Four</title> + </head> + <body> + <div class="actions"> + <a class="action new" href="/">New</a> + <a class="action join" href="">Join</a> + <a class="action watch" href="">Watch</a> + </div> + <div class="board"></div> + <script src="main.js" type="module"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js new file mode 100644 index 0000000000..d38a0140ac --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js @@ -0,0 +1,83 @@ +import { createBoard, playMove } from "./connect4.js"; + +function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event according to who is connecting. + const params = new URLSearchParams(window.location.search); + let event = { type: "init" }; + if (params.has("join")) { + // Second player joins an existing game. + event.join = params.get("join"); + } else if (params.has("watch")) { + // Spectator watches an existing game. + event.watch = params.get("watch"); + } else { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); +} + +function showMessage(message) { + window.setTimeout(() => window.alert(message), 50); +} + +function receiveMoves(board, websocket) { + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "init": + // Create links for inviting the second player and spectators. + document.querySelector(".join").href = "?join=" + event.join; + document.querySelector(".watch").href = "?watch=" + event.watch; + break; + case "play": + // Update the UI with the move. + playMove(board, event.player, event.column, event.row); + break; + case "win": + showMessage(`Player ${event.player} wins!`); + // No further messages are expected; close the WebSocket connection. + websocket.close(1000); + break; + case "error": + showMessage(event.message); + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +} + +function sendMoves(board, websocket) { + // Don't send moves for a spectator watching a game. + const params = new URLSearchParams(window.location.search); + if (params.has("watch")) { + return; + } + + // When clicking a column, send a "play" event for a move in that column. + board.addEventListener("click", ({ target }) => { + const column = target.dataset.column; + // Ignore clicks outside a column. + if (column === undefined) { + return; + } + const event = { + type: "play", + column: parseInt(column, 10), + }; + websocket.send(JSON.stringify(event)); + }); +} + +window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket("ws://localhost:8001/"); + initGame(websocket); + receiveMoves(board, websocket); + sendMoves(board, websocket); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile new file mode 100644 index 0000000000..2e35818f67 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile @@ -0,0 +1 @@ +web: python app.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py new file mode 100644 index 0000000000..c2ee020d20 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python + +import asyncio +import json +import os +import secrets +import signal + +import websockets + +from connect4 import PLAYER1, PLAYER2, Connect4 + + +JOIN = {} + +WATCH = {} + + +async def error(websocket, message): + """ + Send an error message. + + """ + event = { + "type": "error", + "message": message, + } + await websocket.send(json.dumps(event)) + + +async def replay(websocket, game): + """ + Send previous moves. + + """ + # Make a copy to avoid an exception if game.moves changes while iteration + # is in progress. If a move is played while replay is running, moves will + # be sent out of order but each move will be sent once and eventually the + # UI will be consistent. + for player, column, row in game.moves.copy(): + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + + +async def play(websocket, game, player, connected): + """ + Receive and process moves from a player. + + """ + async for message in websocket: + # Parse a "play" event from the UI. + event = json.loads(message) + assert event["type"] == "play" + column = event["column"] + + try: + # Play the move. + row = game.play(player, column) + except RuntimeError as exc: + # Send an "error" event if the move was illegal. + await error(websocket, str(exc)) + continue + + # Send a "play" event to update the UI. + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + websockets.broadcast(connected, json.dumps(event)) + + # If move is winning, send a "win" event. + if game.winner is not None: + event = { + "type": "win", + "player": game.winner, + } + websockets.broadcast(connected, json.dumps(event)) + + +async def start(websocket): + """ + Handle a connection from the first player: start a new game. + + """ + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access tokens. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + watch_key = secrets.token_urlsafe(12) + WATCH[watch_key] = game, connected + + try: + # Send the secret access tokens to the browser of the first player, + # where they'll be used for building "join" and "watch" links. + event = { + "type": "init", + "join": join_key, + "watch": watch_key, + } + await websocket.send(json.dumps(event)) + # Receive and process moves from the first player. + await play(websocket, game, PLAYER1, connected) + finally: + del JOIN[join_key] + del WATCH[watch_key] + + +async def join(websocket, join_key): + """ + Handle a connection from the second player: join an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = JOIN[join_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send the first move, in case the first player already played it. + await replay(websocket, game) + # Receive and process moves from the second player. + await play(websocket, game, PLAYER2, connected) + finally: + connected.remove(websocket) + + +async def watch(websocket, watch_key): + """ + Handle a connection from a spectator: watch an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = WATCH[watch_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send previous moves, in case the game already started. + await replay(websocket, game) + # Keep the connection open, but don't receive any messages. + await websocket.wait_closed() + finally: + connected.remove(websocket) + + +async def handler(websocket): + """ + Handle a connection and dispatch it according to who is connecting. + + """ + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + if "join" in event: + # Second player joins an existing game. + await join(websocket, event["join"]) + elif "watch" in event: + # Spectator watches an existing game. + await watch(websocket, event["watch"]) + else: + # First player starts a new game. + await start(websocket) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + port = int(os.environ.get("PORT", "8001")) + async with websockets.serve(handler, "", port): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html new file mode 100644 index 0000000000..1a16f72a25 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Connect Four</title> + </head> + <body> + <div class="actions"> + <a class="action new" href="/">New</a> + <a class="action join" href="">Join</a> + <a class="action watch" href="">Watch</a> + </div> + <div class="board"></div> + <script src="main.js" type="module"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js new file mode 100644 index 0000000000..3000fa2f78 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js @@ -0,0 +1,93 @@ +import { createBoard, playMove } from "./connect4.js"; + +function getWebSocketServer() { + if (window.location.host === "python-websockets.github.io") { + return "wss://websockets-tutorial.herokuapp.com/"; + } else if (window.location.host === "localhost:8000") { + return "ws://localhost:8001/"; + } else { + throw new Error(`Unsupported host: ${window.location.host}`); + } +} + +function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event according to who is connecting. + const params = new URLSearchParams(window.location.search); + let event = { type: "init" }; + if (params.has("join")) { + // Second player joins an existing game. + event.join = params.get("join"); + } else if (params.has("watch")) { + // Spectator watches an existing game. + event.watch = params.get("watch"); + } else { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); +} + +function showMessage(message) { + window.setTimeout(() => window.alert(message), 50); +} + +function receiveMoves(board, websocket) { + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "init": + // Create links for inviting the second player and spectators. + document.querySelector(".join").href = "?join=" + event.join; + document.querySelector(".watch").href = "?watch=" + event.watch; + break; + case "play": + // Update the UI with the move. + playMove(board, event.player, event.column, event.row); + break; + case "win": + showMessage(`Player ${event.player} wins!`); + // No further messages are expected; close the WebSocket connection. + websocket.close(1000); + break; + case "error": + showMessage(event.message); + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +} + +function sendMoves(board, websocket) { + // Don't send moves for a spectator watching a game. + const params = new URLSearchParams(window.location.search); + if (params.has("watch")) { + return; + } + + // When clicking a column, send a "play" event for a move in that column. + board.addEventListener("click", ({ target }) => { + const column = target.dataset.column; + // Ignore clicks outside a column. + if (column === undefined) { + return; + } + const event = { + type: "play", + column: parseInt(column, 10), + }; + websocket.send(JSON.stringify(event)); + }); +} + +window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket(getWebSocketServer()); + initGame(websocket); + receiveMoves(board, websocket); + sendMoves(board, websocket); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt new file mode 100644 index 0000000000..14774b465e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/app.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/app.py new file mode 100644 index 0000000000..039e21174b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/app.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python + +import asyncio +import http +import http.cookies +import pathlib +import signal +import urllib.parse +import uuid + +import websockets +from websockets.frames import CloseCode + + +# User accounts database + +USERS = {} + + +def create_token(user, lifetime=1): + """Create token for user and delete it once its lifetime is over.""" + token = uuid.uuid4().hex + USERS[token] = user + asyncio.get_running_loop().call_later(lifetime, USERS.pop, token) + return token + + +def get_user(token): + """Find user authenticated by token or return None.""" + return USERS.get(token) + + +# Utilities + + +def get_cookie(raw, key): + cookie = http.cookies.SimpleCookie(raw) + morsel = cookie.get(key) + if morsel is not None: + return morsel.value + + +def get_query_param(path, key): + query = urllib.parse.urlparse(path).query + params = urllib.parse.parse_qs(query) + values = params.get(key, []) + if len(values) == 1: + return values[0] + + +# Main HTTP server + +CONTENT_TYPES = { + ".css": "text/css", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".js": "text/javascript", +} + + +async def serve_html(path, request_headers): + user = get_query_param(path, "user") + path = urllib.parse.urlparse(path).path + if path == "/": + if user is None: + page = "index.html" + else: + page = "test.html" + else: + page = path[1:] + + try: + template = pathlib.Path(__file__).with_name(page) + except ValueError: + pass + else: + if template.is_file(): + headers = {"Content-Type": CONTENT_TYPES[template.suffix]} + body = template.read_bytes() + if user is not None: + token = create_token(user) + body = body.replace(b"TOKEN", token.encode()) + return http.HTTPStatus.OK, headers, body + + return http.HTTPStatus.NOT_FOUND, {}, b"Not found\n" + + +async def noop_handler(websocket): + pass + + +# Send credentials as the first message in the WebSocket connection + + +async def first_message_handler(websocket): + token = await websocket.recv() + user = get_user(token) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Add credentials to the WebSocket URI in a query parameter + + +class QueryParamProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + token = get_query_param(path, "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + +async def query_param_handler(websocket): + user = websocket.user + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Set a cookie on the domain of the WebSocket URI + + +class CookieProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + if "Upgrade" not in headers: + template = pathlib.Path(__file__).with_name(path[1:]) + headers = {"Content-Type": CONTENT_TYPES[template.suffix]} + body = template.read_bytes() + return http.HTTPStatus.OK, headers, body + + token = get_cookie(headers.get("Cookie", ""), "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + +async def cookie_handler(websocket): + user = websocket.user + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Adding credentials to the WebSocket URI in user information + + +class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + if username != "token": + return False + + user = get_user(password) + if user is None: + return False + + self.user = user + return True + + +async def user_info_handler(websocket): + user = websocket.user + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Start all five servers + + +async def main(): + # Set the stop condition when receiving SIGINT or SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGINT, stop.set_result, None) + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + noop_handler, + host="", + port=8000, + process_request=serve_html, + ), websockets.serve( + first_message_handler, + host="", + port=8001, + ), websockets.serve( + query_param_handler, + host="", + port=8002, + create_protocol=QueryParamProtocol, + ), websockets.serve( + cookie_handler, + host="", + port=8003, + create_protocol=CookieProtocol, + ), websockets.serve( + user_info_handler, + host="", + port=8004, + create_protocol=UserInfoProtocol, + ): + print("Running on http://localhost:8000/") + await stop + print("\rExiting") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.html new file mode 100644 index 0000000000..ca17358fd0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Cookie | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] Cookie</p> + <p class="ok">[OK] Cookie</p> + <p class="ko">[KO] Cookie</p> + <script src="script.js"></script> + <script src="cookie.js"></script> + <iframe src="http://localhost:8003/cookie_iframe.html" style="display: none;"></iframe> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.js new file mode 100644 index 0000000000..2cca34fcbb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie.js @@ -0,0 +1,23 @@ +// send token to iframe +window.addEventListener("DOMContentLoaded", () => { + const iframe = document.querySelector("iframe"); + iframe.addEventListener("load", () => { + iframe.contentWindow.postMessage(token, "http://localhost:8003"); + }); +}); + +// once iframe has set cookie, open WebSocket connection +window.addEventListener("message", ({ origin }) => { + if (origin !== "http://localhost:8003") { + return; + } + + const websocket = new WebSocket("ws://localhost:8003/"); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html new file mode 100644 index 0000000000..9f49ebb9a0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Cookie iframe | WebSocket Authentication</title> + </head> + <body> + <script src="cookie_iframe.js"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js new file mode 100644 index 0000000000..2d2e692e8d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js @@ -0,0 +1,9 @@ +// receive token from the parent window, set cookie and notify parent +window.addEventListener("message", ({ origin, data }) => { + if (origin !== "http://localhost:8000") { + return; + } + + document.cookie = `token=${data}; SameSite=Strict`; + window.parent.postMessage("", "http://localhost:8000"); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.html new file mode 100644 index 0000000000..4dc511a176 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>First message | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] First message</p> + <p class="ok">[OK] First message</p> + <p class="ko">[KO] First message</p> + <script src="script.js"></script> + <script src="first_message.js"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.js new file mode 100644 index 0000000000..1acf048baf --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/first_message.js @@ -0,0 +1,11 @@ +window.addEventListener("DOMContentLoaded", () => { + const websocket = new WebSocket("ws://localhost:8001/"); + websocket.onopen = () => websocket.send(token); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/index.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/index.html new file mode 100644 index 0000000000..c37deef270 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body> + <form method="GET"> + <input name="user" placeholder="username"> + </form> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.html new file mode 100644 index 0000000000..27aa454a40 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Query parameter | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] Query parameter</p> + <p class="ok">[OK] Query parameter</p> + <p class="ko">[KO] Query parameter</p> + <script src="script.js"></script> + <script src="query_param.js"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.js new file mode 100644 index 0000000000..6a54d0b6ca --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/query_param.js @@ -0,0 +1,11 @@ +window.addEventListener("DOMContentLoaded", () => { + const uri = `ws://localhost:8002/?token=${token}`; + const websocket = new WebSocket(uri); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/script.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/script.js new file mode 100644 index 0000000000..ec4e5e6709 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/script.js @@ -0,0 +1,51 @@ +var token = window.parent.token; + +function getExpectedEvents() { + return [ + { + type: "open", + }, + { + type: "message", + data: `Hello ${window.parent.user}!`, + }, + { + type: "close", + code: 1000, + reason: "", + wasClean: true, + }, + ]; +} + +function isEqual(expected, actual) { + // good enough for our purposes here! + return JSON.stringify(expected) === JSON.stringify(actual); +} + +function testStep(expected, actual) { + if (isEqual(expected, actual)) { + document.body.className = "ok"; + } else if (isEqual(expected.slice(0, actual.length), actual)) { + document.body.className = "test"; + } else { + document.body.className = "ko"; + } +} + +function runTest(websocket) { + const expected = getExpectedEvents(); + var actual = []; + websocket.addEventListener("open", ({ type }) => { + actual.push({ type }); + testStep(expected, actual); + }); + websocket.addEventListener("message", ({ type, data }) => { + actual.push({ type, data }); + testStep(expected, actual); + }); + websocket.addEventListener("close", ({ type, code, reason, wasClean }) => { + actual.push({ type, code, reason, wasClean }); + testStep(expected, actual); + }); +} diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/style.css b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/style.css new file mode 100644 index 0000000000..6e3918ccae --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/style.css @@ -0,0 +1,69 @@ +/* page layout */ + +body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 0; + height: 100vh; +} +div.title, iframe { + width: 100vw; + height: 20vh; + border: none; +} +div.title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +h1, p { + margin: 0; + width: 24em; +} + +/* text style */ + +h1, input, p { + font-family: monospace; + font-size: 3em; +} +input { + color: #333; + border: 3px solid #999; + padding: 1em; +} +input:focus { + border-color: #333; + outline: none; +} +input::placeholder { + color: #999; + opacity: 1; +} + +/* test results */ + +body.test { + background-color: #666; + color: #fff; +} +body.ok { + background-color: #090; + color: #fff; +} +body.ko { + background-color: #900; + color: #fff; +} +body > p { + display: none; +} +body > p.title, +body.test > p.test, +body.ok > p.ok, +body.ko > p.ko { + display: block; +} diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.html new file mode 100644 index 0000000000..3883d6a39e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body data-token="TOKEN"> + <div class="title"><h1>WebSocket Authentication</h1></div> + <iframe src="first_message.html"></iframe> + <iframe src="query_param.html"></iframe> + <iframe src="cookie.html"></iframe> + <iframe src="user_info.html"></iframe> + <script src="test.js"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.js new file mode 100644 index 0000000000..428830ff31 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/test.js @@ -0,0 +1,6 @@ +// for connecting to WebSocket servers +var token = document.body.dataset.token; + +// for test assertions only +const params = new URLSearchParams(window.location.search); +var user = params.get("user"); diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.html b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.html new file mode 100644 index 0000000000..7b9c99c730 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>User information | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] User information</p> + <p class="ok">[OK] User information</p> + <p class="ko">[KO] User information</p> + <script src="script.js"></script> + <script src="user_info.js"></script> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.js b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.js new file mode 100644 index 0000000000..1dab2ce4c1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/authentication/user_info.js @@ -0,0 +1,11 @@ +window.addEventListener("DOMContentLoaded", () => { + const uri = `ws://token:${token}@localhost:8004/`; + const websocket = new WebSocket(uri); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/clients.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/clients.py new file mode 100644 index 0000000000..fe39dfe051 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/clients.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +import asyncio +import statistics +import sys +import time + +import websockets + + +LATENCIES = {} + + +async def log_latency(interval): + while True: + await asyncio.sleep(interval) + p = statistics.quantiles(LATENCIES.values(), n=100) + print(f"clients = {len(LATENCIES)}") + print( + f"p50 = {p[49] / 1e6:.1f}ms, " + f"p95 = {p[94] / 1e6:.1f}ms, " + f"p99 = {p[98] / 1e6:.1f}ms" + ) + print() + + +async def client(): + try: + async with websockets.connect( + "ws://localhost:8765", + ping_timeout=None, + ) as websocket: + async for msg in websocket: + client_time = time.time_ns() + server_time = int(msg[:19].decode()) + LATENCIES[websocket] = client_time - server_time + except Exception as exc: + print(exc) + + +async def main(count, interval): + asyncio.create_task(log_latency(interval)) + clients = [] + for _ in range(count): + clients.append(asyncio.create_task(client())) + await asyncio.sleep(0.001) # 1ms between each connection + await asyncio.wait(clients) + + +if __name__ == "__main__": + try: + count = int(sys.argv[1]) + interval = float(sys.argv[2]) + except Exception as exc: + print(f"Usage: {sys.argv[0]} count interval") + print(" Connect <count> clients e.g. 1000") + print(" Report latency every <interval> seconds e.g. 1") + print() + print(exc) + else: + asyncio.run(main(count, interval)) diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/server.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/server.py new file mode 100644 index 0000000000..9c9907b7f9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/broadcast/server.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python + +import asyncio +import functools +import os +import sys +import time + +import websockets + + +CLIENTS = set() + + +async def send(websocket, message): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + + +async def relay(queue, websocket): + while True: + message = await queue.get() + await websocket.send(message) + + +class PubSub: + def __init__(self): + self.waiter = asyncio.Future() + + def publish(self, value): + waiter, self.waiter = self.waiter, asyncio.Future() + waiter.set_result((value, self.waiter)) + + async def subscribe(self): + waiter = self.waiter + while True: + value, waiter = await waiter + yield value + + __aiter__ = subscribe + + +PUBSUB = PubSub() + + +async def handler(websocket, method=None): + if method in ["default", "naive", "task", "wait"]: + CLIENTS.add(websocket) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(websocket) + elif method == "queue": + queue = asyncio.Queue() + relay_task = asyncio.create_task(relay(queue, websocket)) + CLIENTS.add(queue) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(queue) + relay_task.cancel() + elif method == "pubsub": + async for message in PUBSUB: + await websocket.send(message) + else: + raise NotImplementedError(f"unsupported method: {method}") + + +async def broadcast(method, size, delay): + """Broadcast messages at regular intervals.""" + load_average = 0 + time_average = 0 + pc1, pt1 = time.perf_counter_ns(), time.process_time_ns() + await asyncio.sleep(delay) + while True: + print(f"clients = {len(CLIENTS)}") + pc0, pt0 = time.perf_counter_ns(), time.process_time_ns() + load_average = 0.9 * load_average + 0.1 * (pt0 - pt1) / (pc0 - pc1) + print( + f"load = {(pt0 - pt1) / (pc0 - pc1) * 100:.1f}% / " + f"average = {load_average * 100:.1f}%, " + f"late = {(pc0 - pc1 - delay * 1e9) / 1e6:.1f} ms" + ) + pc1, pt1 = pc0, pt0 + + assert size > 20 + message = str(time.time_ns()).encode() + b" " + os.urandom(size - 20) + + if method == "default": + websockets.broadcast(CLIENTS, message) + elif method == "naive": + # Since the loop can yield control, make a copy of CLIENTS + # to avoid: RuntimeError: Set changed size during iteration + for websocket in CLIENTS.copy(): + await send(websocket, message) + elif method == "task": + for websocket in CLIENTS: + asyncio.create_task(send(websocket, message)) + elif method == "wait": + if CLIENTS: # asyncio.wait doesn't accept an empty list + await asyncio.wait( + [ + asyncio.create_task(send(websocket, message)) + for websocket in CLIENTS + ] + ) + elif method == "queue": + for queue in CLIENTS: + queue.put_nowait(message) + elif method == "pubsub": + PUBSUB.publish(message) + else: + raise NotImplementedError(f"unsupported method: {method}") + + pc2 = time.perf_counter_ns() + wait = delay + (pc1 - pc2) / 1e9 + time_average = 0.9 * time_average + 0.1 * (pc2 - pc1) + print( + f"broadcast = {(pc2 - pc1) / 1e6:.1f}ms / " + f"average = {time_average / 1e6:.1f}ms, " + f"wait = {wait * 1e3:.1f}ms" + ) + await asyncio.sleep(wait) + print() + + +async def main(method, size, delay): + async with websockets.serve( + functools.partial(handler, method=method), + "localhost", + 8765, + compression=None, + ping_timeout=None, + ): + await broadcast(method, size, delay) + + +if __name__ == "__main__": + try: + method = sys.argv[1] + assert method in ["default", "naive", "task", "wait", "queue", "pubsub"] + size = int(sys.argv[2]) + delay = float(sys.argv[3]) + except Exception as exc: + print(f"Usage: {sys.argv[0]} method size delay") + print(" Start a server broadcasting messages with <method> e.g. naive") + print(" Send a payload of <size> bytes every <delay> seconds") + print() + print(exc) + else: + asyncio.run(main(method, size, delay)) diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/benchmark.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/benchmark.py new file mode 100644 index 0000000000..c5b13c8fa3 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/benchmark.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python + +import getpass +import json +import pickle +import subprocess +import sys +import time +import zlib + + +CORPUS_FILE = "corpus.pkl" + +REPEAT = 10 + +WB, ML = 12, 5 # defaults used as a reference + + +def _corpus(): + OAUTH_TOKEN = getpass.getpass("OAuth Token? ") + COMMIT_API = ( + f'curl -H "Authorization: token {OAUTH_TOKEN}" ' + f"https://api.github.com/repos/python-websockets/websockets/git/commits/:sha" + ) + + commits = [] + + head = subprocess.check_output("git rev-parse HEAD", shell=True).decode().strip() + todo = [head] + seen = set() + + while todo: + sha = todo.pop(0) + commit = subprocess.check_output(COMMIT_API.replace(":sha", sha), shell=True) + commits.append(commit) + seen.add(sha) + for parent in json.loads(commit)["parents"]: + sha = parent["sha"] + if sha not in seen and sha not in todo: + todo.append(sha) + time.sleep(1) # rate throttling + + return commits + + +def corpus(): + data = _corpus() + with open(CORPUS_FILE, "wb") as handle: + pickle.dump(data, handle) + + +def _run(data): + size = {} + duration = {} + + for wbits in range(9, 16): + size[wbits] = {} + duration[wbits] = {} + + for memLevel in range(1, 10): + encoder = zlib.compressobj(wbits=-wbits, memLevel=memLevel) + encoded = [] + + t0 = time.perf_counter() + + for _ in range(REPEAT): + for item in data: + if isinstance(item, str): + item = item.encode("utf-8") + # Taken from PerMessageDeflate.encode + item = encoder.compress(item) + encoder.flush(zlib.Z_SYNC_FLUSH) + if item.endswith(b"\x00\x00\xff\xff"): + item = item[:-4] + encoded.append(item) + + t1 = time.perf_counter() + + size[wbits][memLevel] = sum(len(item) for item in encoded) + duration[wbits][memLevel] = (t1 - t0) / REPEAT + + raw_size = sum(len(item) for item in data) + + print("=" * 79) + print("Compression ratio") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{100 * (1 - size[wbits][memLevel] / raw_size):.1f}%" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + print("=" * 79) + print("CPU time") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{1000 * duration[wbits][memLevel]:.1f}ms" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + print("=" * 79) + print(f"Size vs. {WB} \\ {ML}") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{100 * (size[wbits][memLevel] / size[WB][ML] - 1):.1f}%" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + print("=" * 79) + print(f"Time vs. {WB} \\ {ML}") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{100 * (duration[wbits][memLevel] / duration[WB][ML] - 1):.1f}%" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + +def run(): + with open(CORPUS_FILE, "rb") as handle: + data = pickle.load(handle) + _run(data) + + +try: + run = globals()[sys.argv[1]] +except (KeyError, IndexError): + print(f"Usage: {sys.argv[0]} [corpus|run]") +else: + run() diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/client.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/client.py new file mode 100644 index 0000000000..3ee19ddc59 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/client.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +import asyncio +import statistics +import tracemalloc + +import websockets +from websockets.extensions import permessage_deflate + + +CLIENTS = 20 +INTERVAL = 1 / 10 # seconds + +WB, ML = 12, 5 + +MEM_SIZE = [] + + +async def client(client): + # Space out connections to make them sequential. + await asyncio.sleep(client * INTERVAL) + + tracemalloc.start() + + async with websockets.connect( + "ws://localhost:8765", + extensions=[ + permessage_deflate.ClientPerMessageDeflateFactory( + server_max_window_bits=WB, + client_max_window_bits=WB, + compress_settings={"memLevel": ML}, + ) + ], + ) as ws: + await ws.send("hello") + await ws.recv() + + await ws.send(b"hello") + await ws.recv() + + MEM_SIZE.append(tracemalloc.get_traced_memory()[0]) + tracemalloc.stop() + + # Hold connection open until the end of the test. + await asyncio.sleep(CLIENTS * INTERVAL) + + +async def clients(): + await asyncio.gather(*[client(client) for client in range(CLIENTS + 1)]) + + +asyncio.run(clients()) + + +# First connection incurs non-representative setup costs. +del MEM_SIZE[0] + +print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB") +print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB") diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/server.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/server.py new file mode 100644 index 0000000000..8d1ee3cd7c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/compression/server.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +import asyncio +import os +import signal +import statistics +import tracemalloc + +import websockets +from websockets.extensions import permessage_deflate + + +CLIENTS = 20 +INTERVAL = 1 / 10 # seconds + +WB, ML = 12, 5 + +MEM_SIZE = [] + + +async def handler(ws): + msg = await ws.recv() + await ws.send(msg) + + msg = await ws.recv() + await ws.send(msg) + + MEM_SIZE.append(tracemalloc.get_traced_memory()[0]) + tracemalloc.stop() + + tracemalloc.start() + + # Hold connection open until the end of the test. + await asyncio.sleep(CLIENTS * INTERVAL) + + +async def server(): + loop = asyncio.get_running_loop() + stop = loop.create_future() + + # Set the stop condition when receiving SIGTERM. + print("Stop the server with:") + print(f"kill -TERM {os.getpid()}") + print() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + handler, + "localhost", + 8765, + extensions=[ + permessage_deflate.ServerPerMessageDeflateFactory( + server_max_window_bits=WB, + client_max_window_bits=WB, + compress_settings={"memLevel": ML}, + ) + ], + ): + tracemalloc.start() + await stop + + +asyncio.run(server()) + + +# First connection may incur non-representative setup costs. +del MEM_SIZE[0] + +print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB") +print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB") diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py new file mode 100644 index 0000000000..e3acbe3c20 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py @@ -0,0 +1,101 @@ +"""Benchark parsing WebSocket frames.""" + +import subprocess +import sys +import timeit + +from websockets.extensions.permessage_deflate import PerMessageDeflate +from websockets.frames import Frame, Opcode +from websockets.streams import StreamReader + + +# 256kB of text, compressible by about 70%. +text = subprocess.check_output(["git", "log", "8dd8e410"], text=True) + + +def get_frame(size): + repeat, remainder = divmod(size, 256 * 1024) + payload = repeat * text + text[:remainder] + return Frame(Opcode.TEXT, payload.encode(), True) + + +def parse_frame(data, count, mask, extensions): + reader = StreamReader() + for _ in range(count): + reader.feed_data(data) + parser = Frame.parse( + reader.read_exact, + mask=mask, + extensions=extensions, + ) + try: + next(parser) + except StopIteration: + pass + else: + assert False, "parser should return frame" + reader.feed_eof() + assert reader.at_eof(), "parser should consume all data" + + +def run_benchmark(size, count, compression=False, number=100): + if compression: + extensions = [PerMessageDeflate(True, True, 12, 12, {"memLevel": 5})] + else: + extensions = [] + globals = { + "get_frame": get_frame, + "parse_frame": parse_frame, + "extensions": extensions, + } + sppf = ( + min( + timeit.repeat( + f"parse_frame(data, {count}, mask=True, extensions=extensions)", + f"data = get_frame({size})" + f".serialize(mask=True, extensions=extensions)", + number=number, + globals=globals, + ) + ) + / number + / count + * 1_000_000 + ) + cppf = ( + min( + timeit.repeat( + f"parse_frame(data, {count}, mask=False, extensions=extensions)", + f"data = get_frame({size})" + f".serialize(mask=False, extensions=extensions)", + number=number, + globals=globals, + ) + ) + / number + / count + * 1_000_000 + ) + print(f"{size}\t{compression}\t{sppf:.2f}\t{cppf:.2f}") + + +if __name__ == "__main__": + print("Sizes are in bytes. Times are in µs per frame.", file=sys.stderr) + print("Run `tabs -16` for clean output. Pipe stdout to TSV for saving.") + print(file=sys.stderr) + + print("size\tcompression\tserver\tclient") + run_benchmark(size=8, count=1000, compression=False) + run_benchmark(size=60, count=1000, compression=False) + run_benchmark(size=500, count=1000, compression=False) + run_benchmark(size=4_000, count=1000, compression=False) + run_benchmark(size=30_000, count=200, compression=False) + run_benchmark(size=250_000, count=100, compression=False) + run_benchmark(size=2_000_000, count=20, compression=False) + + run_benchmark(size=8, count=1000, compression=True) + run_benchmark(size=60, count=1000, compression=True) + run_benchmark(size=500, count=200, compression=True) + run_benchmark(size=4_000, count=100, compression=True) + run_benchmark(size=30_000, count=20, compression=True) + run_benchmark(size=250_000, count=10, compression=True) diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py new file mode 100644 index 0000000000..af5a4ecae2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py @@ -0,0 +1,102 @@ +"""Benchark parsing WebSocket handshake requests.""" + +# The parser for responses is designed similarly and should perform similarly. + +import sys +import timeit + +from websockets.http11 import Request +from websockets.streams import StreamReader + + +CHROME_HANDSHAKE = ( + b"GET / HTTP/1.1\r\n" + b"Host: localhost:5678\r\n" + b"Connection: Upgrade\r\n" + b"Pragma: no-cache\r\n" + b"Cache-Control: no-cache\r\n" + b"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + b"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36\r\n" + b"Upgrade: websocket\r\n" + b"Origin: null\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"Accept-Encoding: gzip, deflate, br\r\n" + b"Accept-Language: en-GB,en;q=0.9,en-US;q=0.8,fr;q=0.7\r\n" + b"Sec-WebSocket-Key: ebkySAl+8+e6l5pRKTMkyQ==\r\n" + b"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n" + b"\r\n" +) + +FIREFOX_HANDSHAKE = ( + b"GET / HTTP/1.1\r\n" + b"Host: localhost:5678\r\n" + b"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) " + b"Gecko/20100101 Firefox/111.0\r\n" + b"Accept: */*\r\n" + b"Accept-Language: en-US,en;q=0.7,fr-FR;q=0.3\r\n" + b"Accept-Encoding: gzip, deflate, br\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"Origin: null\r\n" + b"Sec-WebSocket-Extensions: permessage-deflate\r\n" + b"Sec-WebSocket-Key: 1PuS+hnb+0AXsL7z2hNAhw==\r\n" + b"Connection: keep-alive, Upgrade\r\n" + b"Sec-Fetch-Dest: websocket\r\n" + b"Sec-Fetch-Mode: websocket\r\n" + b"Sec-Fetch-Site: cross-site\r\n" + b"Pragma: no-cache\r\n" + b"Cache-Control: no-cache\r\n" + b"Upgrade: websocket\r\n" + b"\r\n" +) + +WEBSOCKETS_HANDSHAKE = ( + b"GET / HTTP/1.1\r\n" + b"Host: localhost:8765\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: 9c55e0/siQ6tJPCs/QR8ZA==\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n" + b"User-Agent: Python/3.11 websockets/11.0\r\n" + b"\r\n" +) + + +def parse_handshake(handshake): + reader = StreamReader() + reader.feed_data(handshake) + parser = Request.parse(reader.read_line) + try: + next(parser) + except StopIteration: + pass + else: + assert False, "parser should return request" + reader.feed_eof() + assert reader.at_eof(), "parser should consume all data" + + +def run_benchmark(name, handshake, number=10000): + ph = ( + min( + timeit.repeat( + "parse_handshake(handshake)", + number=number, + globals={"parse_handshake": parse_handshake, "handshake": handshake}, + ) + ) + / number + * 1_000_000 + ) + print(f"{name}\t{len(handshake)}\t{ph:.1f}") + + +if __name__ == "__main__": + print("Sizes are in bytes. Times are in µs per frame.", file=sys.stderr) + print("Run `tabs -16` for clean output. Pipe stdout to TSV for saving.") + print(file=sys.stderr) + + print("client\tsize\ttime") + run_benchmark("Chrome", CHROME_HANDSHAKE) + run_benchmark("Firefox", FIREFOX_HANDSHAKE) + run_benchmark("websockets", WEBSOCKETS_HANDSHAKE) diff --git a/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/streams.py b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/streams.py new file mode 100644 index 0000000000..ca24a59834 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/experiments/optimization/streams.py @@ -0,0 +1,301 @@ +""" +Benchmark two possible implementations of a stream reader. + +The difference lies in the data structure that buffers incoming data: + +* ``ByteArrayStreamReader`` uses a ``bytearray``; +* ``BytesDequeStreamReader`` uses a ``deque[bytes]``. + +``ByteArrayStreamReader`` is faster for streaming small frames, which is the +standard use case of websockets, likely due to its simple implementation and +to ``bytearray`` being fast at appending data and removing data at the front +(https://hg.python.org/cpython/rev/499a96611baa). + +``BytesDequeStreamReader`` is faster for large frames and for bursts, likely +because it copies payloads only once, while ``ByteArrayStreamReader`` copies +them twice. + +""" + + +import collections +import os +import timeit + + +# Implementations + + +class ByteArrayStreamReader: + def __init__(self): + self.buffer = bytearray() + self.eof = False + + def readline(self): + n = 0 # number of bytes to read + p = 0 # number of bytes without a newline + while True: + n = self.buffer.find(b"\n", p) + 1 + if n > 0: + break + p = len(self.buffer) + yield + r = self.buffer[:n] + del self.buffer[:n] + return r + + def readexactly(self, n): + assert n >= 0 + while len(self.buffer) < n: + yield + r = self.buffer[:n] + del self.buffer[:n] + return r + + def feed_data(self, data): + self.buffer += data + + def feed_eof(self): + self.eof = True + + def at_eof(self): + return self.eof and not self.buffer + + +class BytesDequeStreamReader: + def __init__(self): + self.buffer = collections.deque() + self.eof = False + + def readline(self): + b = [] + while True: + # Read next chunk + while True: + try: + c = self.buffer.popleft() + except IndexError: + yield + else: + break + # Handle chunk + n = c.find(b"\n") + 1 + if n == len(c): + # Read exactly enough data + b.append(c) + break + elif n > 0: + # Read too much data + b.append(c[:n]) + self.buffer.appendleft(c[n:]) + break + else: # n == 0 + # Need to read more data + b.append(c) + return b"".join(b) + + def readexactly(self, n): + if n == 0: + return b"" + b = [] + while True: + # Read next chunk + while True: + try: + c = self.buffer.popleft() + except IndexError: + yield + else: + break + # Handle chunk + n -= len(c) + if n == 0: + # Read exactly enough data + b.append(c) + break + elif n < 0: + # Read too much data + b.append(c[:n]) + self.buffer.appendleft(c[n:]) + break + else: # n >= 0 + # Need to read more data + b.append(c) + return b"".join(b) + + def feed_data(self, data): + self.buffer.append(data) + + def feed_eof(self): + self.eof = True + + def at_eof(self): + return self.eof and not self.buffer + + +# Tests + + +class Protocol: + def __init__(self, StreamReader): + self.reader = StreamReader() + self.events = [] + # Start parser coroutine + self.parser = self.run_parser() + next(self.parser) + + def run_parser(self): + while True: + frame = yield from self.reader.readexactly(2) + self.events.append(frame) + frame = yield from self.reader.readline() + self.events.append(frame) + + def data_received(self, data): + self.reader.feed_data(data) + next(self.parser) # run parser until more data is needed + events, self.events = self.events, [] + return events + + +def run_test(StreamReader): + proto = Protocol(StreamReader) + + actual = proto.data_received(b"a") + expected = [] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"b") + expected = [b"ab"] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"c") + expected = [] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"\n") + expected = [b"c\n"] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"efghi\njklmn") + expected = [b"ef", b"ghi\n", b"jk"] + assert actual == expected, f"{actual} != {expected}" + + +# Benchmarks + + +def get_frame_packets(size, packet_size=None): + if size < 126: + frame = bytes([138, size]) + elif size < 65536: + frame = bytes([138, 126]) + bytes(divmod(size, 256)) + else: + size1, size2 = divmod(size, 65536) + frame = ( + bytes([138, 127]) + bytes(divmod(size1, 256)) + bytes(divmod(size2, 256)) + ) + frame += os.urandom(size) + if packet_size is None: + return [frame] + else: + packets = [] + while frame: + packets.append(frame[:packet_size]) + frame = frame[packet_size:] + return packets + + +def benchmark_stream(StreamReader, packets, size, count): + reader = StreamReader() + for _ in range(count): + for packet in packets: + reader.feed_data(packet) + yield from reader.readexactly(2) + if size >= 65536: + yield from reader.readexactly(4) + elif size >= 126: + yield from reader.readexactly(2) + yield from reader.readexactly(size) + reader.feed_eof() + assert reader.at_eof() + + +def benchmark_burst(StreamReader, packets, size, count): + reader = StreamReader() + for _ in range(count): + for packet in packets: + reader.feed_data(packet) + reader.feed_eof() + for _ in range(count): + yield from reader.readexactly(2) + if size >= 65536: + yield from reader.readexactly(4) + elif size >= 126: + yield from reader.readexactly(2) + yield from reader.readexactly(size) + assert reader.at_eof() + + +def run_benchmark(size, count, packet_size=None, number=1000): + stmt = f"list(benchmark(StreamReader, packets, {size}, {count}))" + setup = f"packets = get_frame_packets({size}, {packet_size})" + context = globals() + + context["StreamReader"] = context["ByteArrayStreamReader"] + context["benchmark"] = context["benchmark_stream"] + bas = min(timeit.repeat(stmt, setup, number=number, globals=context)) + context["benchmark"] = context["benchmark_burst"] + bab = min(timeit.repeat(stmt, setup, number=number, globals=context)) + + context["StreamReader"] = context["BytesDequeStreamReader"] + context["benchmark"] = context["benchmark_stream"] + bds = min(timeit.repeat(stmt, setup, number=number, globals=context)) + context["benchmark"] = context["benchmark_burst"] + bdb = min(timeit.repeat(stmt, setup, number=number, globals=context)) + + print( + f"Frame size = {size} bytes, " + f"frame count = {count}, " + f"packet size = {packet_size}" + ) + print(f"* ByteArrayStreamReader (stream): {bas / number * 1_000_000:.1f}µs") + print( + f"* BytesDequeStreamReader (stream): " + f"{bds / number * 1_000_000:.1f}µs ({(bds / bas - 1) * 100:+.1f}%)" + ) + print(f"* ByteArrayStreamReader (burst): {bab / number * 1_000_000:.1f}µs") + print( + f"* BytesDequeStreamReader (burst): " + f"{bdb / number * 1_000_000:.1f}µs ({(bdb / bab - 1) * 100:+.1f}%)" + ) + print() + + +if __name__ == "__main__": + run_test(ByteArrayStreamReader) + run_test(BytesDequeStreamReader) + + run_benchmark(size=8, count=1000) + run_benchmark(size=60, count=1000) + run_benchmark(size=500, count=500) + run_benchmark(size=4_000, count=200) + run_benchmark(size=30_000, count=100) + run_benchmark(size=250_000, count=50) + run_benchmark(size=2_000_000, count=20) + + run_benchmark(size=4_000, count=200, packet_size=1024) + run_benchmark(size=30_000, count=100, packet_size=1024) + run_benchmark(size=250_000, count=50, packet_size=1024) + run_benchmark(size=2_000_000, count=20, packet_size=1024) + + run_benchmark(size=30_000, count=100, packet_size=4096) + run_benchmark(size=250_000, count=50, packet_size=4096) + run_benchmark(size=2_000_000, count=20, packet_size=4096) + + run_benchmark(size=30_000, count=100, packet_size=16384) + run_benchmark(size=250_000, count=50, packet_size=16384) + run_benchmark(size=2_000_000, count=20, packet_size=16384) + + run_benchmark(size=250_000, count=50, packet_size=65536) + run_benchmark(size=2_000_000, count=20, packet_size=65536) diff --git a/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py new file mode 100644 index 0000000000..59e0cea0f4 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py @@ -0,0 +1,42 @@ +import sys + +import atheris + + +with atheris.instrument_imports(): + from websockets.exceptions import SecurityError + from websockets.http11 import Request + from websockets.streams import StreamReader + + +def test_one_input(data): + reader = StreamReader() + reader.feed_data(data) + reader.feed_eof() + + parser = Request.parse( + reader.read_line, + ) + + try: + next(parser) + except StopIteration as exc: + assert isinstance(exc.value, Request) + return # input accepted + except ( + EOFError, # connection is closed without a full HTTP request + SecurityError, # request exceeds a security limit + ValueError, # request isn't well formatted + ): + return # input rejected with a documented exception + + raise RuntimeError("parsing didn't complete") + + +def main(): + atheris.Setup(sys.argv, test_one_input) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py new file mode 100644 index 0000000000..6906720a49 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py @@ -0,0 +1,44 @@ +import sys + +import atheris + + +with atheris.instrument_imports(): + from websockets.exceptions import SecurityError + from websockets.http11 import Response + from websockets.streams import StreamReader + + +def test_one_input(data): + reader = StreamReader() + reader.feed_data(data) + reader.feed_eof() + + parser = Response.parse( + reader.read_line, + reader.read_exact, + reader.read_to_eof, + ) + try: + next(parser) + except StopIteration as exc: + assert isinstance(exc.value, Response) + return # input accepted + except ( + EOFError, # connection is closed without a full HTTP response + SecurityError, # response exceeds a security limit + LookupError, # response isn't well formatted + ValueError, # response isn't well formatted + ): + return # input rejected with a documented exception + + raise RuntimeError("parsing didn't complete") + + +def main(): + atheris.Setup(sys.argv, test_one_input) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py new file mode 100644 index 0000000000..1509a3549d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py @@ -0,0 +1,51 @@ +import sys + +import atheris + + +with atheris.instrument_imports(): + from websockets.exceptions import PayloadTooBig, ProtocolError + from websockets.frames import Frame + from websockets.streams import StreamReader + + +def test_one_input(data): + fdp = atheris.FuzzedDataProvider(data) + mask = fdp.ConsumeBool() + max_size_enabled = fdp.ConsumeBool() + max_size = fdp.ConsumeInt(4) + payload = fdp.ConsumeBytes(atheris.ALL_REMAINING) + + reader = StreamReader() + reader.feed_data(payload) + reader.feed_eof() + + parser = Frame.parse( + reader.read_exact, + mask=mask, + max_size=max_size if max_size_enabled else None, + ) + + try: + next(parser) + except StopIteration as exc: + assert isinstance(exc.value, Frame) + return # input accepted + except ( + EOFError, # connection is closed without a full WebSocket frame + UnicodeDecodeError, # frame contains invalid UTF-8 + PayloadTooBig, # frame's payload size exceeds ``max_size`` + ProtocolError, # frame contains incorrect values + ): + return # input rejected with a documented exception + + raise RuntimeError("parsing didn't complete") + + +def main(): + atheris.Setup(sys.argv, test_one_input) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/favicon.ico b/testing/web-platform/tests/tools/third_party/websockets/logo/favicon.ico new file mode 100644 index 0000000000..602c14e4eb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/logo/favicon.ico @@ -0,0 +1,2 @@ + h& ��( >��-=��v=��`@��B��U=���;���;���;���B��#J��VE���A���=���<���;���<���R��WN���I���E���E��%>���;���;���;���=��*\��VU���Q���M���N��$K��cE���A���=���D���`����q9 ^���]���Z���U��$R��dM���I���F���g��j�j1��i0��k1Cf��b���_��#[��eU���Q���N���R���t8w�k2��i0��k19d��J]���Y���U���R���|>s�v:��r7��k3�����k2u�j50f��da���]���^����Gk�~@��z>��w<Ҧs@�l2��q��zE�h��;p��ēN��I���C��Bѳ�@�x<��o5��M��o7�ȔOJ��M���J���F(�A��z=��v:��p5ɪw3ϘS%ŒP���L���JﳂC��~A��{>ɪwDʖS�ŒP���M���J���CŻ�D͗UBƔPyÏMY̙f����������������( @ <��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��U��U��U��U��U��U��U��U��B��<���<���<���;���<���<��M<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��A��3<���;���;���;���;���;���;���;���U��U��U��U��U��U��U��U��U��U��U��U��U��U��U��>��>��>��>��>��>��>��F��3A���?���>���;���;���;���;���;���;���<���<��<��<��<��<��<��<��<��<��<��<��<��<��<��=��=��=��=��=��=��J��4F���D���B���?���>���<���;���;���;���;���;���>��->��>��>��>��>��>��>��>��>��>��>��>��>��<��<��<��<��<��M��5J���G���E���C���B���@���=���<���;���;���;���;���=���=��=��=��=��=��=��=��=��=��=��=��=��=��U��U��U��U��R��5N���K���I���G���E���C���B���B��A��'<���;���;���;���;���;���<��Y<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��6S���Q���N���M���K���I���H���H��H��B��U@���>���<���;���;���;���;���<���U��U��U��U��U��U��U��U��U��U���j2�j2Z��6V���T���R���P���O���M���L���L��L��H��JE���A���@���=���<���;���;���;���;���<���<��<��<��<��<��<��<��<��<���k2^��1Z���X���V���T���S���P���P���P��P��K��KH���F���C���B���@���=���<���@���a���}�k��zOŜj2$�j2�j2�j2�j2�j2�j2�j2�j2�i1_���^���\���Z���X���V���V���V��V��M��LL���I���G���F���C���B���@����m��i1��i0��i0��i0��k2u�k2�k2�k2�k2�k2�k2�k2�k2�k1b���_���]���[���Z���Z���Z��Z��T��LR���N���M���J���I���H���G��sG���r6/�j0��i0��i0��i0��i1��i1�i1�i1�i1�i1�i1�i1�i1�k1d���a���`���]���]���]��]��V��MU���R���Q���N���M���K���J��rJ��J���r7p�l2��j1��i0��i0��k1��k1�k1�k1�k1�k1�k1�k1�k1��U���f��sa���a��_a��a��\��NW���V���T���R���P���O���M��qM��M���t8e�q6��o4��l2��j1��i0��k1X�k1�k1�k1�k1�k1�k1�k1�k1��U��U��U��U��U��Ub��F^���\���Z���X���W���T���T��pT��T���{<f�w;��s8��q6��o5��k2��j1�q9 �q9�q9�j2W��U��U��U��U��U�i0�i0�i0�i0�i0�i0a���_���^���[���Z���X���W��pW��W���~@g�z>��x<��v:��s8��r7��p5��l2B�l2�l2�j1}�i0��j0���U��U��U��U�i0�i0�i0�i0�i0���d���a���`���^���[���\��o\��\����Eg��B��~@��z=��y<��v:��t9��r7S�r7�r7�k3~�i0��|J��i0��i0��i0�i0�i0�j1�j1�j1�j1�j1�j1g���c���a���_���_��n_��_����JS��E���C���B��|?��z=��x<��v;R�v;�v;�n4�l2���\�����è���i0�i0�i0�i0�k1�k1�k1�k1�k1�k1���i��ff���d��Bd��d��ĝN +��I���H���E���C��A��|?��~?Q�~?�~?�t8�q6��n4��t=�Ѽ���N��j1љj1�j1�j1�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5M`��J���H���G���D���C���BQ��B��B�z>��t9��s8��q6��m3��l3��k1��k19�k1�k1�k1�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5ÐO���M���J���H���G���G^��G��G�}?��z=��y<��u9��s8��o5��o4��o5>�o5�o5�o5�o5�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;ƑO�ÐO���L���J���I���G2��G��C���B��|?��z=��v:��u9��t7��s5>�s5�s5�s5�s5�s5�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?ʕTtőO�ÐN���M���I���I귆Gʵ�D���C��~@��|?��z=��x<�u;=�u;�u;�u;�u;�u;�u;��D��D��D��D��D��D��D��D��D��D��D��D̙W#ɖS�ŒP�ÐN���K���J���H���F���D���C��}@��|?�}?=�}?�}?�}?�}?�}?�}?�}?��I��I��I��I��I��I��I��I��I��I��I��I��I̘T�ǓQ�őO�ÏN���L���J���I���F���D���C�D<��D��D��D��D��D��D��D��DUUUUUUUUUUUUU��U̗T�ǔQ�ŒP�ÏN���K���J���H���F鶄I8��I��I��I��I��I��I��I��I��I˘WOȓQ�œP�ÑN���Lھ�L�UUUUUUUUUUU�����������������?��?����������0��`������@��������������0����0�� ��?����������������������� diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/github-social-preview.html b/testing/web-platform/tests/tools/third_party/websockets/logo/github-social-preview.html new file mode 100644 index 0000000000..7f2b45badb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/logo/github-social-preview.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>GitHub social preview</title> + <style> + body { + background-color: black; + color: white; + font-family: -apple-system; + font-size: 36px; + font-weight: 100; + text-align: center; + } + p.screenshot { + background-color: white; + box-sizing: border-box; + width: 1280px; + height: 640px; + margin: 40px auto; + padding: 40px; + } + p.screenshot.x2 { + width: 640px; + height: 320px; + padding: 20px; + } + p.screenshot img { + height: 100%; + } + </style> + </head> + <body> + <p>Take a screenshot of this DOM node to make a PNG.</p> + <p>For 2x DPI screens.</p> + <p class="screenshot x2"><img src="vertical.svg" alt="preview @ 2x"></p> + <p>For regular screens.</p> + <p class="screenshot"><img src="vertical.svg" alt="preview"></p> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg new file mode 100644 index 0000000000..ee872dc478 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg @@ -0,0 +1,31 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="256" viewBox="0 0 1024 256"> + <linearGradient id="w" x1="0" y1="0" x2="0.1667" y2="0.6667"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <linearGradient id="s" x1="0" y1="0" x2="0.1667" y2="0.6667"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> +<g> + <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" /> + <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" /> + <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" /> + </g> + <g> + <g fill="#ffd43b"> + <path d="m 271.62046,177.33313 c 0,4.1637 1.46619,7.71227 4.39858,10.64361 2.9324,2.93556 6.48202,4.40069 10.64783,4.40069 4.16475,0 7.71438,-1.46513 10.64572,-4.40069 2.93344,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.7647,-3.95198 2.29514,-5.48032 1.53045,-1.53256 3.35793,-2.29831 5.48349,-2.29831 h 0.12745 c 2.16664,0 3.972,0.76575 5.41923,2.29831 1.53045,1.52834 2.2962,3.35793 2.2962,5.48032 v 35.00332 c 0,4.1637 1.4662,7.71227 4.40069,10.64361 2.93134,2.93556 6.47886,4.40069 10.64572,4.40069 4.20794,0 7.77758,-1.46513 10.70997,-4.40069 2.93345,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.76365,-3.95198 2.29515,-5.48032 1.44302,-1.53256 3.25049,-2.29831 5.41924,-2.29831 h 0.1264 c 2.12661,0 3.95409,0.76575 5.48349,2.29831 1.48831,1.52834 2.23194,3.35793 2.23194,5.48032 v 35.00332 c 0,8.45696 -2.9977,15.68261 -8.98887,21.67484 -5.99329,5.99224 -13.21999,8.98993 -21.67695,8.98993 -10.11696,0 -17.7239,-3.35583 -22.82609,-10.07272 -5.14222,6.71689 -12.77234,10.07272 -22.88719,10.07272 -8.45801,0 -15.68471,-2.99769 -21.67695,-8.98993 C 258.9998,193.01574 256,185.79113 256,177.33313 v -35.00332 c 0,-2.12345 0.76575,-3.95198 2.29619,-5.48032 1.5294,-1.53256 3.33581,-2.29831 5.41924,-2.29831 h 0.1917 c 2.08238,0 3.88774,0.76575 5.42029,2.29831 1.52834,1.52834 2.29409,3.35793 2.29409,5.48032 v 35.00332 z" /> + <path d="m 443.95216,155.97534 c 0.51085,1.06173 0.7668,2.14346 0.7668,3.25048 0,0.8932 -0.16957,1.78536 -0.50979,2.67854 -0.72363,1.99707 -2.08343,3.4422 -4.0805,4.33434 -5.95114,2.67854 -13.77085,6.20711 -23.46228,10.58463 -12.02871,5.43924 -19.08477,8.64866 -21.16715,9.62823 3.22943,4.07944 8.26737,6.11863 15.11067,6.11863 4.5471,0 8.67077,-1.33769 12.36786,-4.01625 3.61283,-2.63534 6.14286,-6.03541 7.58798,-10.20227 1.23342,-3.48538 3.69815,-5.22754 7.39524,-5.22754 2.6343,0 4.7388,1.10702 6.31138,3.31369 0.97746,1.36193 1.46619,2.78598 1.46619,4.27325 0,0.8932 -0.16958,1.80641 -0.50874,2.74069 -2.50791,7.26988 -6.90861,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51702,6.94757 -21.4231,6.94757 -10.11591,0 -18.76563,-3.58965 -25.94809,-10.7742 -7.18351,-7.18353 -10.77527,-15.83219 -10.77527,-25.9502 0,-10.11591 3.59176,-18.76351 10.77527,-25.95019 7.18142,-7.1814 15.83218,-10.77422 25.94809,-10.77422 7.30885,0 13.98257,1.99916 20.01904,5.99223 5.99118,3.91512 10.43296,9.05524 13.32321,15.43298 z m -33.34331,-5.67836 c -5.86583,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12073,9.11949 -6.12073,14.98322 v 0.44661 l 35.63951,-16.00282 c -3.1441,-3.73817 -7.99035,-5.61305 -14.53556,-5.61305 z" /> + <path d="m 465.12141,108.41246 c 2.08238,0 3.88775,0.74469 5.41924,2.23194 1.53045,1.52834 2.29619,3.35793 2.29619,5.48244 v 24.79998 c 4.80202,-4.24796 11.83701,-6.37564 21.10185,-6.37564 10.11591,0 18.76561,3.59177 25.94914,10.77422 7.18245,7.18563 10.77527,15.83429 10.77527,25.9502 0,10.11695 -3.59282,18.76561 -10.77527,25.95018 C 512.70536,204.41035 504.05566,208 493.93869,208 c -10.11696,0 -18.74349,-3.56964 -25.88382,-10.71207 -7.18457,-7.09504 -10.7974,-15.70262 -10.83954,-25.82063 v -55.33941 c 0,-2.12556 0.76576,-3.95409 2.29621,-5.48243 1.52939,-1.48727 3.3358,-2.23196 5.41924,-2.23196 h 0.19063 z m 28.81622,41.88452 c -5.86477,0 -10.85953,2.06343 -14.9832,6.18604 -4.0784,4.12473 -6.11969,9.11949 -6.11969,14.98322 0,5.8237 2.04129,10.79633 6.11969,14.91896 4.12367,4.12263 9.11737,6.18393 14.9832,6.18393 5.82371,0 10.79635,-2.0613 14.92002,-6.18393 4.12051,-4.12263 6.18288,-9.09526 6.18288,-14.91896 0,-5.86267 -2.06237,-10.85849 -6.18288,-14.98322 -4.12367,-4.12261 -9.09525,-6.18604 -14.92002,-6.18604 z" /> + </g> + <g fill="#306998"> + <path d="m 561.26467,150.17375 c -1.87066,0 -3.44325,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23237,4.46389 3.69919,5.73733 0.84898,0.46872 4.39859,1.53045 10.64678,3.18834 5.05795,1.44619 8.81825,3.33686 11.28296,5.67413 3.52963,3.35898 5.29179,8.14097 5.29179,14.34703 0,6.11862 -2.16769,11.36829 -6.50203,15.74581 -4.37857,4.33644 -9.62823,6.50308 -15.74791,6.50308 h -16.64005 c -2.08448,0 -3.88879,-0.76365 -5.42029,-2.29621 -1.53045,-1.44407 -2.2962,-3.25048 -2.2962,-5.41712 v -0.12953 c 0,-2.12345 0.76575,-3.95198 2.2962,-5.48243 1.53044,-1.53045 3.33581,-2.29619 5.42029,-2.29619 h 17.2773 c 1.8696,0 3.44324,-0.6362 4.71773,-1.9107 1.27556,-1.27554 1.91281,-2.84707 1.91281,-4.71774 0,-2.33937 -1.21131,-4.10366 -3.63285,-5.29073 -0.63723,-0.30018 -4.20898,-1.36192 -10.71208,-3.18834 -5.05899,-1.48725 -8.82139,-3.44535 -11.28611,-5.8669 -3.52856,-3.44217 -5.29075,-8.30949 -5.29075,-14.5998 0,-6.12073 2.16876,-11.34721 6.50414,-15.68261 4.37648,-4.37752 9.62718,-6.56839 15.74687,-6.56839 h 11.73166 c 2.12452,0 3.95304,0.76575 5.48349,2.29831 1.52939,1.52834 2.29515,3.35793 2.29515,5.48032 v 0.12745 c 0,2.16876 -0.76576,3.97622 -2.29515,5.4203 -1.53045,1.52834 -3.35897,2.29619 -5.48349,2.29619 z" /> + <path d="m 630.5677,134.55118 c 10.1159,0 18.76456,3.59177 25.94912,10.77422 7.18246,7.18563 10.77422,15.83429 10.77422,25.9502 0,10.11695 -3.59176,18.76561 -10.77422,25.95018 C 649.33331,204.40929 640.6836,208 630.5677,208 c -10.11592,0 -18.76563,-3.58965 -25.9481,-10.77422 -7.18351,-7.18351 -10.77526,-15.83217 -10.77526,-25.95018 0,-10.11591 3.59175,-18.76352 10.77526,-25.9502 7.18247,-7.18245 15.83218,-10.77422 25.9481,-10.77422 z m 0,15.7458 c -5.86585,0 -10.86059,2.06343 -14.98322,6.18604 -4.08155,4.12473 -6.12178,9.11949 -6.12178,14.98322 0,5.8237 2.04023,10.79633 6.12178,14.91896 4.12263,4.12263 9.11632,6.18393 14.98322,6.18393 5.82264,0 10.79527,-2.0613 14.91896,-6.18393 4.12261,-4.12263 6.18393,-9.09526 6.18393,-14.91896 0,-5.86267 -2.06132,-10.85849 -6.18393,-14.98322 -4.12369,-4.12261 -9.09527,-6.18604 -14.91896,-6.18604 z" /> + <path d="m 724.0345,136.27333 c 3.61388,1.14811 5.4203,3.61282 5.4203,7.39523 v 0.32125 c 0,2.59008 -1.04278,4.65138 -3.12516,6.18394 -1.44512,1.01854 -2.93343,1.52834 -4.46178,1.52834 -0.80894,0 -1.63789,-0.12745 -2.48684,-0.38235 -2.08344,-0.67938 -4.23007,-1.02276 -6.43883,-1.02276 -5.86585,0 -10.86165,2.06343 -14.98322,6.18604 -4.08154,4.12473 -6.12074,9.11949 -6.12074,14.98322 0,5.8237 2.0392,10.79633 6.12074,14.91896 4.12157,4.12263 9.11633,6.18393 14.98322,6.18393 2.20982,0 4.35645,-0.33915 6.43883,-1.02065 0.80683,-0.25489 1.61471,-0.38234 2.42259,-0.38234 1.57046,0 3.08197,0.5119 4.52709,1.53254 2.08238,1.52835 3.12514,3.61283 3.12514,6.24819 0,3.74027 -1.80746,6.205 -5.42028,7.39524 -3.56964,1.10491 -7.26673,1.65579 -11.09232,1.65579 -10.11591,0 -18.76562,-3.58965 -25.95019,-10.77423 -7.1814,-7.18351 -10.77422,-15.83217 -10.77422,-25.95018 0,-10.11592 3.59176,-18.76352 10.77422,-25.9502 7.18351,-7.1814 15.83322,-10.77422 25.95019,-10.77422 3.82348,0.002 7.52162,0.57827 11.09126,1.72426 z" /> + <path d="m 748.19829,108.41246 c 2.08132,0 3.88773,0.74469 5.42029,2.23194 1.5294,1.52834 2.29514,3.35793 2.29514,5.48244 v 44.18284 h 2.42259 c 5.44031,0 10.17805,-1.80642 14.21852,-5.4203 3.95198,-3.61283 6.20394,-8.07461 6.75693,-13.38852 0.25491,-1.99705 1.10597,-3.63494 2.5511,-4.90837 1.44408,-1.35982 3.16517,-2.04131 5.16328,-2.04131 h 0.19066 c 2.25405,0 4.14578,0.85212 5.67517,2.55109 1.36087,1.48727 2.04026,3.20942 2.04026,5.16329 0,0.25491 -0.0222,0.53298 -0.0632,0.82895 -1.02064,10.66889 -5.10115,18.65923 -12.24147,23.97103 3.73922,2.29831 7.18246,6.18604 10.32973,11.66849 3.27155,5.65306 4.90944,11.75483 4.90944,18.29688 v 3.25471 c 0,2.16664 -0.7668,3.972 -2.29515,5.41713 -1.53255,1.53256 -3.33791,2.29619 -5.4203,2.29619 h -0.1917 c -2.08342,0 -3.88879,-0.76363 -5.41922,-2.29619 -1.53045,-1.44408 -2.29514,-3.25049 -2.29514,-5.41713 v -3.25471 c -0.0442,-5.77629 -2.10555,-10.73102 -6.185,-14.85575 -4.12367,-4.07944 -9.09736,-6.11863 -14.91896,-6.11863 h -5.22754 v 24.22804 c 0,2.16664 -0.76574,3.97199 -2.29514,5.41712 -1.5315,1.53256 -3.33897,2.29621 -5.42028,2.29621 h -0.19381 c -2.08237,0 -3.88668,-0.76365 -5.41819,-2.29621 -1.52939,-1.44407 -2.29515,-3.25048 -2.29515,-5.41712 v -84.15879 c 0,-2.12556 0.76576,-3.95408 2.29515,-5.48243 1.53045,-1.48727 3.33582,-2.23195 5.41819,-2.23195 h 0.19381 z" /> + <path d="m 876.85801,155.97534 c 0.5098,1.06173 0.76469,2.14346 0.76469,3.25048 0,0.8932 -0.17063,1.78536 -0.50874,2.67854 -0.72362,1.99707 -2.08342,3.4422 -4.08049,4.33434 -5.95115,2.67854 -13.77191,6.20711 -23.46229,10.58463 -12.02869,5.43924 -19.08476,8.64866 -21.16715,9.62823 3.22838,4.07944 8.26632,6.11863 15.11066,6.11863 4.54606,0 8.66973,-1.33769 12.36893,-4.01625 3.61176,-2.63534 6.14075,-6.03541 7.58587,-10.20227 1.23238,-3.48538 3.6992,-5.22754 7.39524,-5.22754 2.63536,0 4.73985,1.10702 6.31348,3.31369 0.97536,1.36193 1.46515,2.78598 1.46515,4.27325 0,0.8932 -0.16958,1.80641 -0.5098,2.74069 -2.50791,7.26988 -6.9065,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51598,6.94757 -21.42206,6.94757 -10.1159,0 -18.76561,-3.58965 -25.94808,-10.7742 -7.18351,-7.18353 -10.77526,-15.83219 -10.77526,-25.9502 0,-10.11591 3.59175,-18.76351 10.77526,-25.95019 7.18141,-7.1814 15.83218,-10.77422 25.94808,-10.77422 7.30887,0 13.98364,1.99916 20.01906,5.99223 5.99223,3.91512 10.43294,9.05524 13.32426,15.43298 z m -33.34436,-5.67836 c -5.86479,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12074,9.11949 -6.12074,14.98322 v 0.44661 l 35.63952,-16.00282 c -3.14516,-3.73817 -7.99034,-5.61305 -14.53556,-5.61305 z" /> + <path d="m 898.02411,108.41246 c 2.08238,0 3.88879,0.74469 5.42028,2.23194 1.52939,1.52834 2.29515,3.35793 2.29515,5.48244 v 18.42434 h 9.56398 c 2.08237,0 3.88772,0.76575 5.42028,2.29831 1.5294,1.52834 2.29304,3.35793 2.29304,5.48032 v 0.12745 c 0,2.16876 -0.76364,3.97621 -2.29304,5.4203 -1.5315,1.52834 -3.33791,2.29619 -5.42028,2.29619 h -9.56398 v 37.80405 c 0,1.23446 0.42343,2.27724 1.27554,3.12514 0.85002,0.85212 1.9128,1.27555 3.1873,1.27555 h 5.10114 c 2.08237,0 3.88772,0.76574 5.42028,2.29619 1.5294,1.53045 2.29304,3.35898 2.29304,5.48243 v 0.12954 c 0,2.16664 -0.76364,3.97199 -2.29304,5.41711 C 919.1923,207.23635 917.38589,208 915.30352,208 h -5.10114 c -5.52563,0 -10.26442,-1.95387 -14.21746,-5.86478 -3.91196,-3.95198 -5.86479,-8.67078 -5.86479,-14.15532 v -71.85095 c 0,-2.12558 0.7647,-3.9541 2.29515,-5.48245 1.53045,-1.48725 3.33686,-2.23193 5.41924,-2.23193 h 0.18959 z" /> + <path d="m 951.70877,150.17375 c -1.87066,0 -3.44324,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23238,4.46389 3.69711,5.73733 0.8521,0.46872 4.40067,1.53045 10.64886,3.18834 5.05691,1.44619 8.81825,3.33686 11.28402,5.67413 3.52751,3.35898 5.2918,8.14097 5.2918,14.34703 0,6.11862 -2.16876,11.36829 -6.5031,15.74581 -4.37752,4.33644 -9.62822,6.50308 -15.74789,6.50308 h -16.64007 c -2.08342,0 -3.88879,-0.76365 -5.42028,-2.29621 -1.53045,-1.44407 -2.2941,-3.25048 -2.2941,-5.41712 v -0.12953 c 0,-2.12345 0.76365,-3.95198 2.2941,-5.48243 1.53045,-1.53045 3.33686,-2.29619 5.42028,-2.29619 h 17.2773 c 1.86962,0 3.4443,-0.6362 4.71775,-1.9107 1.27554,-1.27554 1.91279,-2.84707 1.91279,-4.71774 0,-2.33937 -1.2113,-4.10366 -3.63283,-5.29073 -0.63936,-0.30018 -4.209,-1.36192 -10.71208,-3.18834 -5.05901,-1.48725 -8.8214,-3.44535 -11.28613,-5.8669 -3.52856,-3.44217 -5.29073,-8.30949 -5.29073,-14.5998 0,-6.12073 2.16875,-11.34721 6.50413,-15.68261 4.37647,-4.37752 9.62718,-6.56839 15.74791,-6.56839 h 11.73063 c 2.1266,0 3.95304,0.76575 5.48243,2.29831 1.53045,1.52834 2.29514,3.35793 2.29514,5.48032 v 0.12745 c 0,2.16876 -0.76469,3.97622 -2.29514,5.4203 -1.52939,1.52834 -3.35687,2.29619 -5.48243,2.29619 z" /> + </g> + </g> +</svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/icon.html b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.html new file mode 100644 index 0000000000..6a71ec23bc --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Icon</title> + <style> + body { + font-family: -apple-system; + font-size: 24px; + font-weight: 100; + text-align: center; + } + </style> + </head> + <body> + <p>Take a screenshot of these DOM nodes to2x make a PNG.</p> + <p><img src="icon.svg" height="8" width="8" alt="8x8 / 16x16 @ 2x"></p> + <p><img src="icon.svg" height="16" width="16" alt="16x16 / 32x32 @ 2x"></p> + <p><img src="icon.svg" height="32" width="32" alt="32x32 / 32x32 @ 2x"></p> + <p><img src="icon.svg" height="32" width="32" alt="32x32 / 64x64 @ 2x"></p> + <p><img src="icon.svg" height="64" width="64" alt="64x64 / 128x128 @ 2x"></p> + <p><img src="icon.svg" height="128" width="128" alt="128x128 / 256x256 @ 2x"></p> + <p><img src="icon.svg" height="256" width="256" alt="256x256 / 512x512 @ 2x"></p> + <p><img src="icon.svg" height="512" width="512" alt="512x512 / 1024x1024 @ 2x"></p> + </body> +</html> diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg new file mode 100644 index 0000000000..cb760940aa --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"> + <linearGradient id="w" x1="0" y1="0" x2="0.6667" y2="0.6667"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <linearGradient id="s" x1="0" y1="0" x2="0.6667" y2="0.6667"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> + <g> + <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" /> + <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" /> + <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" /> + </g> +</svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg new file mode 100644 index 0000000000..a073139e33 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="360" height="120" viewBox="0 0 21 7"> + <linearGradient id="w" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> + <linearGradient id="s" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <polyline fill="none" stroke="url(#w)" stroke-linecap="round" stroke-linejoin="round" + points="1,1 1,5 5,5 5,1 5,5 9,5 9,1"/> + <polyline fill="none" stroke="url(#s)" stroke-linecap="round" stroke-linejoin="round" + points="19,1 11,1 11,3 19,3 19,5 11,5"/> +</svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg new file mode 100644 index 0000000000..b07fb22387 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg @@ -0,0 +1,31 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320"> + <linearGradient id="w" x1="0.2333" y1="0" x2="0.5889" y2="0.5333"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <linearGradient id="s" x1="0.2333" y1="0" x2="0.5889" y2="0.5333"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> + <g> + <path fill="url(#w)" d="m 263.40708,146.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.04076,-35.919454 c -3.43568,-3.42217 -7.33248,-5.347474 -11.58962,-5.723468 -2.22981,-0.198219 -4.47388,0.03111 -6.64036,0.675545 -3.24213,0.944875 -6.13552,2.664848 -8.59366,5.116366 -3.83437,3.819499 -5.86349,8.414979 -5.87598,13.287801 -0.0607,4.95281 1.95153,9.60074 5.8082,13.44424 l 55.62289,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 160.50255,128.2618 c -5.21417,-5.19459 -11.7029,-6.98745 -18.22998,-5.04881 -3.2457,0.9431 -6.13553,2.66307 -8.59545,5.11459 -3.83437,3.82127 -5.86527,8.41676 -5.87597,13.28957 -0.0562,4.95281 1.95152,9.60252 5.80641,13.4478 l 58.10689,57.90577 c 8.31984,8.29143 19.34042,11.9376 32.74331,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" /> + <path fill="url(#s)" d="m 308.76038,138.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 259.37292,69.225372 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384848 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.515442 5.26947,-18.272611 -1.51003,-25.028952 L 299.00456,29.727312 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.626122 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" /> + <path fill="#ffffff" d="m 327.48093,85.181572 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" /> + </g> + <g> + <g fill="#ffd43b"> + <path d="m 25.719398,284.91839 c 0,2.59075 0.912299,4.79875 2.736898,6.62269 1.824599,1.82657 4.033255,2.73821 6.625313,2.73821 2.591402,0 4.800058,-0.91164 6.624002,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475811,-2.45901 1.42809,-3.40998 0.952278,-0.95359 2.089375,-1.43006 3.411947,-1.43006 h 0.0793 c 1.348132,0 2.471467,0.47647 3.371969,1.43006 0.952278,0.95097 1.428745,2.08938 1.428745,3.40998 v 21.77984 c 0,2.59075 0.912299,4.79875 2.738209,6.62269 1.823944,1.82657 4.031289,2.73821 6.624002,2.73821 2.618274,0 4.839382,-0.91164 6.663981,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475156,-2.45901 1.42809,-3.40998 0.897881,-0.95359 2.022526,-1.43006 3.371969,-1.43006 h 0.07865 c 1.323228,0 2.460325,0.47647 3.411948,1.43006 0.926062,0.95097 1.388766,2.08938 1.388766,3.40998 v 21.77984 c 0,5.26211 -1.865233,9.75807 -5.593077,13.48657 -3.729156,3.7285 -8.22577,5.59373 -13.487876,5.59373 -6.294998,0 -11.028207,-2.08807 -14.202904,-6.26747 -3.199602,4.1794 -7.94723,6.26747 -14.240916,6.26747 -5.262763,0 -9.759377,-1.86523 -13.487876,-5.59373 C 17.866544,294.67646 16,290.18115 16,284.91839 v -21.77984 c 0,-1.32126 0.476467,-2.45901 1.428745,-3.40998 0.951623,-0.95359 2.075612,-1.43006 3.371969,-1.43006 h 0.11928 c 1.295702,0 2.419036,0.47647 3.372625,1.43006 0.950967,0.95097 1.427434,2.08938 1.427434,3.40998 v 21.77984 z" /> + <path d="m 132.94801,271.6291 c 0.31786,0.66063 0.47712,1.33371 0.47712,2.02252 0,0.55577 -0.10551,1.11089 -0.3172,1.66665 -0.45026,1.24262 -1.29636,2.14181 -2.53898,2.69692 -3.70293,1.66665 -8.56853,3.8622 -14.59875,6.58599 -7.48453,3.38442 -11.87497,5.38139 -13.17067,5.9909 2.00942,2.53832 5.14414,3.80715 9.40219,3.80715 2.82931,0 5.39515,-0.83234 7.69556,-2.499 2.24798,-1.63977 3.82222,-3.75537 4.72141,-6.34808 0.76746,-2.16868 2.30107,-3.25269 4.60148,-3.25269 1.63912,0 2.94859,0.68881 3.92708,2.06185 0.6082,0.84742 0.9123,1.7335 0.9123,2.65891 0,0.55577 -0.10552,1.12399 -0.31655,1.70532 -1.56048,4.52348 -4.29869,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.41059,4.32293 -13.32993,4.32293 -6.29434,0 -11.67639,-2.23356 -16.145474,-6.70395 -4.469743,-4.46975 -6.704615,-9.85114 -6.704615,-16.14679 0,-6.29434 2.234872,-11.67507 6.704615,-16.14678 4.468434,-4.46843 9.851134,-6.70396 16.145474,-6.70396 4.54773,0 8.70027,1.24392 12.45629,3.7285 3.72785,2.43607 6.49162,5.63437 8.29,9.60274 z m -20.74695,-3.5332 c -3.64985,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.808452,5.67435 -3.808452,9.32289 v 0.27789 l 22.175692,-9.95731 c -1.95633,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" /> + <path d="m 146.11999,242.03442 c 1.2957,0 2.41904,0.46336 3.37197,1.38876 0.95228,0.95097 1.42874,2.08938 1.42874,3.4113 v 15.4311 c 2.98792,-2.64318 7.36525,-3.96707 13.13004,-3.96707 6.29434,0 11.67638,2.23488 16.14613,6.70396 4.46908,4.47106 6.70461,9.85245 6.70461,16.14679 0,6.29499 -2.23553,11.67638 -6.70461,16.14678 -4.46909,4.4704 -9.85113,6.70396 -16.14613,6.70396 -6.295,0 -11.66262,-2.22111 -16.10549,-6.66529 -4.4704,-4.41469 -6.71838,-9.77052 -6.7446,-16.06617 v -34.43341 c 0,-1.32257 0.47647,-2.46032 1.42875,-3.41129 0.95162,-0.92541 2.07561,-1.38877 3.37197,-1.38877 h 0.11862 z m 17.93009,26.06148 c -3.64919,0 -6.75704,1.28391 -9.32288,3.84909 -2.53767,2.5665 -3.80781,5.67435 -3.80781,9.32289 0,3.62364 1.27014,6.71772 3.80781,9.28291 2.56584,2.56519 5.67303,3.84778 9.32288,3.84778 3.62364,0 6.71773,-1.28259 9.28357,-3.84778 2.56387,-2.56519 3.84712,-5.65927 3.84712,-9.28291 0,-3.64788 -1.28325,-6.75639 -3.84712,-9.32289 -2.56584,-2.56518 -5.65927,-3.84909 -9.28357,-3.84909 z" /> + </g> + <g fill="#306998"> + <path d="m 205.94246,268.01922 c -1.16397,0 -2.14247,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30172,3.56989 0.52825,0.29165 2.7369,0.95228 6.62466,1.98386 3.14717,0.89985 5.48691,2.07627 7.02051,3.53057 2.19621,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34879,7.0736 -4.04571,9.79739 -2.72444,2.69823 -5.9909,4.04636 -9.7987,4.04636 h -10.35381 c -1.29701,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42875,-2.02252 -1.42875,-3.37065 v -0.0806 c 0,-1.32126 0.47647,-2.45901 1.42875,-3.41129 0.95227,-0.95228 2.07561,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16331,0 2.14246,-0.39586 2.93548,-1.18888 0.79368,-0.79367 1.19019,-1.77151 1.19019,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26044,-3.29201 -0.3965,-0.18678 -2.61892,-0.84742 -6.66529,-1.98386 -3.14782,-0.9254 -5.48887,-2.14377 -7.02247,-3.65051 -2.19555,-2.1418 -3.29202,-5.17035 -3.29202,-9.08432 0,-3.80846 1.34945,-7.06049 4.04702,-9.75807 2.72314,-2.72379 5.99024,-4.087 9.79805,-4.087 h 7.2997 c 1.32192,0 2.45967,0.47647 3.41195,1.43006 0.95162,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47647,2.47409 -1.42809,3.37263 -0.95228,0.95097 -2.09003,1.42874 -3.41195,1.42874 z" /> + <path d="m 249.06434,258.29851 c 6.29434,0 11.67573,2.23488 16.14612,6.70396 4.46909,4.47106 6.70396,9.85245 6.70396,16.14679 0,6.29499 -2.23487,11.67638 -6.70396,16.14678 -4.46974,4.46974 -9.85178,6.70396 -16.14612,6.70396 -6.29435,0 -11.67639,-2.23356 -16.14548,-6.70396 -4.46974,-4.46974 -6.70461,-9.85113 -6.70461,-16.14678 0,-6.29434 2.23487,-11.67508 6.70461,-16.14679 4.46909,-4.46908 9.85113,-6.70396 16.14548,-6.70396 z m 0,9.79739 c -3.64986,0 -6.7577,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80911,5.67435 -3.80911,9.32289 0,3.62364 1.26948,6.71772 3.80911,9.28291 2.56519,2.56519 5.67238,3.84778 9.32289,3.84778 3.62298,0 6.71706,-1.28259 9.28291,-3.84778 2.56518,-2.56519 3.84778,-5.65927 3.84778,-9.28291 0,-3.64788 -1.2826,-6.75639 -3.84778,-9.32289 -2.56585,-2.56518 -5.65928,-3.84909 -9.28291,-3.84909 z" /> + <path d="m 307.22146,259.37007 c 2.24864,0.71438 3.37263,2.24798 3.37263,4.60148 v 0.19989 c 0,1.6116 -0.64884,2.89419 -1.94454,3.84778 -0.89919,0.63376 -1.82525,0.95097 -2.77622,0.95097 -0.50334,0 -1.01913,-0.0793 -1.54737,-0.23791 -1.29636,-0.42272 -2.63204,-0.63638 -4.00638,-0.63638 -3.64986,0 -6.75836,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80846,5.67435 -3.80846,9.32289 0,3.62364 1.26883,6.71772 3.80846,9.28291 2.56453,2.56519 5.67238,3.84778 9.32289,3.84778 1.375,0 2.71068,-0.21103 4.00638,-0.63507 0.50203,-0.1586 1.00471,-0.2379 1.50739,-0.2379 0.97718,0 1.91767,0.31851 2.81686,0.95358 1.2957,0.95097 1.94453,2.24798 1.94453,3.88776 0,2.32728 -1.12464,3.86089 -3.37262,4.60148 -2.22111,0.6875 -4.52152,1.03027 -6.90189,1.03027 -6.29434,0 -11.67638,-2.23356 -16.14678,-6.70396 -4.46843,-4.46974 -6.70396,-9.85113 -6.70396,-16.14678 0,-6.29435 2.23487,-11.67508 6.70396,-16.14679 4.46974,-4.46843 9.85178,-6.70396 16.14678,-6.70396 2.37906,0.001 4.68012,0.35981 6.90123,1.07287 z" /> + <path d="m 322.25671,242.03442 c 1.29504,0 2.41903,0.46336 3.37262,1.38876 0.95163,0.95097 1.42809,2.08938 1.42809,3.4113 v 27.49154 h 1.50739 c 3.38508,0 6.33301,-1.12399 8.84708,-3.37263 2.45901,-2.24798 3.86023,-5.0242 4.20431,-8.33063 0.15861,-1.24261 0.68816,-2.26174 1.58735,-3.0541 0.89854,-0.84611 1.96944,-1.27015 3.21271,-1.27015 h 0.11863 c 1.40252,0 2.5796,0.53021 3.53122,1.58735 0.84676,0.92541 1.26949,1.99697 1.26949,3.21271 0,0.15861 -0.0138,0.33163 -0.0393,0.51579 -0.63507,6.63842 -3.17405,11.61019 -7.61692,14.91531 2.32663,1.43006 4.46909,3.84909 6.42739,7.26039 2.03563,3.51746 3.05476,7.31412 3.05476,11.38473 v 2.02515 c 0,1.34813 -0.47712,2.47147 -1.42809,3.37066 -0.95359,0.95359 -2.07692,1.42874 -3.37263,1.42874 h -0.11928 c -1.29635,0 -2.41969,-0.47515 -3.37196,-1.42874 -0.95228,-0.89854 -1.42809,-2.02253 -1.42809,-3.37066 v -2.02515 c -0.0275,-3.59414 -1.31012,-6.67708 -3.84844,-9.24358 -2.56584,-2.53832 -5.66058,-3.80715 -9.28291,-3.80715 h -3.25269 v 15.07523 c 0,1.34813 -0.47646,2.47146 -1.42809,3.37065 -0.95293,0.95359 -2.07758,1.42875 -3.37262,1.42875 h -0.12059 c -1.2957,0 -2.41838,-0.47516 -3.37132,-1.42875 -0.95162,-0.89853 -1.42809,-2.02252 -1.42809,-3.37065 v -52.36547 c 0,-1.32257 0.47647,-2.46032 1.42809,-3.41129 0.95228,-0.92541 2.07562,-1.38877 3.37132,-1.38877 h 0.12059 z" /> + <path d="m 402.31164,271.6291 c 0.31721,0.66063 0.47581,1.33371 0.47581,2.02252 0,0.55577 -0.10617,1.11089 -0.31655,1.66665 -0.45025,1.24262 -1.29635,2.14181 -2.53897,2.69692 -3.70294,1.66665 -8.56919,3.8622 -14.59876,6.58599 -7.48452,3.38442 -11.87496,5.38139 -13.17067,5.9909 2.00877,2.53832 5.14349,3.80715 9.40219,3.80715 2.82866,0 5.3945,-0.83234 7.69622,-2.499 2.24732,-1.63977 3.82091,-3.75537 4.7201,-6.34808 0.76681,-2.16868 2.30172,-3.25269 4.60148,-3.25269 1.63978,0 2.94924,0.68881 3.92839,2.06185 0.60689,0.84742 0.91165,1.7335 0.91165,2.65891 0,0.55577 -0.10552,1.12399 -0.31721,1.70532 -1.56048,4.52348 -4.29738,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.40994,4.32293 -13.32928,4.32293 -6.29434,0 -11.67638,-2.23356 -16.14547,-6.70395 -4.46974,-4.46975 -6.70461,-9.85114 -6.70461,-16.14679 0,-6.29434 2.23487,-11.67507 6.70461,-16.14678 4.46843,-4.46843 9.85113,-6.70396 16.14547,-6.70396 4.54774,0 8.70093,1.24392 12.4563,3.7285 3.7285,2.43607 6.49161,5.63437 8.29065,9.60274 z m -20.7476,-3.5332 c -3.6492,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.80846,5.67435 -3.80846,9.32289 v 0.27789 l 22.1757,-9.95731 c -1.95699,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" /> + <path d="m 415.48166,242.03442 c 1.2957,0 2.41969,0.46336 3.37262,1.38876 0.95162,0.95097 1.42809,2.08938 1.42809,3.4113 v 11.46403 h 5.95092 c 1.2957,0 2.41903,0.47647 3.37262,1.43006 0.95163,0.95097 1.42678,2.08938 1.42678,3.40998 v 0.0793 c 0,1.34945 -0.47515,2.47409 -1.42678,3.37263 -0.95293,0.95097 -2.07692,1.42874 -3.37262,1.42874 h -5.95092 v 23.52252 c 0,0.76811 0.26347,1.41695 0.79367,1.94453 0.5289,0.53021 1.19019,0.79368 1.98321,0.79368 h 3.17404 c 1.2957,0 2.41903,0.47646 3.37262,1.42874 0.95163,0.95228 1.42678,2.09003 1.42678,3.41129 v 0.0806 c 0,1.34813 -0.47515,2.47146 -1.42678,3.37065 C 428.65298,303.52484 427.52899,304 426.23329,304 h -3.17404 c -3.43817,0 -6.38675,-1.21574 -8.84642,-3.6492 -2.43411,-2.45901 -3.6492,-5.39515 -3.6492,-8.80775 v -44.70726 c 0,-1.32258 0.47581,-2.46033 1.42809,-3.4113 0.95228,-0.9254 2.07627,-1.38876 3.37197,-1.38876 h 0.11797 z" /> + <path d="m 448.88545,268.01922 c -1.16397,0 -2.14246,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30042,3.56989 0.5302,0.29165 2.7382,0.95228 6.62596,1.98386 3.14652,0.89985 5.48691,2.07627 7.02117,3.53057 2.19489,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34945,7.0736 -4.04637,9.79739 -2.72379,2.69823 -5.99089,4.04636 -9.79869,4.04636 h -10.35382 c -1.29635,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42744,-2.02252 -1.42744,-3.37065 v -0.0806 c 0,-1.32126 0.47516,-2.45901 1.42744,-3.41129 0.95228,-0.95228 2.07627,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16332,0 2.14312,-0.39586 2.93549,-1.18888 0.79367,-0.79367 1.19018,-1.77151 1.19018,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26043,-3.29201 -0.39782,-0.18678 -2.61893,-0.84742 -6.66529,-1.98386 -3.14783,-0.9254 -5.48887,-2.14377 -7.02248,-3.65051 -2.19555,-2.1418 -3.29201,-5.17035 -3.29201,-9.08432 0,-3.80846 1.34944,-7.06049 4.04701,-9.75807 2.72314,-2.72379 5.99025,-4.087 9.7987,-4.087 h 7.29906 c 1.32322,0 2.45967,0.47647 3.41129,1.43006 0.95228,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47581,2.47409 -1.42809,3.37263 -0.95162,0.95097 -2.08872,1.42874 -3.41129,1.42874 z" /> + </g> + </g> +</svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/pyproject.toml b/testing/web-platform/tests/tools/third_party/websockets/pyproject.toml new file mode 100644 index 0000000000..f24616dd7e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/pyproject.toml @@ -0,0 +1,87 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "websockets" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +requires-python = ">=3.8" +license = { text = "BSD-3-Clause" } +authors = [ + { name = "Aymeric Augustin", email = "aymeric.augustin@m4x.org" }, +] +keywords = ["WebSocket"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dynamic = ["version", "readme"] + +[project.urls] +Homepage = "https://github.com/python-websockets/websockets" +Changelog = "https://websockets.readthedocs.io/en/stable/project/changelog.html" +Documentation = "https://websockets.readthedocs.io/" +Funding = "https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme" +Tracker = "https://github.com/python-websockets/websockets/issues" + +# On a macOS runner, build Intel, Universal, and Apple Silicon wheels. +[tool.cibuildwheel.macos] +archs = ["x86_64", "universal2", "arm64"] + +# On an Linux Intel runner with QEMU installed, build Intel and ARM wheels. +[tool.cibuildwheel.linux] +archs = ["auto", "aarch64"] + +[tool.coverage.run] +branch = true +omit = [ + # */websockets matches src/websockets and .tox/**/site-packages/websockets + "*/websockets/__main__.py", + "*/websockets/legacy/async_timeout.py", + "*/websockets/legacy/compatibility.py", + "tests/maxi_cov.py", +] + +[tool.coverage.paths] +source = [ + "src/websockets", + ".tox/*/lib/python*/site-packages/websockets", +] + +[tool.coverage.report] +exclude_lines = [ + "except ImportError:", + "if self.debug:", + "if sys.platform != \"win32\":", + "if typing.TYPE_CHECKING:", + "pragma: no cover", + "raise AssertionError", + "raise NotImplementedError", + "self.fail\\(\".*\"\\)", + "@unittest.skip", +] + +[tool.ruff] +select = [ + "E", # pycodestyle + "F", # Pyflakes + "W", # pycodestyle + "I", # isort +] +ignore = [ + "F403", + "F405", +] + +[tool.ruff.isort] +combine-as-imports = true +lines-after-imports = 2 diff --git a/testing/web-platform/tests/tools/third_party/websockets/setup.cfg b/testing/web-platform/tests/tools/third_party/websockets/setup.cfg deleted file mode 100644 index dc424fe195..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[bdist_wheel] -python-tag = py37.py38.py39.py310 - -[metadata] -license_file = LICENSE -project_urls = - Changelog = https://websockets.readthedocs.io/en/stable/project/changelog.html - Documentation = https://websockets.readthedocs.io/ - Funding = https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme - Tracker = https://github.com/aaugustin/websockets/issues - -[flake8] -ignore = E203,E731,F403,F405,W503 -max-line-length = 88 - -[isort] -profile = black -combine_as_imports = True -lines_after_imports = 2 - -[coverage:run] -branch = True -omit = - */__main__.py -source = - websockets - tests - -[coverage:paths] -source = - src/websockets - .tox/*/lib/python*/site-packages/websockets - -[coverage:report] -exclude_lines = - if self.debug: - pragma: no cover - -[egg_info] -tag_build = -tag_date = 0 - diff --git a/testing/web-platform/tests/tools/third_party/websockets/setup.py b/testing/web-platform/tests/tools/third_party/websockets/setup.py index b2d07737d2..ae0aaa65de 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/setup.py +++ b/testing/web-platform/tests/tools/third_party/websockets/setup.py @@ -1,3 +1,4 @@ +import os import pathlib import re @@ -6,58 +7,32 @@ import setuptools root_dir = pathlib.Path(__file__).parent -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +exec((root_dir / "src" / "websockets" / "version.py").read_text(encoding="utf-8")) -long_description = (root_dir / 'README.rst').read_text(encoding='utf-8') - -# PyPI disables the "raw" directive. +# PyPI disables the "raw" directive. Remove this section of the README. long_description = re.sub( r"^\.\. raw:: html.*?^(?=\w)", "", - long_description, + (root_dir / "README.rst").read_text(encoding="utf-8"), flags=re.DOTALL | re.MULTILINE, ) -exec((root_dir / 'src' / 'websockets' / 'version.py').read_text(encoding='utf-8')) - -packages = ['websockets', 'websockets/legacy', 'websockets/extensions'] - -ext_modules = [ - setuptools.Extension( - 'websockets.speedups', - sources=['src/websockets/speedups.c'], - optional=not (root_dir / '.cibuildwheel').exists(), - ) -] - +# Set BUILD_EXTENSION to yes or no to force building or not building the +# speedups extension. If unset, the extension is built only if possible. +if os.environ.get("BUILD_EXTENSION") == "no": + ext_modules = [] +else: + ext_modules = [ + setuptools.Extension( + "websockets.speedups", + sources=["src/websockets/speedups.c"], + optional=os.environ.get("BUILD_EXTENSION") != "yes", + ) + ] + +# Static values are declared in pyproject.toml. setuptools.setup( - name='websockets', version=version, - description=description, long_description=long_description, - url='https://github.com/aaugustin/websockets', - author='Aymeric Augustin', - author_email='aymeric.augustin@m4x.org', - license='BSD', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - ], - package_dir = {'': 'src'}, - package_data = {'websockets': ['py.typed']}, - packages=packages, ext_modules=ext_modules, - include_package_data=True, - zip_safe=False, - python_requires='>=3.7', - test_loader='unittest:TestLoader', ) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO deleted file mode 100644 index 3b042a3f9f..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO +++ /dev/null @@ -1,174 +0,0 @@ -Metadata-Version: 2.1 -Name: websockets -Version: 10.3 -Summary: An implementation of the WebSocket Protocol (RFC 6455 & 7692) -Home-page: https://github.com/aaugustin/websockets -Author: Aymeric Augustin -Author-email: aymeric.augustin@m4x.org -License: BSD -Project-URL: Changelog, https://websockets.readthedocs.io/en/stable/project/changelog.html -Project-URL: Documentation, https://websockets.readthedocs.io/ -Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme -Project-URL: Tracker, https://github.com/aaugustin/websockets/issues -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Requires-Python: >=3.7 -License-File: LICENSE - -.. image:: logo/horizontal.svg - :width: 480px - :alt: websockets - -|licence| |version| |pyversions| |wheel| |tests| |docs| - -.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |version| image:: https://img.shields.io/pypi/v/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main - :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml - -.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg - :target: https://websockets.readthedocs.io/ - -What is ``websockets``? ------------------------ - -websockets is a library for building WebSocket_ servers and clients in Python -with a focus on correctness, simplicity, robustness, and performance. - -.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API - -Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it -provides an elegant coroutine-based API. - -`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ - -Here's how a client sends and receives messages: - -.. copy-pasted because GitHub doesn't support the include directive - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import connect - - async def hello(uri): - async with connect(uri) as websocket: - await websocket.send("Hello world!") - await websocket.recv() - - asyncio.run(hello("ws://localhost:8765")) - -And here's an echo server: - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import serve - - async def echo(websocket): - async for message in websocket: - await websocket.send(message) - - async def main(): - async with serve(echo, "localhost", 8765): - await asyncio.Future() # run forever - - asyncio.run(main()) - -Does that look good? - -`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_ - -Why should I use ``websockets``? --------------------------------- - -The development of ``websockets`` is shaped by four principles: - -1. **Correctness**: ``websockets`` is heavily tested for compliance - with :rfc:`6455`. Continuous integration fails under 100% branch - coverage. - -2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and - ``await ws.send(msg)``. ``websockets`` takes care of managing connections - so you can focus on your application. - -3. **Robustness**: ``websockets`` is built for production. For example, it was - the only library to `handle backpressure correctly`_ before the issue - became widely known in the Python community. - -4. **Performance**: memory usage is optimized and configurable. A C extension - accelerates expensive operations. It's pre-compiled for Linux, macOS and - Windows and packaged in the wheel format for each system and Python version. - -Documentation is a first class concern in the project. Head over to `Read the -Docs`_ and see for yourself. - -.. _Read the Docs: https://websockets.readthedocs.io/ -.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers - -Why shouldn't I use ``websockets``? ------------------------------------ - -* If you prefer callbacks over coroutines: ``websockets`` was created to - provide the best coroutine-based API to manage WebSocket connections in - Python. Pick another library for a callback-based API. - -* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims - at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol - and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP - is minimal — just enough for a HTTP health check. - - If you want to do both in the same server, look at HTTP frameworks that - build on top of ``websockets`` to support WebSocket connections, like - Sanic_. - -.. _Sanic: https://sanicframework.org/en/ - -What else? ----------- - -Bug reports, patches and suggestions are welcome! - -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -.. _Tidelift security contact: https://tidelift.com/security - -For anything else, please open an issue_ or send a `pull request`_. - -.. _issue: https://github.com/aaugustin/websockets/issues/new -.. _pull request: https://github.com/aaugustin/websockets/compare/ - -Participants must uphold the `Contributor Covenant code of conduct`_. - -.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md - -``websockets`` is released under the `BSD license`_. - -.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE - - diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt deleted file mode 100644 index 2a51106fee..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt +++ /dev/null @@ -1,42 +0,0 @@ -LICENSE -MANIFEST.in -README.rst -setup.cfg -setup.py -src/websockets/__init__.py -src/websockets/__main__.py -src/websockets/auth.py -src/websockets/client.py -src/websockets/connection.py -src/websockets/datastructures.py -src/websockets/exceptions.py -src/websockets/frames.py -src/websockets/headers.py -src/websockets/http.py -src/websockets/http11.py -src/websockets/imports.py -src/websockets/py.typed -src/websockets/server.py -src/websockets/speedups.c -src/websockets/streams.py -src/websockets/typing.py -src/websockets/uri.py -src/websockets/utils.py -src/websockets/version.py -src/websockets.egg-info/PKG-INFO -src/websockets.egg-info/SOURCES.txt -src/websockets.egg-info/dependency_links.txt -src/websockets.egg-info/not-zip-safe -src/websockets.egg-info/top_level.txt -src/websockets/extensions/__init__.py -src/websockets/extensions/base.py -src/websockets/extensions/permessage_deflate.py -src/websockets/legacy/__init__.py -src/websockets/legacy/auth.py -src/websockets/legacy/client.py -src/websockets/legacy/compatibility.py -src/websockets/legacy/framing.py -src/websockets/legacy/handshake.py -src/websockets/legacy/http.py -src/websockets/legacy/protocol.py -src/websockets/legacy/server.py
\ No newline at end of file diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe deleted file mode 100644 index 8b13789179..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt deleted file mode 100644 index 5474af7431..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt +++ /dev/null @@ -1,3 +0,0 @@ -websockets -websockets/extensions -websockets/legacy diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py index ec34841247..fdb028f4c4 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py @@ -1,23 +1,24 @@ from __future__ import annotations +import typing + from .imports import lazy_import -from .version import version as __version__ # noqa +from .version import version as __version__ # noqa: F401 -__all__ = [ # noqa +__all__ = [ + # .client + "ClientProtocol", + # .datastructures + "Headers", + "HeadersLike", + "MultipleValuesError", + # .exceptions "AbortHandshake", - "basic_auth_protocol_factory", - "BasicAuthWebSocketServerProtocol", - "broadcast", - "ClientConnection", - "connect", "ConnectionClosed", "ConnectionClosedError", "ConnectionClosedOK", - "Data", "DuplicateParameter", - "ExtensionName", - "ExtensionParameter", "InvalidHandshake", "InvalidHeader", "InvalidHeaderFormat", @@ -31,84 +32,159 @@ __all__ = [ # noqa "InvalidStatusCode", "InvalidUpgrade", "InvalidURI", - "LoggerLike", "NegotiationError", - "Origin", - "parse_uri", "PayloadTooBig", "ProtocolError", "RedirectHandshake", "SecurityError", - "serve", - "ServerConnection", - "Subprotocol", - "unix_connect", - "unix_serve", - "WebSocketClientProtocol", - "WebSocketCommonProtocol", "WebSocketException", "WebSocketProtocolError", + # .legacy.auth + "BasicAuthWebSocketServerProtocol", + "basic_auth_protocol_factory", + # .legacy.client + "WebSocketClientProtocol", + "connect", + "unix_connect", + # .legacy.protocol + "WebSocketCommonProtocol", + "broadcast", + # .legacy.server "WebSocketServer", "WebSocketServerProtocol", - "WebSocketURI", + "serve", + "unix_serve", + # .server + "ServerProtocol", + # .typing + "Data", + "ExtensionName", + "ExtensionParameter", + "LoggerLike", + "StatusLike", + "Origin", + "Subprotocol", ] -lazy_import( - globals(), - aliases={ - "auth": ".legacy", - "basic_auth_protocol_factory": ".legacy.auth", - "BasicAuthWebSocketServerProtocol": ".legacy.auth", - "broadcast": ".legacy.protocol", - "ClientConnection": ".client", - "connect": ".legacy.client", - "unix_connect": ".legacy.client", - "WebSocketClientProtocol": ".legacy.client", - "Headers": ".datastructures", - "MultipleValuesError": ".datastructures", - "WebSocketException": ".exceptions", - "ConnectionClosed": ".exceptions", - "ConnectionClosedError": ".exceptions", - "ConnectionClosedOK": ".exceptions", - "InvalidHandshake": ".exceptions", - "SecurityError": ".exceptions", - "InvalidMessage": ".exceptions", - "InvalidHeader": ".exceptions", - "InvalidHeaderFormat": ".exceptions", - "InvalidHeaderValue": ".exceptions", - "InvalidOrigin": ".exceptions", - "InvalidUpgrade": ".exceptions", - "InvalidStatus": ".exceptions", - "InvalidStatusCode": ".exceptions", - "NegotiationError": ".exceptions", - "DuplicateParameter": ".exceptions", - "InvalidParameterName": ".exceptions", - "InvalidParameterValue": ".exceptions", - "AbortHandshake": ".exceptions", - "RedirectHandshake": ".exceptions", - "InvalidState": ".exceptions", - "InvalidURI": ".exceptions", - "PayloadTooBig": ".exceptions", - "ProtocolError": ".exceptions", - "WebSocketProtocolError": ".exceptions", - "protocol": ".legacy", - "WebSocketCommonProtocol": ".legacy.protocol", - "ServerConnection": ".server", - "serve": ".legacy.server", - "unix_serve": ".legacy.server", - "WebSocketServerProtocol": ".legacy.server", - "WebSocketServer": ".legacy.server", - "Data": ".typing", - "LoggerLike": ".typing", - "Origin": ".typing", - "ExtensionHeader": ".typing", - "ExtensionParameter": ".typing", - "Subprotocol": ".typing", - }, - deprecated_aliases={ - "framing": ".legacy", - "handshake": ".legacy", - "parse_uri": ".uri", - "WebSocketURI": ".uri", - }, -) +# When type checking, import non-deprecated aliases eagerly. Else, import on demand. +if typing.TYPE_CHECKING: + from .client import ClientProtocol + from .datastructures import Headers, HeadersLike, MultipleValuesError + from .exceptions import ( + AbortHandshake, + ConnectionClosed, + ConnectionClosedError, + ConnectionClosedOK, + DuplicateParameter, + InvalidHandshake, + InvalidHeader, + InvalidHeaderFormat, + InvalidHeaderValue, + InvalidMessage, + InvalidOrigin, + InvalidParameterName, + InvalidParameterValue, + InvalidState, + InvalidStatus, + InvalidStatusCode, + InvalidUpgrade, + InvalidURI, + NegotiationError, + PayloadTooBig, + ProtocolError, + RedirectHandshake, + SecurityError, + WebSocketException, + WebSocketProtocolError, + ) + from .legacy.auth import ( + BasicAuthWebSocketServerProtocol, + basic_auth_protocol_factory, + ) + from .legacy.client import WebSocketClientProtocol, connect, unix_connect + from .legacy.protocol import WebSocketCommonProtocol, broadcast + from .legacy.server import ( + WebSocketServer, + WebSocketServerProtocol, + serve, + unix_serve, + ) + from .server import ServerProtocol + from .typing import ( + Data, + ExtensionName, + ExtensionParameter, + LoggerLike, + Origin, + StatusLike, + Subprotocol, + ) +else: + lazy_import( + globals(), + aliases={ + # .client + "ClientProtocol": ".client", + # .datastructures + "Headers": ".datastructures", + "HeadersLike": ".datastructures", + "MultipleValuesError": ".datastructures", + # .exceptions + "AbortHandshake": ".exceptions", + "ConnectionClosed": ".exceptions", + "ConnectionClosedError": ".exceptions", + "ConnectionClosedOK": ".exceptions", + "DuplicateParameter": ".exceptions", + "InvalidHandshake": ".exceptions", + "InvalidHeader": ".exceptions", + "InvalidHeaderFormat": ".exceptions", + "InvalidHeaderValue": ".exceptions", + "InvalidMessage": ".exceptions", + "InvalidOrigin": ".exceptions", + "InvalidParameterName": ".exceptions", + "InvalidParameterValue": ".exceptions", + "InvalidState": ".exceptions", + "InvalidStatus": ".exceptions", + "InvalidStatusCode": ".exceptions", + "InvalidUpgrade": ".exceptions", + "InvalidURI": ".exceptions", + "NegotiationError": ".exceptions", + "PayloadTooBig": ".exceptions", + "ProtocolError": ".exceptions", + "RedirectHandshake": ".exceptions", + "SecurityError": ".exceptions", + "WebSocketException": ".exceptions", + "WebSocketProtocolError": ".exceptions", + # .legacy.auth + "BasicAuthWebSocketServerProtocol": ".legacy.auth", + "basic_auth_protocol_factory": ".legacy.auth", + # .legacy.client + "WebSocketClientProtocol": ".legacy.client", + "connect": ".legacy.client", + "unix_connect": ".legacy.client", + # .legacy.protocol + "WebSocketCommonProtocol": ".legacy.protocol", + "broadcast": ".legacy.protocol", + # .legacy.server + "WebSocketServer": ".legacy.server", + "WebSocketServerProtocol": ".legacy.server", + "serve": ".legacy.server", + "unix_serve": ".legacy.server", + # .server + "ServerProtocol": ".server", + # .typing + "Data": ".typing", + "ExtensionName": ".typing", + "ExtensionParameter": ".typing", + "LoggerLike": ".typing", + "Origin": ".typing", + "StatusLike": "typing", + "Subprotocol": ".typing", + }, + deprecated_aliases={ + "framing": ".legacy", + "handshake": ".legacy", + "parse_uri": ".uri", + "WebSocketURI": ".uri", + }, + ) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py index c562d21b54..f2ea5cf4e8 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py @@ -1,16 +1,18 @@ from __future__ import annotations import argparse -import asyncio import os import signal import sys import threading -from typing import Any, Set -from .exceptions import ConnectionClosed -from .frames import Close -from .legacy.client import connect + +try: + import readline # noqa: F401 +except ImportError: # Windows has no `readline` normally + pass + +from .sync.client import ClientConnection, connect from .version import version as websockets_version @@ -46,21 +48,6 @@ if sys.platform == "win32": raise RuntimeError("unable to set console mode") -def exit_from_event_loop_thread( - loop: asyncio.AbstractEventLoop, - stop: asyncio.Future[None], -) -> None: - loop.stop() - if not stop.done(): - # When exiting the thread that runs the event loop, raise - # KeyboardInterrupt in the main thread to exit the program. - if sys.platform == "win32": - ctrl_c = signal.CTRL_C_EVENT - else: - ctrl_c = signal.SIGINT - os.kill(os.getpid(), ctrl_c) - - def print_during_input(string: str) -> None: sys.stdout.write( # Save cursor position @@ -93,63 +80,20 @@ def print_over_input(string: str) -> None: sys.stdout.flush() -async def run_client( - uri: str, - loop: asyncio.AbstractEventLoop, - inputs: asyncio.Queue[str], - stop: asyncio.Future[None], -) -> None: - try: - websocket = await connect(uri) - except Exception as exc: - print_over_input(f"Failed to connect to {uri}: {exc}.") - exit_from_event_loop_thread(loop, stop) - return - else: - print_during_input(f"Connected to {uri}.") - - try: - while True: - incoming: asyncio.Future[Any] = asyncio.create_task(websocket.recv()) - outgoing: asyncio.Future[Any] = asyncio.create_task(inputs.get()) - done: Set[asyncio.Future[Any]] - pending: Set[asyncio.Future[Any]] - done, pending = await asyncio.wait( - [incoming, outgoing, stop], return_when=asyncio.FIRST_COMPLETED - ) - - # Cancel pending tasks to avoid leaking them. - if incoming in pending: - incoming.cancel() - if outgoing in pending: - outgoing.cancel() - - if incoming in done: - try: - message = incoming.result() - except ConnectionClosed: - break - else: - if isinstance(message, str): - print_during_input("< " + message) - else: - print_during_input("< (binary) " + message.hex()) - - if outgoing in done: - message = outgoing.result() - await websocket.send(message) - - if stop in done: - break - - finally: - await websocket.close() - assert websocket.close_code is not None and websocket.close_reason is not None - close_status = Close(websocket.close_code, websocket.close_reason) - - print_over_input(f"Connection closed: {close_status}.") - - exit_from_event_loop_thread(loop, stop) +def print_incoming_messages(websocket: ClientConnection, stop: threading.Event) -> None: + for message in websocket: + if isinstance(message, str): + print_during_input("< " + message) + else: + print_during_input("< (binary) " + message.hex()) + if not stop.is_set(): + # When the server closes the connection, raise KeyboardInterrupt + # in the main thread to exit the program. + if sys.platform == "win32": + ctrl_c = signal.CTRL_C_EVENT + else: + ctrl_c = signal.SIGINT + os.kill(os.getpid(), ctrl_c) def main() -> None: @@ -184,29 +128,17 @@ def main() -> None: sys.stderr.flush() try: - import readline # noqa - except ImportError: # Windows has no `readline` normally - pass - - # Create an event loop that will run in a background thread. - loop = asyncio.new_event_loop() - - # Due to zealous removal of the loop parameter in the Queue constructor, - # we need a factory coroutine to run in the freshly created event loop. - async def queue_factory() -> asyncio.Queue[str]: - return asyncio.Queue() - - # Create a queue of user inputs. There's no need to limit its size. - inputs: asyncio.Queue[str] = loop.run_until_complete(queue_factory()) - - # Create a stop condition when receiving SIGINT or SIGTERM. - stop: asyncio.Future[None] = loop.create_future() + websocket = connect(args.uri) + except Exception as exc: + print(f"Failed to connect to {args.uri}: {exc}.") + sys.exit(1) + else: + print(f"Connected to {args.uri}.") - # Schedule the task that will manage the connection. - loop.create_task(run_client(args.uri, loop, inputs, stop)) + stop = threading.Event() - # Start the event loop in a background thread. - thread = threading.Thread(target=loop.run_forever) + # Start the thread that reads messages from the connection. + thread = threading.Thread(target=print_incoming_messages, args=(websocket, stop)) thread.start() # Read from stdin in the main thread in order to receive signals. @@ -214,17 +146,14 @@ def main() -> None: while True: # Since there's no size limit, put_nowait is identical to put. message = input("> ") - loop.call_soon_threadsafe(inputs.put_nowait, message) + websocket.send(message) except (KeyboardInterrupt, EOFError): # ^C, ^D - loop.call_soon_threadsafe(stop.set_result, None) + stop.set() + websocket.close() + print_over_input("Connection closed.") - # Wait for the event loop to terminate. thread.join() - # For reasons unclear, even though the loop is closed in the thread, - # it still thinks it's running here. - loop.close() - if __name__ == "__main__": main() diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py index afcb38cffe..b792e02f5c 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py @@ -1,4 +1,6 @@ from __future__ import annotations # See #940 for why lazy_import isn't used here for backwards compatibility. -from .legacy.auth import * # noqa +# See #1400 for why listing compatibility imports in __all__ helps PyCharm. +from .legacy.auth import * +from .legacy.auth import __all__ # noqa: F401 diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py index df8e53429a..b2f622042d 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Generator, List, Optional, Sequence +import warnings +from typing import Any, Generator, List, Optional, Sequence -from .connection import CLIENT, CONNECTING, OPEN, Connection, State from .datastructures import Headers, MultipleValuesError from .exceptions import ( InvalidHandshake, @@ -23,8 +23,8 @@ from .headers import ( parse_subprotocol, parse_upgrade, ) -from .http import USER_AGENT from .http11 import Request, Response +from .protocol import CLIENT, CONNECTING, OPEN, Protocol, State from .typing import ( ConnectionOption, ExtensionHeader, @@ -38,13 +38,15 @@ from .utils import accept_key, generate_key # See #940 for why lazy_import isn't used here for backwards compatibility. -from .legacy.client import * # isort:skip # noqa +# See #1400 for why listing compatibility imports in __all__ helps PyCharm. +from .legacy.client import * # isort:skip # noqa: I001 +from .legacy.client import __all__ as legacy__all__ -__all__ = ["ClientConnection"] +__all__ = ["ClientProtocol"] + legacy__all__ -class ClientConnection(Connection): +class ClientProtocol(Protocol): """ Sans-I/O implementation of a WebSocket client connection. @@ -60,16 +62,17 @@ class ClientConnection(Connection): preference. state: initial state of the WebSocket connection. max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. + :obj:`None` disables the limit. logger: logger for this connection; defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../topics/logging>` for details. + see the :doc:`logging guide <../../topics/logging>` for details. """ def __init__( self, wsuri: WebSocketURI, + *, origin: Optional[Origin] = None, extensions: Optional[Sequence[ClientExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, @@ -89,7 +92,7 @@ class ClientConnection(Connection): self.available_subprotocols = subprotocols self.key = generate_key() - def connect(self) -> Request: # noqa: F811 + def connect(self) -> Request: """ Create a handshake request to open a connection. @@ -131,8 +134,6 @@ class ClientConnection(Connection): protocol_header = build_subprotocol(self.available_subprotocols) headers["Sec-WebSocket-Protocol"] = protocol_header - headers["User-Agent"] = USER_AGENT - return Request(self.wsuri.resource_name, headers) def process_response(self, response: Response) -> None: @@ -223,7 +224,6 @@ class ClientConnection(Connection): extensions = headers.get_all("Sec-WebSocket-Extensions") if extensions: - if self.available_extensions is None: raise InvalidHandshake("no extensions supported") @@ -232,9 +232,7 @@ class ClientConnection(Connection): ) for name, response_params in parsed_extensions: - for extension_factory in self.available_extensions: - # Skip non-matching extensions based on their name. if extension_factory.name != name: continue @@ -281,7 +279,6 @@ class ClientConnection(Connection): subprotocols = headers.get_all("Sec-WebSocket-Protocol") if subprotocols: - if self.available_subprotocols is None: raise InvalidHandshake("no subprotocols supported") @@ -317,11 +314,17 @@ class ClientConnection(Connection): def parse(self) -> Generator[None, None, None]: if self.state is CONNECTING: - response = yield from Response.parse( - self.reader.read_line, - self.reader.read_exact, - self.reader.read_to_eof, - ) + try: + response = yield from Response.parse( + self.reader.read_line, + self.reader.read_exact, + self.reader.read_to_eof, + ) + except Exception as exc: + self.handshake_exc = exc + self.parser = self.discard() + next(self.parser) # start coroutine + yield if self.debug: code, phrase = response.status_code, response.reason_phrase @@ -335,13 +338,23 @@ class ClientConnection(Connection): self.process_response(response) except InvalidHandshake as exc: response._exception = exc + self.events.append(response) self.handshake_exc = exc self.parser = self.discard() next(self.parser) # start coroutine - else: - assert self.state is CONNECTING - self.state = OPEN - finally: - self.events.append(response) + yield + + assert self.state is CONNECTING + self.state = OPEN + self.events.append(response) yield from super().parse() + + +class ClientConnection(ClientProtocol): + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "ClientConnection was renamed to ClientProtocol", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py index db8b536993..88bcda1aaf 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py @@ -1,702 +1,13 @@ from __future__ import annotations -import enum -import logging -import uuid -from typing import Generator, List, Optional, Type, Union +import warnings -from .exceptions import ( - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - InvalidState, - PayloadTooBig, - ProtocolError, -) -from .extensions import Extension -from .frames import ( - OK_CLOSE_CODES, - OP_BINARY, - OP_CLOSE, - OP_CONT, - OP_PING, - OP_PONG, - OP_TEXT, - Close, - Frame, -) -from .http11 import Request, Response -from .streams import StreamReader -from .typing import LoggerLike, Origin, Subprotocol - - -__all__ = [ - "Connection", - "Side", - "State", - "SEND_EOF", -] - -Event = Union[Request, Response, Frame] -"""Events that :meth:`~Connection.events_received` may return.""" - - -class Side(enum.IntEnum): - """A WebSocket connection is either a server or a client.""" - - SERVER, CLIENT = range(2) - - -SERVER = Side.SERVER -CLIENT = Side.CLIENT - - -class State(enum.IntEnum): - """A WebSocket connection is in one of these four states.""" - - CONNECTING, OPEN, CLOSING, CLOSED = range(4) - - -CONNECTING = State.CONNECTING -OPEN = State.OPEN -CLOSING = State.CLOSING -CLOSED = State.CLOSED - - -SEND_EOF = b"" -"""Sentinel signaling that the TCP connection must be half-closed.""" - - -class Connection: - """ - Sans-I/O implementation of a WebSocket connection. - - Args: - side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`. - state: initial state of the WebSocket connection. - max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. - logger: logger for this connection; depending on ``side``, - defaults to ``logging.getLogger("websockets.client")`` - or ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../topics/logging>` for details. - - """ - - def __init__( - self, - side: Side, - state: State = OPEN, - max_size: Optional[int] = 2**20, - logger: Optional[LoggerLike] = None, - ) -> None: - # Unique identifier. For logs. - self.id: uuid.UUID = uuid.uuid4() - """Unique identifier of the connection. Useful in logs.""" - - # Logger or LoggerAdapter for this connection. - if logger is None: - logger = logging.getLogger(f"websockets.{side.name.lower()}") - self.logger: LoggerLike = logger - """Logger for this connection.""" - - # Track if DEBUG is enabled. Shortcut logging calls if it isn't. - self.debug = logger.isEnabledFor(logging.DEBUG) - - # Connection side. CLIENT or SERVER. - self.side = side - - # Connection state. Initially OPEN because subclasses handle CONNECTING. - self.state = state - - # Maximum size of incoming messages in bytes. - self.max_size = max_size - - # Current size of incoming message in bytes. Only set while reading a - # fragmented message i.e. a data frames with the FIN bit not set. - self.cur_size: Optional[int] = None - - # True while sending a fragmented message i.e. a data frames with the - # FIN bit not set. - self.expect_continuation_frame = False - - # WebSocket protocol parameters. - self.origin: Optional[Origin] = None - self.extensions: List[Extension] = [] - self.subprotocol: Optional[Subprotocol] = None - - # Close code and reason, set when a close frame is sent or received. - self.close_rcvd: Optional[Close] = None - self.close_sent: Optional[Close] = None - self.close_rcvd_then_sent: Optional[bool] = None - - # Track if an exception happened during the handshake. - self.handshake_exc: Optional[Exception] = None - """ - Exception to raise if the opening handshake failed. - - :obj:`None` if the opening handshake succeeded. - - """ - - # Track if send_eof() was called. - self.eof_sent = False - - # Parser state. - self.reader = StreamReader() - self.events: List[Event] = [] - self.writes: List[bytes] = [] - self.parser = self.parse() - next(self.parser) # start coroutine - self.parser_exc: Optional[Exception] = None - - @property - def state(self) -> State: - """ - WebSocket connection state. - - Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`. - - """ - return self._state - - @state.setter - def state(self, state: State) -> None: - if self.debug: - self.logger.debug("= connection is %s", state.name) - self._state = state - - @property - def close_code(self) -> Optional[int]: - """ - `WebSocket close code`_. - - .. _WebSocket close code: - https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return 1006 - else: - return self.close_rcvd.code - - @property - def close_reason(self) -> Optional[str]: - """ - `WebSocket close reason`_. - - .. _WebSocket close reason: - https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return "" - else: - return self.close_rcvd.reason - - @property - def close_exc(self) -> ConnectionClosed: - """ - Exception to raise when trying to interact with a closed connection. - - Don't raise this exception while the connection :attr:`state` - is :attr:`~websockets.connection.State.CLOSING`; wait until - it's :attr:`~websockets.connection.State.CLOSED`. - - Indeed, the exception includes the close code and reason, which are - known only once the connection is closed. - - Raises: - AssertionError: if the connection isn't closed yet. - - """ - assert self.state is CLOSED, "connection isn't closed yet" - exc_type: Type[ConnectionClosed] - if ( - self.close_rcvd is not None - and self.close_sent is not None - and self.close_rcvd.code in OK_CLOSE_CODES - and self.close_sent.code in OK_CLOSE_CODES - ): - exc_type = ConnectionClosedOK - else: - exc_type = ConnectionClosedError - exc: ConnectionClosed = exc_type( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - # Chain to the exception raised in the parser, if any. - exc.__cause__ = self.parser_exc - return exc - - # Public methods for receiving data. - - def receive_data(self, data: bytes) -> None: - """ - Receive data from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network. - - You should call :meth:`events_received` and process resulting events. - - Raises: - EOFError: if :meth:`receive_eof` was called earlier. - - """ - self.reader.feed_data(data) - next(self.parser) - - def receive_eof(self) -> None: - """ - Receive the end of the data stream from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network. - - You aren't expected to call :meth:`events_received`; it won't return - any new events. - - Raises: - EOFError: if :meth:`receive_eof` was called earlier. - - """ - self.reader.feed_eof() - next(self.parser) - - # Public methods for sending events. - - def send_continuation(self, data: bytes, fin: bool) -> None: - """ - Send a `Continuation frame`_. - - .. _Continuation frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing the same kind of data - as the initial frame. - fin: FIN bit; set it to :obj:`True` if this is the last frame - of a fragmented message and to :obj:`False` otherwise. - - Raises: - ProtocolError: if a fragmented message isn't in progress. - - """ - if not self.expect_continuation_frame: - raise ProtocolError("unexpected continuation frame") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_CONT, data, fin)) - - def send_text(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Text frame`_. - - .. _Text frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing text encoded with UTF-8. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: if a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_TEXT, data, fin)) - - def send_binary(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Binary frame`_. +# lazy_import doesn't support this use case. +from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401 - .. _Binary frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - Parameters: - data: payload containing arbitrary binary data. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: if a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_BINARY, data, fin)) - - def send_close(self, code: Optional[int] = None, reason: str = "") -> None: - """ - Send a `Close frame`_. - - .. _Close frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 - - Parameters: - code: close code. - reason: close reason. - - Raises: - ProtocolError: if a fragmented message is being sent, if the code - isn't valid, or if a reason is provided without a code - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - if code is None: - if reason != "": - raise ProtocolError("cannot send a reason without a code") - close = Close(1005, "") - data = b"" - else: - close = Close(code, reason) - data = close.serialize() - # send_frame() guarantees that self.state is OPEN at this point. - # 7.1.3. The WebSocket Closing Handshake is Started - self.send_frame(Frame(OP_CLOSE, data)) - self.close_sent = close - self.state = CLOSING - - def send_ping(self, data: bytes) -> None: - """ - Send a `Ping frame`_. - - .. _Ping frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - Parameters: - data: payload containing arbitrary binary data. - - """ - self.send_frame(Frame(OP_PING, data)) - - def send_pong(self, data: bytes) -> None: - """ - Send a `Pong frame`_. - - .. _Pong frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - Parameters: - data: payload containing arbitrary binary data. - - """ - self.send_frame(Frame(OP_PONG, data)) - - def fail(self, code: int, reason: str = "") -> None: - """ - `Fail the WebSocket connection`_. - - .. _Fail the WebSocket connection: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7 - - Parameters: - code: close code - reason: close reason - - Raises: - ProtocolError: if the code isn't valid. - """ - # 7.1.7. Fail the WebSocket Connection - - # Send a close frame when the state is OPEN (a close frame was already - # sent if it's CLOSING), except when failing the connection because - # of an error reading from or writing to the network. - if self.state is OPEN: - if code != 1006: - close = Close(code, reason) - data = close.serialize() - self.send_frame(Frame(OP_CLOSE, data)) - self.close_sent = close - self.state = CLOSING - - # When failing the connection, a server closes the TCP connection - # without waiting for the client to complete the handshake, while a - # client waits for the server to close the TCP connection, possibly - # after sending a close frame that the client will ignore. - if self.side is SERVER and not self.eof_sent: - self.send_eof() - - # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue - # to attempt to process data(including a responding Close frame) from - # the remote endpoint after being instructed to _Fail the WebSocket - # Connection_." - self.parser = self.discard() - next(self.parser) # start coroutine - - # Public method for getting incoming events after receiving data. - - def events_received(self) -> List[Event]: - """ - Fetch events generated from data received from the network. - - Call this method immediately after any of the ``receive_*()`` methods. - - Process resulting events, likely by passing them to the application. - - Returns: - List[Event]: Events read from the connection. - """ - events, self.events = self.events, [] - return events - - # Public method for getting outgoing data after receiving data or sending events. - - def data_to_send(self) -> List[bytes]: - """ - Obtain data to send to the network. - - Call this method immediately after any of the ``receive_*()``, - ``send_*()``, or :meth:`fail` methods. - - Write resulting data to the connection. - - The empty bytestring :data:`~websockets.connection.SEND_EOF` signals - the end of the data stream. When you receive it, half-close the TCP - connection. - - Returns: - List[bytes]: Data to write to the connection. - - """ - writes, self.writes = self.writes, [] - return writes - - def close_expected(self) -> bool: - """ - Tell if the TCP connection is expected to close soon. - - Call this method immediately after any of the ``receive_*()`` or - :meth:`fail` methods. - - If it returns :obj:`True`, schedule closing the TCP connection after a - short timeout if the other side hasn't already closed it. - - Returns: - bool: Whether the TCP connection is expected to close soon. - - """ - # We expect a TCP close if and only if we sent a close frame: - # * Normal closure: once we send a close frame, we expect a TCP close: - # server waits for client to complete the TCP closing handshake; - # client waits for server to initiate the TCP closing handshake. - # * Abnormal closure: we always send a close frame and the same logic - # applies, except on EOFError where we don't send a close frame - # because we already received the TCP close, so we don't expect it. - # We already got a TCP Close if and only if the state is CLOSED. - return self.state is CLOSING or self.handshake_exc is not None - - # Private methods for receiving data. - - def parse(self) -> Generator[None, None, None]: - """ - Parse incoming data into frames. - - :meth:`receive_data` and :meth:`receive_eof` run this generator - coroutine until it needs more data or reaches EOF. - - """ - try: - while True: - if (yield from self.reader.at_eof()): - if self.debug: - self.logger.debug("< EOF") - # If the WebSocket connection is closed cleanly, with a - # closing handhshake, recv_frame() substitutes parse() - # with discard(). This branch is reached only when the - # connection isn't closed cleanly. - raise EOFError("unexpected end of stream") - - if self.max_size is None: - max_size = None - elif self.cur_size is None: - max_size = self.max_size - else: - max_size = self.max_size - self.cur_size - - # During a normal closure, execution ends here on the next - # iteration of the loop after receiving a close frame. At - # this point, recv_frame() replaced parse() by discard(). - frame = yield from Frame.parse( - self.reader.read_exact, - mask=self.side is SERVER, - max_size=max_size, - extensions=self.extensions, - ) - - if self.debug: - self.logger.debug("< %s", frame) - - self.recv_frame(frame) - - except ProtocolError as exc: - self.fail(1002, str(exc)) - self.parser_exc = exc - - except EOFError as exc: - self.fail(1006, str(exc)) - self.parser_exc = exc - - except UnicodeDecodeError as exc: - self.fail(1007, f"{exc.reason} at position {exc.start}") - self.parser_exc = exc - - except PayloadTooBig as exc: - self.fail(1009, str(exc)) - self.parser_exc = exc - - except Exception as exc: - self.logger.error("parser failed", exc_info=True) - # Don't include exception details, which may be security-sensitive. - self.fail(1011) - self.parser_exc = exc - - # During an abnormal closure, execution ends here after catching an - # exception. At this point, fail() replaced parse() by discard(). - yield - raise AssertionError("parse() shouldn't step after error") # pragma: no cover - - def discard(self) -> Generator[None, None, None]: - """ - Discard incoming data. - - This coroutine replaces :meth:`parse`: - - - after receiving a close frame, during a normal closure (1.4); - - after sending a close frame, during an abnormal closure (7.1.7). - - """ - # The server close the TCP connection in the same circumstances where - # discard() replaces parse(). The client closes the connection later, - # after the server closes the connection or a timeout elapses. - # (The latter case cannot be handled in this Sans-I/O layer.) - assert (self.side is SERVER) == (self.eof_sent) - while not (yield from self.reader.at_eof()): - self.reader.discard() - if self.debug: - self.logger.debug("< EOF") - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is CLIENT: - self.send_eof() - self.state = CLOSED - # If discard() completes normally, execution ends here. - yield - # Once the reader reaches EOF, its feed_data/eof() methods raise an - # error, so our receive_data/eof() methods don't step the generator. - raise AssertionError("discard() shouldn't step after EOF") # pragma: no cover - - def recv_frame(self, frame: Frame) -> None: - """ - Process an incoming frame. - - """ - if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY: - if self.cur_size is not None: - raise ProtocolError("expected a continuation frame") - if frame.fin: - self.cur_size = None - else: - self.cur_size = len(frame.data) - - elif frame.opcode is OP_CONT: - if self.cur_size is None: - raise ProtocolError("unexpected continuation frame") - if frame.fin: - self.cur_size = None - else: - self.cur_size += len(frame.data) - - elif frame.opcode is OP_PING: - # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST - # send a Pong frame in response" - pong_frame = Frame(OP_PONG, frame.data) - self.send_frame(pong_frame) - - elif frame.opcode is OP_PONG: - # 5.5.3 Pong: "A response to an unsolicited Pong frame is not - # expected." - pass - - elif frame.opcode is OP_CLOSE: - # 7.1.5. The WebSocket Connection Close Code - # 7.1.6. The WebSocket Connection Close Reason - self.close_rcvd = Close.parse(frame.data) - if self.state is CLOSING: - assert self.close_sent is not None - self.close_rcvd_then_sent = False - - if self.cur_size is not None: - raise ProtocolError("incomplete fragmented message") - - # 5.5.1 Close: "If an endpoint receives a Close frame and did - # not previously send a Close frame, the endpoint MUST send a - # Close frame in response. (When sending a Close frame in - # response, the endpoint typically echos the status code it - # received.)" - - if self.state is OPEN: - # Echo the original data instead of re-serializing it with - # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthetizes a 1005 close code. - # The rest is identical to send_close(). - self.send_frame(Frame(OP_CLOSE, frame.data)) - self.close_sent = self.close_rcvd - self.close_rcvd_then_sent = True - self.state = CLOSING - - # 7.1.2. Start the WebSocket Closing Handshake: "Once an - # endpoint has both sent and received a Close control frame, - # that endpoint SHOULD _Close the WebSocket Connection_" - - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is SERVER: - self.send_eof() - - # 1.4. Closing Handshake: "after receiving a control frame - # indicating the connection should be closed, a peer discards - # any further data received." - self.parser = self.discard() - next(self.parser) # start coroutine - - else: # pragma: no cover - # This can't happen because Frame.parse() validates opcodes. - raise AssertionError(f"unexpected opcode: {frame.opcode:02x}") - - self.events.append(frame) - - # Private methods for sending events. - - def send_frame(self, frame: Frame) -> None: - if self.state is not OPEN: - raise InvalidState( - f"cannot write to a WebSocket in the {self.state.name} state" - ) - - if self.debug: - self.logger.debug("> %s", frame) - self.writes.append( - frame.serialize(mask=self.side is CLIENT, extensions=self.extensions) - ) - - def send_eof(self) -> None: - assert not self.eof_sent - self.eof_sent = True - if self.debug: - self.logger.debug("> EOF") - self.writes.append(SEND_EOF) +warnings.warn( + "websockets.connection was renamed to websockets.protocol " + "and Connection was renamed to Protocol", + DeprecationWarning, +) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py index 36a2cbaf99..a0a648463a 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys from typing import ( Any, Dict, @@ -9,17 +8,12 @@ from typing import ( List, Mapping, MutableMapping, + Protocol, Tuple, Union, ) -if sys.version_info[:2] >= (3, 8): - from typing import Protocol -else: # pragma: no cover - Protocol = object # mypy will report errors on Python 3.7. - - __all__ = ["Headers", "HeadersLike", "MultipleValuesError"] diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py index 0c4fc51851..f7169e3b17 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py @@ -34,6 +34,7 @@ import http from typing import Optional from . import datastructures, frames, http11 +from .typing import StatusLike __all__ = [ @@ -120,19 +121,23 @@ class ConnectionClosed(WebSocketException): @property def code(self) -> int: - return 1006 if self.rcvd is None else self.rcvd.code + if self.rcvd is None: + return frames.CloseCode.ABNORMAL_CLOSURE + return self.rcvd.code @property def reason(self) -> str: - return "" if self.rcvd is None else self.rcvd.reason + if self.rcvd is None: + return "" + return self.rcvd.reason class ConnectionClosedError(ConnectionClosed): """ Like :exc:`ConnectionClosed`, when the connection terminated with an error. - A close code other than 1000 (OK) or 1001 (going away) was received or - sent, or the closing handshake didn't complete properly. + A close frame with a code other than 1000 (OK) or 1001 (going away) was + received or sent, or the closing handshake didn't complete properly. """ @@ -141,7 +146,8 @@ class ConnectionClosedOK(ConnectionClosed): """ Like :exc:`ConnectionClosed`, when the connection terminated properly. - A close code 1000 (OK) or 1001 (going away) was received and sent. + A close code with code 1000 (OK) or 1001 (going away) or without a code was + received and sent. """ @@ -171,7 +177,7 @@ class InvalidMessage(InvalidHandshake): class InvalidHeader(InvalidHandshake): """ - Raised when a HTTP header doesn't have a valid format or value. + Raised when an HTTP header doesn't have a valid format or value. """ @@ -190,7 +196,7 @@ class InvalidHeader(InvalidHandshake): class InvalidHeaderFormat(InvalidHeader): """ - Raised when a HTTP header cannot be parsed. + Raised when an HTTP header cannot be parsed. The format of the header doesn't match the grammar for that header. @@ -202,7 +208,7 @@ class InvalidHeaderFormat(InvalidHeader): class InvalidHeaderValue(InvalidHeader): """ - Raised when a HTTP header has a wrong value. + Raised when an HTTP header has a wrong value. The format of the header is correct but a value isn't acceptable. @@ -310,7 +316,7 @@ class InvalidParameterValue(NegotiationError): class AbortHandshake(InvalidHandshake): """ - Raised to abort the handshake on purpose and return a HTTP response. + Raised to abort the handshake on purpose and return an HTTP response. This exception is an implementation detail. @@ -325,11 +331,12 @@ class AbortHandshake(InvalidHandshake): def __init__( self, - status: http.HTTPStatus, + status: StatusLike, headers: datastructures.HeadersLike, body: bytes = b"", ) -> None: - self.status = status + # If a user passes an int instead of a HTTPStatus, fix it automatically. + self.status = http.HTTPStatus(status) self.headers = datastructures.Headers(headers) self.body = body @@ -369,7 +376,7 @@ class InvalidState(WebSocketException, AssertionError): class InvalidURI(WebSocketException): """ - Raised when connecting to an URI that isn't a valid WebSocket URI. + Raised when connecting to a URI that isn't a valid WebSocket URI. """ diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py index 0609676185..6c481a46cc 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py @@ -38,6 +38,7 @@ class Extension: PayloadTooBig: if decoding the payload exceeds ``max_size``. """ + raise NotImplementedError def encode(self, frame: frames.Frame) -> frames.Frame: """ @@ -50,6 +51,7 @@ class Extension: Frame: Encoded frame. """ + raise NotImplementedError class ClientExtensionFactory: @@ -69,6 +71,7 @@ class ClientExtensionFactory: List[ExtensionParameter]: Parameters to send to the server. """ + raise NotImplementedError def process_response_params( self, @@ -91,6 +94,7 @@ class ClientExtensionFactory: NegotiationError: if parameters aren't acceptable. """ + raise NotImplementedError class ServerExtensionFactory: @@ -126,3 +130,4 @@ class ServerExtensionFactory: the client aren't acceptable. """ + raise NotImplementedError diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py index e0de5e8f85..b391837c66 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py @@ -211,7 +211,6 @@ def _extract_parameters( client_max_window_bits: Optional[Union[int, bool]] = None for name, value in params: - if name == "server_no_context_takeover": if server_no_context_takeover: raise exceptions.DuplicateParameter(name) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py index 043b688b52..6b1befb2e0 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py @@ -13,7 +13,7 @@ from .typing import Data try: from .speedups import apply_mask -except ImportError: # pragma: no cover +except ImportError: from .utils import apply_mask @@ -52,45 +52,70 @@ DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG -# See https://www.iana.org/assignments/websocket/websocket.xhtml -CLOSE_CODES = { - 1000: "OK", - 1001: "going away", - 1002: "protocol error", - 1003: "unsupported type", +class CloseCode(enum.IntEnum): + """Close code values for WebSocket close frames.""" + + NORMAL_CLOSURE = 1000 + GOING_AWAY = 1001 + PROTOCOL_ERROR = 1002 + UNSUPPORTED_DATA = 1003 # 1004 is reserved - 1005: "no status code [internal]", - 1006: "connection closed abnormally [internal]", - 1007: "invalid data", - 1008: "policy violation", - 1009: "message too big", - 1010: "extension required", - 1011: "unexpected error", - 1012: "service restart", - 1013: "try again later", - 1014: "bad gateway", - 1015: "TLS failure [internal]", + NO_STATUS_RCVD = 1005 + ABNORMAL_CLOSURE = 1006 + INVALID_DATA = 1007 + POLICY_VIOLATION = 1008 + MESSAGE_TOO_BIG = 1009 + MANDATORY_EXTENSION = 1010 + INTERNAL_ERROR = 1011 + SERVICE_RESTART = 1012 + TRY_AGAIN_LATER = 1013 + BAD_GATEWAY = 1014 + TLS_HANDSHAKE = 1015 + + +# See https://www.iana.org/assignments/websocket/websocket.xhtml +CLOSE_CODE_EXPLANATIONS: dict[int, str] = { + CloseCode.NORMAL_CLOSURE: "OK", + CloseCode.GOING_AWAY: "going away", + CloseCode.PROTOCOL_ERROR: "protocol error", + CloseCode.UNSUPPORTED_DATA: "unsupported data", + CloseCode.NO_STATUS_RCVD: "no status received [internal]", + CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]", + CloseCode.INVALID_DATA: "invalid frame payload data", + CloseCode.POLICY_VIOLATION: "policy violation", + CloseCode.MESSAGE_TOO_BIG: "message too big", + CloseCode.MANDATORY_EXTENSION: "mandatory extension", + CloseCode.INTERNAL_ERROR: "internal error", + CloseCode.SERVICE_RESTART: "service restart", + CloseCode.TRY_AGAIN_LATER: "try again later", + CloseCode.BAD_GATEWAY: "bad gateway", + CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]", } # Close code that are allowed in a close frame. # Using a set optimizes `code in EXTERNAL_CLOSE_CODES`. EXTERNAL_CLOSE_CODES = { - 1000, - 1001, - 1002, - 1003, - 1007, - 1008, - 1009, - 1010, - 1011, - 1012, - 1013, - 1014, + CloseCode.NORMAL_CLOSURE, + CloseCode.GOING_AWAY, + CloseCode.PROTOCOL_ERROR, + CloseCode.UNSUPPORTED_DATA, + CloseCode.INVALID_DATA, + CloseCode.POLICY_VIOLATION, + CloseCode.MESSAGE_TOO_BIG, + CloseCode.MANDATORY_EXTENSION, + CloseCode.INTERNAL_ERROR, + CloseCode.SERVICE_RESTART, + CloseCode.TRY_AGAIN_LATER, + CloseCode.BAD_GATEWAY, } -OK_CLOSE_CODES = {1000, 1001} + +OK_CLOSE_CODES = { + CloseCode.NORMAL_CLOSURE, + CloseCode.GOING_AWAY, + CloseCode.NO_STATUS_RCVD, +} BytesLike = bytes, bytearray, memoryview @@ -123,7 +148,7 @@ class Frame: def __str__(self) -> str: """ - Return a human-readable represention of a frame. + Return a human-readable representation of a frame. """ coding = None @@ -191,6 +216,8 @@ class Frame: extensions: list of extensions, applied in reverse order. Raises: + EOFError: if the connection is closed without a full WebSocket frame. + UnicodeDecodeError: if the frame contains invalid UTF-8. PayloadTooBig: if the frame's payload size exceeds ``max_size``. ProtocolError: if the frame contains incorrect values. @@ -383,7 +410,7 @@ class Close: def __str__(self) -> str: """ - Return a human-readable represention of a close code and reason. + Return a human-readable representation of a close code and reason. """ if 3000 <= self.code < 4000: @@ -391,7 +418,7 @@ class Close: elif 4000 <= self.code < 5000: explanation = "private use" else: - explanation = CLOSE_CODES.get(self.code, "unknown") + explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown") result = f"{self.code} ({explanation})" if self.reason: @@ -419,7 +446,7 @@ class Close: close.check() return close elif len(data) == 0: - return cls(1005, "") + return cls(CloseCode.NO_STATUS_RCVD, "") else: raise exceptions.ProtocolError("close frame too short") diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py index b14fa94bdc..9f86f6a1ff 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +import typing from .imports import lazy_import from .version import version as websockets_version @@ -9,18 +10,22 @@ from .version import version as websockets_version # For backwards compatibility: -lazy_import( - globals(), - # Headers and MultipleValuesError used to be defined in this module. - aliases={ - "Headers": ".datastructures", - "MultipleValuesError": ".datastructures", - }, - deprecated_aliases={ - "read_request": ".legacy.http", - "read_response": ".legacy.http", - }, -) +# When type checking, import non-deprecated aliases eagerly. Else, import on demand. +if typing.TYPE_CHECKING: + from .datastructures import Headers, MultipleValuesError # noqa: F401 +else: + lazy_import( + globals(), + # Headers and MultipleValuesError used to be defined in this module. + aliases={ + "Headers": ".datastructures", + "MultipleValuesError": ".datastructures", + }, + deprecated_aliases={ + "read_request": ".legacy.http", + "read_response": ".legacy.http", + }, + ) __all__ = ["USER_AGENT"] diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py index 84048fa47b..ec4e3b8b7d 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py @@ -8,14 +8,12 @@ from typing import Callable, Generator, Optional from . import datastructures, exceptions -# Maximum total size of headers is around 256 * 4 KiB = 1 MiB -MAX_HEADERS = 256 +# Maximum total size of headers is around 128 * 8 KiB = 1 MiB. +MAX_HEADERS = 128 -# We can use the same limit for the request line and header lines: -# "GET <4096 bytes> HTTP/1.1\r\n" = 4111 bytes -# "Set-Cookie: <4097 bytes>\r\n" = 4111 bytes -# (RFC requires 4096 bytes; for some reason Firefox supports 4097 bytes.) -MAX_LINE = 4111 +# Limit request line and header lines. 8KiB is the most common default +# configuration of popular HTTP servers. +MAX_LINE = 8192 # Support for HTTP response bodies is intended to read an error message # returned by a server. It isn't designed to perform large file transfers. @@ -70,7 +68,7 @@ class Request: def exception(self) -> Optional[Exception]: # pragma: no cover warnings.warn( "Request.exception is deprecated; " - "use ServerConnection.handshake_exc instead", + "use ServerProtocol.handshake_exc instead", DeprecationWarning, ) return self._exception @@ -174,7 +172,7 @@ class Response: def exception(self) -> Optional[Exception]: # pragma: no cover warnings.warn( "Response.exception is deprecated; " - "use ClientConnection.handshake_exc instead", + "use ClientProtocol.handshake_exc instead", DeprecationWarning, ) return self._exception diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py new file mode 100644 index 0000000000..8264094f5b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py @@ -0,0 +1,265 @@ +# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py +# Licensed under the Apache License (Apache-2.0) + +import asyncio +import enum +import sys +import warnings +from types import TracebackType +from typing import Optional, Type + + +# From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py +# Licensed under the Python Software Foundation License (PSF-2.0) + +if sys.version_info >= (3, 11): + from typing import final +else: + # @final exists in 3.8+, but we backport it for all versions + # before 3.11 to keep support for the __final__ attribute. + # See https://bugs.python.org/issue46342 + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... + @final + class Leaf: + ... + class Other(Leaf): # Error reported by type checker + ... + + There is no runtime checking of these properties. The decorator + sets the ``__final__`` attribute to ``True`` on the decorated object + to allow runtime introspection. + """ + try: + f.__final__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return f + + +# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py + +__version__ = "4.0.2" + + +__all__ = ("timeout", "timeout_at", "Timeout") + + +def timeout(delay: Optional[float]) -> "Timeout": + """timeout context manager. + + Useful in cases when you want to apply timeout logic around block + of code or in cases when asyncio.wait_for is not suitable. For example: + + >>> async with timeout(0.001): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + delay - value in seconds or None to disable timeout logic + """ + loop = asyncio.get_running_loop() + if delay is not None: + deadline = loop.time() + delay # type: Optional[float] + else: + deadline = None + return Timeout(deadline, loop) + + +def timeout_at(deadline: Optional[float]) -> "Timeout": + """Schedule the timeout at absolute time. + + deadline argument points on the time in the same clock system + as loop.time(). + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + + >>> async with timeout_at(loop.time() + 10): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + """ + loop = asyncio.get_running_loop() + return Timeout(deadline, loop) + + +class _State(enum.Enum): + INIT = "INIT" + ENTER = "ENTER" + TIMEOUT = "TIMEOUT" + EXIT = "EXIT" + + +@final +class Timeout: + # Internal class, please don't instantiate it directly + # Use timeout() and timeout_at() public factories instead. + # + # Implementation note: `async with timeout()` is preferred + # over `with timeout()`. + # While technically the Timeout class implementation + # doesn't need to be async at all, + # the `async with` statement explicitly points that + # the context manager should be used from async function context. + # + # This design allows to avoid many silly misusages. + # + # TimeoutError is raised immediately when scheduled + # if the deadline is passed. + # The purpose is to time out as soon as possible + # without waiting for the next await expression. + + __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler") + + def __init__( + self, deadline: Optional[float], loop: asyncio.AbstractEventLoop + ) -> None: + self._loop = loop + self._state = _State.INIT + + self._timeout_handler = None # type: Optional[asyncio.Handle] + if deadline is None: + self._deadline = None # type: Optional[float] + else: + self.update(deadline) + + def __enter__(self) -> "Timeout": + warnings.warn( + "with timeout() is deprecated, use async with timeout() instead", + DeprecationWarning, + stacklevel=2, + ) + self._do_enter() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + async def __aenter__(self) -> "Timeout": + self._do_enter() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + @property + def expired(self) -> bool: + """Is timeout expired during execution?""" + return self._state == _State.TIMEOUT + + @property + def deadline(self) -> Optional[float]: + return self._deadline + + def reject(self) -> None: + """Reject scheduled timeout if any.""" + # cancel is maybe better name but + # task.cancel() raises CancelledError in asyncio world. + if self._state not in (_State.INIT, _State.ENTER): + raise RuntimeError(f"invalid state {self._state.value}") + self._reject() + + def _reject(self) -> None: + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._timeout_handler = None + + def shift(self, delay: float) -> None: + """Advance timeout on delay seconds. + + The delay can be negative. + + Raise RuntimeError if shift is called when deadline is not scheduled + """ + deadline = self._deadline + if deadline is None: + raise RuntimeError("cannot shift timeout if deadline is not scheduled") + self.update(deadline + delay) + + def update(self, deadline: float) -> None: + """Set deadline to absolute value. + + deadline argument points on the time in the same clock system + as loop.time(). + + If new deadline is in the past the timeout is raised immediately. + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + """ + if self._state == _State.EXIT: + raise RuntimeError("cannot reschedule after exit from context manager") + if self._state == _State.TIMEOUT: + raise RuntimeError("cannot reschedule expired timeout") + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._deadline = deadline + if self._state != _State.INIT: + self._reschedule() + + def _reschedule(self) -> None: + assert self._state == _State.ENTER + deadline = self._deadline + if deadline is None: + return + + now = self._loop.time() + if self._timeout_handler is not None: + self._timeout_handler.cancel() + + task = asyncio.current_task() + if deadline <= now: + self._timeout_handler = self._loop.call_soon(self._on_timeout, task) + else: + self._timeout_handler = self._loop.call_at(deadline, self._on_timeout, task) + + def _do_enter(self) -> None: + if self._state != _State.INIT: + raise RuntimeError(f"invalid state {self._state.value}") + self._state = _State.ENTER + self._reschedule() + + def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: + if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: + self._timeout_handler = None + raise asyncio.TimeoutError + # timeout has not expired + self._state = _State.EXIT + self._reject() + return None + + def _on_timeout(self, task: "asyncio.Task[None]") -> None: + task.cancel() + self._state = _State.TIMEOUT + # drop the reference early + self._timeout_handler = None + + +# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py index 8825c14ecf..d3425836e1 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py @@ -67,7 +67,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): Returns: bool: :obj:`True` if the handshake should continue; - :obj:`False` if it should fail with a HTTP 401 error. + :obj:`False` if it should fail with an HTTP 401 error. """ if self._check_credentials is not None: @@ -81,7 +81,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): request_headers: Headers, ) -> Optional[HTTPResponse]: """ - Check HTTP Basic Auth and return a HTTP 401 response if needed. + Check HTTP Basic Auth and return an HTTP 401 response if needed. """ try: @@ -118,8 +118,8 @@ def basic_auth_protocol_factory( realm: Optional[str] = None, credentials: Optional[Union[Credentials, Iterable[Credentials]]] = None, check_credentials: Optional[Callable[[str, str], Awaitable[bool]]] = None, - create_protocol: Optional[Callable[[Any], BasicAuthWebSocketServerProtocol]] = None, -) -> Callable[[Any], BasicAuthWebSocketServerProtocol]: + create_protocol: Optional[Callable[..., BasicAuthWebSocketServerProtocol]] = None, +) -> Callable[..., BasicAuthWebSocketServerProtocol]: """ Protocol factory that enforces HTTP Basic Auth. @@ -135,20 +135,20 @@ def basic_auth_protocol_factory( ) Args: - realm: indicates the scope of protection. It should contain only ASCII - characters because the encoding of non-ASCII characters is - undefined. Refer to section 2.2 of :rfc:`7235` for details. - credentials: defines hard coded authorized credentials. It can be a + realm: Scope of protection. It should contain only ASCII characters + because the encoding of non-ASCII characters is undefined. + Refer to section 2.2 of :rfc:`7235` for details. + credentials: Hard coded authorized credentials. It can be a ``(username, password)`` pair or a list of such pairs. - check_credentials: defines a coroutine that verifies credentials. - This coroutine receives ``username`` and ``password`` arguments + check_credentials: Coroutine that verifies credentials. + It receives ``username`` and ``password`` arguments and returns a :class:`bool`. One of ``credentials`` or ``check_credentials`` must be provided but not both. - create_protocol: factory that creates the protocol. By default, this + create_protocol: Factory that creates the protocol. By default, this is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced by a subclass. Raises: - TypeError: if the ``credentials`` or ``check_credentials`` argument is + TypeError: If the ``credentials`` or ``check_credentials`` argument is wrong. """ @@ -175,11 +175,7 @@ def basic_auth_protocol_factory( return hmac.compare_digest(expected_password, password) if create_protocol is None: - # Not sure why mypy cannot figure this out. - create_protocol = cast( - Callable[[Any], BasicAuthWebSocketServerProtocol], - BasicAuthWebSocketServerProtocol, - ) + create_protocol = BasicAuthWebSocketServerProtocol return functools.partial( create_protocol, diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py index fadc3efe87..48622523ee 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py @@ -44,6 +44,7 @@ from ..headers import ( from ..http import USER_AGENT from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol from ..uri import WebSocketURI, parse_uri +from .compatibility import asyncio_timeout from .handshake import build_request, check_response from .http import read_response from .protocol import WebSocketCommonProtocol @@ -65,12 +66,13 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): await process(message) The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away). It raises + 1000 (OK) or 1001 (going away) or without a close code. It raises a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is closed with any other code. See :func:`connect` for the documentation of ``logger``, ``origin``, - ``extensions``, ``subprotocols``, and ``extra_headers``. + ``extensions``, ``subprotocols``, ``extra_headers``, and + ``user_agent_header``. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -89,6 +91,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): extensions: Optional[Sequence[ClientExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, **kwargs: Any, ) -> None: if logger is None: @@ -98,6 +101,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): self.available_extensions = extensions self.available_subprotocols = subprotocols self.extra_headers = extra_headers + self.user_agent_header = user_agent_header def write_http_request(self, path: str, headers: Headers) -> None: """ @@ -127,16 +131,12 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): after this coroutine returns. Raises: - InvalidMessage: if the HTTP message is malformed or isn't an + InvalidMessage: If the HTTP message is malformed or isn't an HTTP/1.1 GET response. """ try: status_code, reason, headers = await read_response(self.reader) - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: # pragma: no cover - raise except Exception as exc: raise InvalidMessage("did not receive a valid HTTP response") from exc @@ -185,7 +185,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Extensions") if header_values: - if available_extensions is None: raise InvalidHandshake("no extensions supported") @@ -194,9 +193,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): ) for name, response_params in parsed_header_values: - for extension_factory in available_extensions: - # Skip non-matching extensions based on their name. if extension_factory.name != name: continue @@ -242,7 +239,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Protocol") if header_values: - if available_subprotocols is None: raise InvalidHandshake("no subprotocols supported") @@ -274,15 +270,15 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): Args: wsuri: URI of the WebSocket server. - origin: value of the ``Origin`` header. - available_extensions: list of supported extensions, in order in - which they should be tried. - available_subprotocols: list of supported subprotocols, in order - of decreasing preference. - extra_headers: arbitrary HTTP headers to add to the request. + origin: Value of the ``Origin`` header. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + extra_headers: Arbitrary HTTP headers to add to the handshake request. Raises: - InvalidHandshake: if the handshake fails. + InvalidHandshake: If the handshake fails. """ request_headers = Headers() @@ -315,7 +311,8 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): if self.extra_headers is not None: request_headers.update(self.extra_headers) - request_headers.setdefault("User-Agent", USER_AGENT) + if self.user_agent_header is not None: + request_headers.setdefault("User-Agent", self.user_agent_header) self.write_http_request(wsuri.resource_name, request_headers) @@ -376,25 +373,26 @@ class Connect: Args: uri: URI of the WebSocket server. - create_protocol: factory for the :class:`asyncio.Protocol` managing - the connection; defaults to :class:`WebSocketClientProtocol`; may - be set to a wrapper or a subclass to customize connection handling. - logger: logger for this connection; - defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../topics/logging>` for details. - compression: shortcut that enables the "permessage-deflate" extension - by default; may be set to :obj:`None` to disable compression; - see the :doc:`compression guide <../topics/compression>` for details. - origin: value of the ``Origin`` header. This is useful when connecting - to a server that validates the ``Origin`` header to defend against - Cross-Site WebSocket Hijacking attacks. - extensions: list of supported extensions, in order in which they - should be tried. - subprotocols: list of supported subprotocols, in order of decreasing + create_protocol: Factory for the :class:`asyncio.Protocol` managing + the connection. It defaults to :class:`WebSocketClientProtocol`. + Set it to a wrapper or a subclass to customize connection handling. + logger: Logger for this client. + It defaults to ``logging.getLogger("websockets.client")``. + See the :doc:`logging guide <../../topics/logging>` for details. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + origin: Value of the ``Origin`` header, for servers that require it. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing preference. - extra_headers: arbitrary HTTP headers to add to the request. - open_timeout: timeout for opening the connection in seconds; - :obj:`None` to disable the timeout + extra_headers: Arbitrary HTTP headers to add to the handshake request. + user_agent_header: Value of the ``User-Agent`` request header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. + open_timeout: Timeout for opening the connection in seconds. + :obj:`None` disables the timeout. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -415,13 +413,11 @@ class Connect: the TCP connection. The host name from ``uri`` is still used in the TLS handshake for secure connections and in the ``Host`` header. - Returns: - WebSocketClientProtocol: WebSocket connection. - Raises: - InvalidURI: if ``uri`` isn't a valid WebSocket URI. - InvalidHandshake: if the opening handshake fails. - ~asyncio.TimeoutError: if the opening handshake times out. + InvalidURI: If ``uri`` isn't a valid WebSocket URI. + OSError: If the TCP connection fails. + InvalidHandshake: If the opening handshake fails. + ~asyncio.TimeoutError: If the opening handshake times out. """ @@ -431,13 +427,14 @@ class Connect: self, uri: str, *, - create_protocol: Optional[Callable[[Any], WebSocketClientProtocol]] = None, + create_protocol: Optional[Callable[..., WebSocketClientProtocol]] = None, logger: Optional[LoggerLike] = None, compression: Optional[str] = "deflate", origin: Optional[Origin] = None, extensions: Optional[Sequence[ClientExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, open_timeout: Optional[float] = 10, ping_interval: Optional[float] = 20, ping_timeout: Optional[float] = 20, @@ -503,6 +500,7 @@ class Connect: extensions=extensions, subprotocols=subprotocols, extra_headers=extra_headers, + user_agent_header=user_agent_header, ping_interval=ping_interval, ping_timeout=ping_timeout, close_timeout=close_timeout, @@ -530,6 +528,8 @@ class Connect: else: # If sock is given, host and port shouldn't be specified. host, port = None, None + if kwargs.get("ssl"): + kwargs.setdefault("server_hostname", wsuri.host) # If host and port are given, override values from the URI. host = kwargs.pop("host", host) port = kwargs.pop("port", port) @@ -597,10 +597,6 @@ class Connect: try: async with self as protocol: yield protocol - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: # pragma: no cover - raise except Exception: # Add a random initial delay between 0 and 5 seconds. # See 7.2.3. Recovering from Abnormal Closure in RFC 6544. @@ -647,13 +643,13 @@ class Connect: return self.__await_impl_timeout__().__await__() async def __await_impl_timeout__(self) -> WebSocketClientProtocol: - return await asyncio.wait_for(self.__await_impl__(), self.open_timeout) + async with asyncio_timeout(self.open_timeout): + return await self.__await_impl__() async def __await_impl__(self) -> WebSocketClientProtocol: for redirects in range(self.MAX_REDIRECTS_ALLOWED): - transport, protocol = await self._create_connection() - protocol = cast(WebSocketClientProtocol, protocol) - + _transport, _protocol = await self._create_connection() + protocol = cast(WebSocketClientProtocol, _protocol) try: await protocol.handshake( self._wsuri, @@ -701,7 +697,7 @@ def unix_connect( It's mainly useful for debugging servers listening on Unix sockets. Args: - path: file system path to the Unix socket. + path: File system path to the Unix socket. uri: URI of the WebSocket server; the host is used in the TLS handshake for secure connections and in the ``Host`` header. diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py index df81de9dbc..6bd01e70de 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py @@ -1,13 +1,12 @@ from __future__ import annotations -import asyncio import sys -from typing import Any, Dict -def loop_if_py_lt_38(loop: asyncio.AbstractEventLoop) -> Dict[str, Any]: - """ - Helper for the removal of the loop argument in Python 3.10. +__all__ = ["asyncio_timeout"] - """ - return {"loop": loop} if sys.version_info[:2] < (3, 8) else {} + +if sys.version_info[:2] >= (3, 11): + from asyncio import timeout as asyncio_timeout # noqa: F401 +else: + from .async_timeout import timeout as asyncio_timeout # noqa: F401 diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py index c4de7eb28b..b77b869e3f 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import struct from typing import Any, Awaitable, Callable, NamedTuple, Optional, Sequence, Tuple @@ -10,12 +9,11 @@ from ..exceptions import PayloadTooBig, ProtocolError try: from ..speedups import apply_mask -except ImportError: # pragma: no cover +except ImportError: from ..utils import apply_mask class Frame(NamedTuple): - fin: bool opcode: frames.Opcode data: bytes @@ -53,16 +51,16 @@ class Frame(NamedTuple): Read a WebSocket frame. Args: - reader: coroutine that reads exactly the requested number of + reader: Coroutine that reads exactly the requested number of bytes, unless the end of file is reached. - mask: whether the frame should be masked i.e. whether the read + mask: Whether the frame should be masked i.e. whether the read happens on the server side. - max_size: maximum payload size in bytes. - extensions: list of extensions, applied in reverse order. + max_size: Maximum payload size in bytes. + extensions: List of extensions, applied in reverse order. Raises: - PayloadTooBig: if the frame exceeds ``max_size``. - ProtocolError: if the frame contains incorrect values. + PayloadTooBig: If the frame exceeds ``max_size``. + ProtocolError: If the frame contains incorrect values. """ @@ -130,14 +128,14 @@ class Frame(NamedTuple): Write a WebSocket frame. Args: - frame: frame to write. - write: function that writes bytes. - mask: whether the frame should be masked i.e. whether the write + frame: Frame to write. + write: Function that writes bytes. + mask: Whether the frame should be masked i.e. whether the write happens on the client side. - extensions: list of extensions, applied in order. + extensions: List of extensions, applied in order. Raises: - ProtocolError: if the frame contains incorrect values. + ProtocolError: If the frame contains incorrect values. """ # The frame is written in a single call to write in order to prevent @@ -147,8 +145,11 @@ class Frame(NamedTuple): # Backwards compatibility with previously documented public APIs - -from ..frames import Close, prepare_ctrl as encode_data, prepare_data # noqa +from ..frames import ( # noqa: E402, F401, I001 + Close, + prepare_ctrl as encode_data, + prepare_data, +) def parse_close(data: bytes) -> Tuple[int, str]: @@ -156,14 +157,15 @@ def parse_close(data: bytes) -> Tuple[int, str]: Parse the payload from a close frame. Returns: - Tuple[int, str]: close code and reason. + Close code and reason. Raises: - ProtocolError: if data is ill-formed. - UnicodeDecodeError: if the reason isn't valid UTF-8. + ProtocolError: If data is ill-formed. + UnicodeDecodeError: If the reason isn't valid UTF-8. """ - return dataclasses.astuple(Close.parse(data)) # type: ignore + close = Close.parse(data) + return close.code, close.reason def serialize_close(code: int, reason: str) -> bytes: diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py index 569937bb9a..ad8faf0404 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py @@ -21,7 +21,7 @@ def build_request(headers: Headers) -> str: Update request headers passed in argument. Args: - headers: handshake request headers. + headers: Handshake request headers. Returns: str: ``key`` that must be passed to :func:`check_response`. @@ -45,14 +45,14 @@ def check_request(headers: Headers) -> str: the responsibility of the caller. Args: - headers: handshake request headers. + headers: Handshake request headers. Returns: str: ``key`` that must be passed to :func:`build_response`. Raises: - InvalidHandshake: if the handshake request is invalid; - then the server must return 400 Bad Request error. + InvalidHandshake: If the handshake request is invalid. + Then, the server must return a 400 Bad Request error. """ connection: List[ConnectionOption] = sum( @@ -110,8 +110,8 @@ def build_response(headers: Headers, key: str) -> None: Update response headers passed in argument. Args: - headers: handshake response headers. - key: returned by :func:`check_request`. + headers: Handshake response headers. + key: Returned by :func:`check_request`. """ headers["Upgrade"] = "websocket" @@ -128,11 +128,11 @@ def check_response(headers: Headers, key: str) -> None: the caller. Args: - headers: handshake response headers. - key: returned by :func:`build_request`. + headers: Handshake response headers. + key: Returned by :func:`build_request`. Raises: - InvalidHandshake: if the handshake response is invalid. + InvalidHandshake: If the handshake response is invalid. """ connection: List[ConnectionOption] = sum( diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py index cc2ef1f067..2ac7f7092d 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py @@ -10,8 +10,8 @@ from ..exceptions import SecurityError __all__ = ["read_request", "read_response"] -MAX_HEADERS = 256 -MAX_LINE = 4110 +MAX_HEADERS = 128 +MAX_LINE = 8192 def d(value: bytes) -> str: @@ -56,12 +56,12 @@ async def read_request(stream: asyncio.StreamReader) -> Tuple[str, Headers]: body, it may be read from ``stream`` after this coroutine returns. Args: - stream: input to read the request from + stream: Input to read the request from. Raises: - EOFError: if the connection is closed without a full HTTP request - SecurityError: if the request exceeds a security limit - ValueError: if the request isn't well formatted + EOFError: If the connection is closed without a full HTTP request. + SecurityError: If the request exceeds a security limit. + ValueError: If the request isn't well formatted. """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1 @@ -103,12 +103,12 @@ async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, Headers body, it may be read from ``stream`` after this coroutine returns. Args: - stream: input to read the response from + stream: Input to read the response from. Raises: - EOFError: if the connection is closed without a full HTTP response - SecurityError: if the response exceeds a security limit - ValueError: if the response isn't well formatted + EOFError: If the connection is closed without a full HTTP response. + SecurityError: If the response exceeds a security limit. + ValueError: If the response isn't well formatted. """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2 @@ -192,7 +192,7 @@ async def read_line(stream: asyncio.StreamReader) -> bytes: """ # Security: this is bounded by the StreamReader's limit (default = 32 KiB). line = await stream.readline() - # Security: this guarantees header values are small (hard-coded = 4 KiB) + # Security: this guarantees header values are small (hard-coded = 8 KiB) if len(line) > MAX_LINE: raise SecurityError("line too long") # Not mandatory but safe - https://www.rfc-editor.org/rfc/rfc7230.html#section-3.5 diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py index 3f734fe760..19cee0e652 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py @@ -7,6 +7,8 @@ import logging import random import ssl import struct +import sys +import time import uuid import warnings from typing import ( @@ -14,17 +16,18 @@ from typing import ( AsyncIterable, AsyncIterator, Awaitable, + Callable, Deque, Dict, Iterable, List, Mapping, Optional, + Tuple, Union, cast, ) -from ..connection import State from ..datastructures import Headers from ..exceptions import ( ConnectionClosed, @@ -44,12 +47,14 @@ from ..frames import ( OP_PONG, OP_TEXT, Close, + CloseCode, Opcode, prepare_ctrl, prepare_data, ) +from ..protocol import State from ..typing import Data, LoggerLike, Subprotocol -from .compatibility import loop_if_py_lt_38 +from .compatibility import asyncio_timeout from .framing import Frame @@ -76,38 +81,38 @@ class WebSocketCommonProtocol(asyncio.Protocol): simplicity. Once the connection is open, a Ping_ frame is sent every ``ping_interval`` - seconds. This serves as a keepalive. It helps keeping the connection - open, especially in the presence of proxies with short timeouts on - inactive connections. Set ``ping_interval`` to :obj:`None` to disable - this behavior. + seconds. This serves as a keepalive. It helps keeping the connection open, + especially in the presence of proxies with short timeouts on inactive + connections. Set ``ping_interval`` to :obj:`None` to disable this behavior. .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 If the corresponding Pong_ frame isn't received within ``ping_timeout`` - seconds, the connection is considered unusable and is closed with code - 1011. This ensures that the remote endpoint remains responsive. Set + seconds, the connection is considered unusable and is closed with code 1011. + This ensures that the remote endpoint remains responsive. Set ``ping_timeout`` to :obj:`None` to disable this behavior. .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + See the discussion of :doc:`timeouts <../../topics/timeouts>` for details. + The ``close_timeout`` parameter defines a maximum wait time for completing the closing handshake and terminating the TCP connection. For legacy reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds for clients and ``4 * close_timeout`` for servers. - See the discussion of :doc:`timeouts <../topics/timeouts>` for details. - - ``close_timeout`` needs to be a parameter of the protocol because - websockets usually calls :meth:`close` implicitly upon exit: + ``close_timeout`` is a parameter of the protocol because websockets usually + calls :meth:`close` implicitly upon exit: - * on the client side, when :func:`~websockets.client.connect` is used as a + * on the client side, when using :func:`~websockets.client.connect` as a context manager; - * on the server side, when the connection handler terminates; + * on the server side, when the connection handler terminates. - To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`. + To apply a timeout to any other API, wrap it in :func:`~asyncio.timeout` or + :func:`~asyncio.wait_for`. The ``max_size`` parameter enforces the maximum size for incoming messages - in bytes. The default value is 1 MiB. If a larger message is received, + in bytes. The default value is 1 MiB. If a larger message is received, :meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError` and the connection will be closed with code 1009. @@ -124,38 +129,38 @@ class WebSocketCommonProtocol(asyncio.Protocol): Since Python can use up to 4 bytes of memory to represent a single character, each connection may use up to ``4 * max_size * max_queue`` - bytes of memory to store incoming messages. By default, this is 128 MiB. + bytes of memory to store incoming messages. By default, this is 128 MiB. You may want to lower the limits, depending on your application's requirements. The ``read_limit`` argument sets the high-water limit of the buffer for incoming bytes. The low-water limit is half the high-water limit. The - default value is 64 KiB, half of asyncio's default (based on the current + default value is 64 KiB, half of asyncio's default (based on the current implementation of :class:`~asyncio.StreamReader`). The ``write_limit`` argument sets the high-water limit of the buffer for outgoing bytes. The low-water limit is a quarter of the high-water limit. - The default value is 64 KiB, equal to asyncio's default (based on the + The default value is 64 KiB, equal to asyncio's default (based on the current implementation of ``FlowControlMixin``). - See the discussion of :doc:`memory usage <../topics/memory>` for details. + See the discussion of :doc:`memory usage <../../topics/memory>` for details. Args: - logger: logger for this connection; - defaults to ``logging.getLogger("websockets.protocol")``; - see the :doc:`logging guide <../topics/logging>` for details. - ping_interval: delay between keepalive pings in seconds; - :obj:`None` to disable keepalive pings. - ping_timeout: timeout for keepalive pings in seconds; - :obj:`None` to disable timeouts. - close_timeout: timeout for closing the connection in seconds; - for legacy reasons, the actual timeout is 4 or 5 times larger. - max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. - max_queue: maximum number of incoming messages in receive buffer; - :obj:`None` to disable the limit. - read_limit: high-water mark of read buffer in bytes. - write_limit: high-water mark of write buffer in bytes. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.protocol")``. + See the :doc:`logging guide <../../topics/logging>` for details. + ping_interval: Delay between keepalive pings in seconds. + :obj:`None` disables keepalive pings. + ping_timeout: Timeout for keepalive pings in seconds. + :obj:`None` disables timeouts. + close_timeout: Timeout for closing the connection in seconds. + For legacy reasons, the actual timeout is 4 or 5 times larger. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + max_queue: Maximum number of incoming messages in receive buffer. + :obj:`None` disables the limit. + read_limit: High-water mark of read buffer in bytes. + write_limit: High-water mark of write buffer in bytes. """ @@ -217,8 +222,6 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Logger or LoggerAdapter for this connection. if logger is None: logger = logging.getLogger("websockets.protocol") - # https://github.com/python/typeshed/issues/5561 - logger = cast(logging.Logger, logger) self.logger: LoggerLike = logging.LoggerAdapter(logger, {"websocket": self}) """Logger for this connection.""" @@ -242,7 +245,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): self._paused = False self._drain_waiter: Optional[asyncio.Future[None]] = None - self._drain_lock = asyncio.Lock(**loop_if_py_lt_38(loop)) + self._drain_lock = asyncio.Lock() # This class implements the data transfer and closing handshake, which # are shared between the client-side and the server-side. @@ -285,7 +288,19 @@ class WebSocketCommonProtocol(asyncio.Protocol): self._fragmented_message_waiter: Optional[asyncio.Future[None]] = None # Mapping of ping IDs to pong waiters, in chronological order. - self.pings: Dict[bytes, asyncio.Future[None]] = {} + self.pings: Dict[bytes, Tuple[asyncio.Future[float], float]] = {} + + self.latency: float = 0 + """ + Latency of the connection, in seconds. + + This value is updated after sending a ping frame and receiving a + matching pong frame. Before the first ping, :attr:`latency` is ``0``. + + By default, websockets enables a :ref:`keepalive <keepalive>` mechanism + that sends ping frames automatically at regular intervals. You can also + send ping frames and measure latency with :meth:`ping`. + """ # Task running the data transfer. self.transfer_data_task: asyncio.Task[None] @@ -325,7 +340,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): # write(...); yield from drain() # in a loop would never call connection_lost(), so it # would not see an error when the socket is closed. - await asyncio.sleep(0, **loop_if_py_lt_38(self.loop)) + await asyncio.sleep(0) await self._drain_helper() def connection_open(self) -> None: @@ -445,7 +460,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): if self.state is not State.CLOSED: return None elif self.close_rcvd is None: - return 1006 + return CloseCode.ABNORMAL_CLOSURE else: return self.close_rcvd.code @@ -471,10 +486,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): """ Iterate on incoming messages. - The iterator exits normally when the connection is closed with the - close code 1000 (OK) or 1001(going away). It raises - a :exc:`~websockets.exceptions.ConnectionClosedError` exception when - the connection is closed with any other code. + The iterator exits normally when the connection is closed with the close + code 1000 (OK) or 1001 (going away) or without a close code. + + It raises a :exc:`~websockets.exceptions.ConnectionClosedError` + exception when the connection is closed with any other code. """ try: @@ -488,8 +504,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): Receive the next message. When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises + :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal connection closure and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol error or a network failure. This is how you detect the end of the @@ -498,8 +514,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): Canceling :meth:`recv` is safe. There's no risk of losing the next message. The next invocation of :meth:`recv` will return it. - This makes it possible to enforce a timeout by wrapping :meth:`recv` - in :func:`~asyncio.wait_for`. + This makes it possible to enforce a timeout by wrapping :meth:`recv` in + :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`. Returns: Data: A string (:class:`str`) for a Text_ frame. A bytestring @@ -509,8 +525,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 Raises: - ConnectionClosed: when the connection is closed. - RuntimeError: if two coroutines call :meth:`recv` concurrently. + ConnectionClosed: When the connection is closed. + RuntimeError: If two coroutines call :meth:`recv` concurrently. """ if self._pop_message_waiter is not None: @@ -536,7 +552,6 @@ class WebSocketCommonProtocol(asyncio.Protocol): await asyncio.wait( [pop_message_waiter, self.transfer_data_task], return_when=asyncio.FIRST_COMPLETED, - **loop_if_py_lt_38(self.loop), ) finally: self._pop_message_waiter = None @@ -613,8 +628,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): to send. Raises: - ConnectionClosed: when the connection is closed. - TypeError: if ``message`` doesn't have a supported type. + ConnectionClosed: When the connection is closed. + TypeError: If ``message`` doesn't have a supported type. """ await self.ensure_open() @@ -639,16 +654,15 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Fragmented message -- regular iterator. elif isinstance(message, Iterable): - # Work around https://github.com/python/mypy/issues/6227 message = cast(Iterable[Data], message) iter_message = iter(message) try: - message_chunk = next(iter_message) + fragment = next(iter_message) except StopIteration: return - opcode, data = prepare_data(message_chunk) + opcode, data = prepare_data(fragment) self._fragmented_message_waiter = asyncio.Future() try: @@ -656,8 +670,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): await self.write_frame(False, opcode, data) # Other fragments. - for message_chunk in iter_message: - confirm_opcode, data = prepare_data(message_chunk) + for fragment in iter_message: + confirm_opcode, data = prepare_data(fragment) if confirm_opcode != opcode: raise TypeError("data contains inconsistent types") await self.write_frame(False, OP_CONT, data) @@ -668,7 +682,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): except (Exception, asyncio.CancelledError): # We're half-way through a fragmented message and we can't # complete it. This makes the connection unusable. - self.fail_connection(1011) + self.fail_connection(CloseCode.INTERNAL_ERROR) raise finally: @@ -678,18 +692,22 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Fragmented message -- asynchronous iterator elif isinstance(message, AsyncIterable): - # aiter_message = aiter(message) without aiter - # https://github.com/python/mypy/issues/5738 - aiter_message = type(message).__aiter__(message) # type: ignore + # Implement aiter_message = aiter(message) without aiter + # Work around https://github.com/python/mypy/issues/5738 + aiter_message = cast( + Callable[[AsyncIterable[Data]], AsyncIterator[Data]], + type(message).__aiter__, + )(message) try: - # message_chunk = anext(aiter_message) without anext - # https://github.com/python/mypy/issues/5738 - message_chunk = await type(aiter_message).__anext__( # type: ignore - aiter_message - ) + # Implement fragment = anext(aiter_message) without anext + # Work around https://github.com/python/mypy/issues/5738 + fragment = await cast( + Callable[[AsyncIterator[Data]], Awaitable[Data]], + type(aiter_message).__anext__, + )(aiter_message) except StopAsyncIteration: return - opcode, data = prepare_data(message_chunk) + opcode, data = prepare_data(fragment) self._fragmented_message_waiter = asyncio.Future() try: @@ -697,11 +715,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): await self.write_frame(False, opcode, data) # Other fragments. - # https://github.com/python/mypy/issues/5738 - # coverage reports this code as not covered, but it is - # exercised by tests - changing it breaks the tests! - async for message_chunk in aiter_message: # type: ignore # pragma: no cover # noqa - confirm_opcode, data = prepare_data(message_chunk) + async for fragment in aiter_message: + confirm_opcode, data = prepare_data(fragment) if confirm_opcode != opcode: raise TypeError("data contains inconsistent types") await self.write_frame(False, OP_CONT, data) @@ -712,7 +727,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): except (Exception, asyncio.CancelledError): # We're half-way through a fragmented message and we can't # complete it. This makes the connection unusable. - self.fail_connection(1011) + self.fail_connection(CloseCode.INTERNAL_ERROR) raise finally: @@ -722,7 +737,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): else: raise TypeError("data must be str, bytes-like, or iterable") - async def close(self, code: int = 1000, reason: str = "") -> None: + async def close( + self, + code: int = CloseCode.NORMAL_CLOSURE, + reason: str = "", + ) -> None: """ Perform the closing handshake. @@ -747,19 +766,16 @@ class WebSocketCommonProtocol(asyncio.Protocol): """ try: - await asyncio.wait_for( - self.write_close_frame(Close(code, reason)), - self.close_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.close_timeout): + await self.write_close_frame(Close(code, reason)) except asyncio.TimeoutError: # If the close frame cannot be sent because the send buffers # are full, the closing handshake won't complete anyway. # Fail the connection to shut down faster. self.fail_connection() - # If no close frame is received within the timeout, wait_for() cancels - # the data transfer task and raises TimeoutError. + # If no close frame is received within the timeout, asyncio_timeout() + # cancels the data transfer task and raises TimeoutError. # If close() is called multiple times concurrently and one of these # calls hits the timeout, the data transfer task will be canceled. @@ -768,11 +784,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): try: # If close() is canceled during the wait, self.transfer_data_task # is canceled before the timeout elapses. - await asyncio.wait_for( - self.transfer_data_task, - self.close_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.close_timeout): + await self.transfer_data_task except (asyncio.TimeoutError, asyncio.CancelledError): pass @@ -798,8 +811,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 - A ping may serve as a keepalive or as a check that the remote endpoint - received all messages up to this point + A ping may serve as a keepalive, as a check that the remote endpoint + received all messages up to this point, or to measure :attr:`latency`. Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return immediately, it means the write buffer is full. If you don't want to @@ -814,18 +827,20 @@ class WebSocketCommonProtocol(asyncio.Protocol): containing four random bytes. Returns: - ~asyncio.Future: A future that will be completed when the - corresponding pong is received. You can ignore it if you - don't intend to wait. + ~asyncio.Future[float]: A future that will be completed when the + corresponding pong is received. You can ignore it if you don't + intend to wait. The result of the future is the latency of the + connection in seconds. :: pong_waiter = await ws.ping() - await pong_waiter # only if you want to wait for the pong + # only if you want to wait for the corresponding pong + latency = await pong_waiter Raises: - ConnectionClosed: when the connection is closed. - RuntimeError: if another ping was sent with the same data and + ConnectionClosed: When the connection is closed. + RuntimeError: If another ping was sent with the same data and the corresponding pong wasn't received yet. """ @@ -842,11 +857,14 @@ class WebSocketCommonProtocol(asyncio.Protocol): while data is None or data in self.pings: data = struct.pack("!I", random.getrandbits(32)) - self.pings[data] = self.loop.create_future() + pong_waiter = self.loop.create_future() + # Resolution of time.monotonic() may be too low on Windows. + ping_timestamp = time.perf_counter() + self.pings[data] = (pong_waiter, ping_timestamp) await self.write_frame(True, OP_PING, data) - return asyncio.shield(self.pings[data]) + return asyncio.shield(pong_waiter) async def pong(self, data: Data = b"") -> None: """ @@ -861,11 +879,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): wait, you should close the connection. Args: - data (Data): payload of the pong; a string will be encoded to + data (Data): Payload of the pong. A string will be encoded to UTF-8. Raises: - ConnectionClosed: when the connection is closed. + ConnectionClosed: When the connection is closed. """ await self.ensure_open() @@ -973,7 +991,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): except ProtocolError as exc: self.transfer_data_exc = exc - self.fail_connection(1002) + self.fail_connection(CloseCode.PROTOCOL_ERROR) except (ConnectionError, TimeoutError, EOFError, ssl.SSLError) as exc: # Reading data with self.reader.readexactly may raise: @@ -984,15 +1002,15 @@ class WebSocketCommonProtocol(asyncio.Protocol): # bytes are available than requested; # - ssl.SSLError if the other side infringes the TLS protocol. self.transfer_data_exc = exc - self.fail_connection(1006) + self.fail_connection(CloseCode.ABNORMAL_CLOSURE) except UnicodeDecodeError as exc: self.transfer_data_exc = exc - self.fail_connection(1007) + self.fail_connection(CloseCode.INVALID_DATA) except PayloadTooBig as exc: self.transfer_data_exc = exc - self.fail_connection(1009) + self.fail_connection(CloseCode.MESSAGE_TOO_BIG) except Exception as exc: # This shouldn't happen often because exceptions expected under @@ -1001,7 +1019,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): self.logger.error("data transfer failed", exc_info=True) self.transfer_data_exc = exc - self.fail_connection(1011) + self.fail_connection(CloseCode.INTERNAL_ERROR) async def read_message(self) -> Optional[Data]: """ @@ -1030,7 +1048,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): return frame.data.decode("utf-8") if text else frame.data # 5.4. Fragmentation - chunks: List[Data] = [] + fragments: List[Data] = [] max_size = self.max_size if text: decoder_factory = codecs.getincrementaldecoder("utf-8") @@ -1038,14 +1056,14 @@ class WebSocketCommonProtocol(asyncio.Protocol): if max_size is None: def append(frame: Frame) -> None: - nonlocal chunks - chunks.append(decoder.decode(frame.data, frame.fin)) + nonlocal fragments + fragments.append(decoder.decode(frame.data, frame.fin)) else: def append(frame: Frame) -> None: - nonlocal chunks, max_size - chunks.append(decoder.decode(frame.data, frame.fin)) + nonlocal fragments, max_size + fragments.append(decoder.decode(frame.data, frame.fin)) assert isinstance(max_size, int) max_size -= len(frame.data) @@ -1053,14 +1071,14 @@ class WebSocketCommonProtocol(asyncio.Protocol): if max_size is None: def append(frame: Frame) -> None: - nonlocal chunks - chunks.append(frame.data) + nonlocal fragments + fragments.append(frame.data) else: def append(frame: Frame) -> None: - nonlocal chunks, max_size - chunks.append(frame.data) + nonlocal fragments, max_size + fragments.append(frame.data) assert isinstance(max_size, int) max_size -= len(frame.data) @@ -1074,7 +1092,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): raise ProtocolError("unexpected opcode") append(frame) - return ("" if text else b"").join(chunks) + return ("" if text else b"").join(fragments) async def read_data_frame(self, max_size: Optional[int]) -> Optional[Frame]: """ @@ -1099,7 +1117,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): try: # Echo the original data instead of re-serializing it with # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthetizes a 1005 close code. + # is empty and Close.parse() synthesizes a 1005 close code. await self.write_close_frame(self.close_rcvd, frame.data) except ConnectionClosed: # Connection closed before we could echo the close frame. @@ -1117,18 +1135,20 @@ class WebSocketCommonProtocol(asyncio.Protocol): elif frame.opcode == OP_PONG: if frame.data in self.pings: + pong_timestamp = time.perf_counter() # Sending a pong for only the most recent ping is legal. # Acknowledge all previous pings too in that case. ping_id = None ping_ids = [] - for ping_id, ping in self.pings.items(): + for ping_id, (pong_waiter, ping_timestamp) in self.pings.items(): ping_ids.append(ping_id) - if not ping.done(): - ping.set_result(None) + if not pong_waiter.done(): + pong_waiter.set_result(pong_timestamp - ping_timestamp) if ping_id == frame.data: + self.latency = pong_timestamp - ping_timestamp break - else: # pragma: no cover - assert False, "ping_id is in self.pings" + else: + raise AssertionError("solicited pong not found in pings") # Remove acknowledged pings from self.pings. for ping_id in ping_ids: del self.pings[ping_id] @@ -1231,10 +1251,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): try: while True: - await asyncio.sleep( - self.ping_interval, - **loop_if_py_lt_38(self.loop), - ) + await asyncio.sleep(self.ping_interval) # ping() raises CancelledError if the connection is closed, # when close_connection() cancels self.keepalive_ping_task. @@ -1247,23 +1264,18 @@ class WebSocketCommonProtocol(asyncio.Protocol): if self.ping_timeout is not None: try: - await asyncio.wait_for( - pong_waiter, - self.ping_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.ping_timeout): + await pong_waiter self.logger.debug("% received keepalive pong") except asyncio.TimeoutError: if self.debug: self.logger.debug("! timed out waiting for keepalive pong") - self.fail_connection(1011, "keepalive ping timeout") + self.fail_connection( + CloseCode.INTERNAL_ERROR, + "keepalive ping timeout", + ) break - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: - raise - except ConnectionClosed: pass @@ -1297,9 +1309,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): # A client should wait for a TCP close from the server. if self.is_client and hasattr(self, "transfer_data_task"): if await self.wait_for_connection_lost(): - # Coverage marks this line as a partially executed branch. - # I supect a bug in coverage. Ignore it for now. - return # pragma: no cover + return if self.debug: self.logger.debug("! timed out waiting for TCP close") @@ -1317,9 +1327,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): pass if await self.wait_for_connection_lost(): - # Coverage marks this line as a partially executed branch. - # I supect a bug in coverage. Ignore it for now. - return # pragma: no cover + return if self.debug: self.logger.debug("! timed out waiting for TCP close") @@ -1352,12 +1360,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Abort the TCP connection. Buffers are discarded. if self.debug: self.logger.debug("x aborting TCP connection") - self.transport.abort() + # Due to a bug in coverage, this is erroneously reported as not covered. + self.transport.abort() # pragma: no cover # connection_lost() is called quickly after aborting. - # Coverage marks this line as a partially executed branch. - # I supect a bug in coverage. Ignore it for now. - await self.wait_for_connection_lost() # pragma: no cover + await self.wait_for_connection_lost() async def wait_for_connection_lost(self) -> bool: """ @@ -1369,11 +1376,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): """ if not self.connection_lost_waiter.done(): try: - await asyncio.wait_for( - asyncio.shield(self.connection_lost_waiter), - self.close_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.close_timeout): + await asyncio.shield(self.connection_lost_waiter) except asyncio.TimeoutError: pass # Re-check self.connection_lost_waiter.done() synchronously because @@ -1381,7 +1385,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): # and the moment this coroutine resumes running. return self.connection_lost_waiter.done() - def fail_connection(self, code: int = 1006, reason: str = "") -> None: + def fail_connection( + self, + code: int = CloseCode.ABNORMAL_CLOSURE, + reason: str = "", + ) -> None: """ 7.1.7. Fail the WebSocket Connection @@ -1412,7 +1420,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): # sent if it's CLOSING), except when failing the connection because of # an error reading from or writing to the network. # Don't send a close frame if the connection is broken. - if code != 1006 and self.state is State.OPEN: + if code != CloseCode.ABNORMAL_CLOSURE and self.state is State.OPEN: close = Close(code, reason) # Write the close frame without draining the write buffer. @@ -1449,13 +1457,13 @@ class WebSocketCommonProtocol(asyncio.Protocol): assert self.state is State.CLOSED exc = self.connection_closed_exc() - for ping in self.pings.values(): - ping.set_exception(exc) + for pong_waiter, _ping_timestamp in self.pings.values(): + pong_waiter.set_exception(exc) # If the exception is never retrieved, it will be logged when ping # is garbage-collected. This is confusing for users. # Given that ping is done (with an exception), canceling it does # nothing, but it prevents logging the exception. - ping.cancel() + pong_waiter.cancel() # asyncio.Protocol methods @@ -1496,7 +1504,6 @@ class WebSocketCommonProtocol(asyncio.Protocol): self.connection_lost_waiter.set_result(None) if True: # pragma: no cover - # Copied from asyncio.StreamReaderProtocol if self.reader is not None: if exc is None: @@ -1552,13 +1559,17 @@ class WebSocketCommonProtocol(asyncio.Protocol): self.reader.feed_eof() -def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> None: +def broadcast( + websockets: Iterable[WebSocketCommonProtocol], + message: Data, + raise_exceptions: bool = False, +) -> None: """ Broadcast a message to several WebSocket connections. - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. + A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like + object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent + as a Binary_ frame. .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 @@ -1566,33 +1577,42 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N :func:`broadcast` pushes the message synchronously to all connections even if their write buffers are overflowing. There's no backpressure. - :func:`broadcast` skips silently connections that aren't open in order to - avoid errors on connections where the closing handshake is in progress. - - If you broadcast messages faster than a connection can handle them, - messages will pile up in its write buffer until the connection times out. - Keep low values for ``ping_interval`` and ``ping_timeout`` to prevent - excessive memory usage by slow connections when you use :func:`broadcast`. + If you broadcast messages faster than a connection can handle them, messages + will pile up in its write buffer until the connection times out. Keep + ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage + from slow connections. Unlike :meth:`~websockets.server.WebSocketServerProtocol.send`, :func:`broadcast` doesn't support sending fragmented messages. Indeed, - fragmentation is useful for sending large messages without buffering - them in memory, while :func:`broadcast` buffers one copy per connection - as fast as possible. + fragmentation is useful for sending large messages without buffering them in + memory, while :func:`broadcast` buffers one copy per connection as fast as + possible. + + :func:`broadcast` skips connections that aren't open in order to avoid + errors on connections where the closing handshake is in progress. + + :func:`broadcast` ignores failures to write the message on some connections. + It continues writing to other connections. On Python 3.11 and above, you + may set ``raise_exceptions`` to :obj:`True` to record failures and raise all + exceptions in a :pep:`654` :exc:`ExceptionGroup`. Args: - websockets (Iterable[WebSocketCommonProtocol]): WebSocket connections - to which the message will be sent. - message (Data): message to send. + websockets: WebSocket connections to which the message will be sent. + message: Message to send. + raise_exceptions: Whether to raise an exception in case of failures. Raises: - RuntimeError: if a connection is busy sending a fragmented message. - TypeError: if ``message`` doesn't have a supported type. + TypeError: If ``message`` doesn't have a supported type. """ if not isinstance(message, (str, bytes, bytearray, memoryview)): raise TypeError("data must be str or bytes-like") + if raise_exceptions: + if sys.version_info[:2] < (3, 11): # pragma: no cover + raise ValueError("raise_exceptions requires at least Python 3.11") + exceptions = [] + opcode, data = prepare_data(message) for websocket in websockets: @@ -1600,6 +1620,26 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N continue if websocket._fragmented_message_waiter is not None: - raise RuntimeError("busy sending a fragmented message") + if raise_exceptions: + exception = RuntimeError("sending a fragmented message") + exceptions.append(exception) + else: + websocket.logger.warning( + "skipped broadcast: sending a fragmented message", + ) + + try: + websocket.write_frame_sync(True, opcode, data) + except Exception as write_exception: + if raise_exceptions: + exception = RuntimeError("failed to write message") + exception.__cause__ = write_exception + exceptions.append(exception) + else: + websocket.logger.warning( + "skipped broadcast: failed to write message", + exc_info=True, + ) - websocket.write_frame_sync(True, opcode, data) + if raise_exceptions: + raise ExceptionGroup("skipped broadcast", exceptions) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py index 3e51db1b71..7c24dd74af 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py @@ -25,7 +25,6 @@ from typing import ( cast, ) -from ..connection import State from ..datastructures import Headers, HeadersLike, MultipleValuesError from ..exceptions import ( AbortHandshake, @@ -45,8 +44,9 @@ from ..headers import ( validate_subprotocols, ) from ..http import USER_AGENT -from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol -from .compatibility import loop_if_py_lt_38 +from ..protocol import State +from ..typing import ExtensionHeader, LoggerLike, Origin, StatusLike, Subprotocol +from .compatibility import asyncio_timeout from .handshake import build_response, check_request from .http import read_request from .protocol import WebSocketCommonProtocol @@ -57,7 +57,7 @@ __all__ = ["serve", "unix_serve", "WebSocketServerProtocol", "WebSocketServer"] HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]] -HTTPResponse = Tuple[http.HTTPStatus, HeadersLike, bytes] +HTTPResponse = Tuple[StatusLike, HeadersLike, bytes] class WebSocketServerProtocol(WebSocketCommonProtocol): @@ -73,7 +73,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): await process(message) The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away). It raises + 1000 (OK) or 1001 (going away) or without a close code. It raises a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is closed with any other code. @@ -84,7 +84,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): ws_server: WebSocket server that created this connection. See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``, - ``extensions``, ``subprotocols``, and ``extra_headers``. + ``extensions``, ``subprotocols``, ``extra_headers``, and ``server_header``. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -108,12 +108,14 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): extensions: Optional[Sequence[ServerExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLikeOrCallable] = None, + server_header: Optional[str] = USER_AGENT, process_request: Optional[ Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]] ] = None, select_subprotocol: Optional[ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] ] = None, + open_timeout: Optional[float] = 10, **kwargs: Any, ) -> None: if logger is None: @@ -132,8 +134,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): self.available_extensions = extensions self.available_subprotocols = subprotocols self.extra_headers = extra_headers + self.server_header = server_header self._process_request = process_request self._select_subprotocol = select_subprotocol + self.open_timeout = open_timeout def connection_made(self, transport: asyncio.BaseTransport) -> None: """ @@ -153,22 +157,20 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): Handle the lifecycle of a WebSocket connection. Since this method doesn't have a caller able to handle exceptions, it - attemps to log relevant ones and guarantees that the TCP connection is + attempts to log relevant ones and guarantees that the TCP connection is closed before exiting. """ try: - try: - await self.handshake( - origins=self.origins, - available_extensions=self.available_extensions, - available_subprotocols=self.available_subprotocols, - extra_headers=self.extra_headers, - ) - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: # pragma: no cover + async with asyncio_timeout(self.open_timeout): + await self.handshake( + origins=self.origins, + available_extensions=self.available_extensions, + available_subprotocols=self.available_subprotocols, + extra_headers=self.extra_headers, + ) + except asyncio.TimeoutError: # pragma: no cover raise except ConnectionError: raise @@ -216,14 +218,16 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): ) headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - headers.setdefault("Server", USER_AGENT) + if self.server_header is not None: + headers.setdefault("Server", self.server_header) + headers.setdefault("Content-Length", str(len(body))) headers.setdefault("Content-Type", "text/plain") headers.setdefault("Connection", "close") self.write_http_response(status, headers, body) self.logger.info( - "connection failed (%d %s)", status.value, status.phrase + "connection rejected (%d %s)", status.value, status.phrase ) await self.close_transport() return @@ -325,9 +329,9 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): You may override this method in a :class:`WebSocketServerProtocol` subclass, for example: - * to return a HTTP 200 OK response on a given path; then a load + * to return an HTTP 200 OK response on a given path; then a load balancer can use this path for a health check; - * to authenticate the request and return a HTTP 401 Unauthorized or a + * to authenticate the request and return an HTTP 401 Unauthorized or an HTTP 403 Forbidden when authentication fails. You may also override this method with the ``process_request`` @@ -345,7 +349,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): request_headers: request headers. Returns: - Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]: :obj:`None` + Optional[Tuple[StatusLike, HeadersLike, bytes]]: :obj:`None` to continue the WebSocket handshake normally. An HTTP response, represented by a 3-uple of the response status, @@ -439,15 +443,12 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Extensions") if header_values and available_extensions: - parsed_header_values: List[ExtensionHeader] = sum( [parse_extension(header_value) for header_value in header_values], [] ) for name, request_params in parsed_header_values: - for ext_factory in available_extensions: - # Skip non-matching extensions based on their name. if ext_factory.name != name: continue @@ -499,7 +500,6 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Protocol") if header_values and available_subprotocols: - parsed_header_values: List[Subprotocol] = sum( [parse_subprotocol(header_value) for header_value in header_values], [] ) @@ -516,31 +516,29 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): server_subprotocols: Sequence[Subprotocol], ) -> Optional[Subprotocol]: """ - Pick a subprotocol among those offered by the client. + Pick a subprotocol among those supported by the client and the server. - If several subprotocols are supported by the client and the server, - the default implementation selects the preferred subprotocol by - giving equal value to the priorities of the client and the server. - If no subprotocol is supported by the client and the server, it - proceeds without a subprotocol. + If several subprotocols are available, select the preferred subprotocol + by giving equal weight to the preferences of the client and the server. - This is unlikely to be the most useful implementation in practice. - Many servers providing a subprotocol will require that the client - uses that subprotocol. Such rules can be implemented in a subclass. + If no subprotocol is available, proceed without a subprotocol. - You may also override this method with the ``select_subprotocol`` - argument of :func:`serve` and :class:`WebSocketServerProtocol`. + You may provide a ``select_subprotocol`` argument to :func:`serve` or + :class:`WebSocketServerProtocol` to override this logic. For example, + you could reject the handshake if the client doesn't support a + particular subprotocol, rather than accept the handshake without that + subprotocol. Args: client_subprotocols: list of subprotocols offered by the client. server_subprotocols: list of subprotocols available on the server. Returns: - Optional[Subprotocol]: Selected subprotocol. + Optional[Subprotocol]: Selected subprotocol, if a common subprotocol + was found. :obj:`None` to continue without a subprotocol. - """ if self._select_subprotocol is not None: return self._select_subprotocol(client_subprotocols, server_subprotocols) @@ -548,10 +546,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): subprotocols = set(client_subprotocols) & set(server_subprotocols) if not subprotocols: return None - priority = lambda p: ( - client_subprotocols.index(p) + server_subprotocols.index(p) - ) - return sorted(subprotocols, key=priority)[0] + return sorted( + subprotocols, + key=lambda p: client_subprotocols.index(p) + server_subprotocols.index(p), + )[0] async def handshake( self, @@ -594,7 +592,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): # The connection may drop while process_request is running. if self.state is State.CLOSED: - raise self.connection_closed_exc() # pragma: no cover + # This subclass of ConnectionError is silently ignored in handler(). + raise BrokenPipeError("connection closed during opening handshake") # Change the response to a 503 error if the server is shutting down. if not self.ws_server.is_serving(): @@ -635,7 +634,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): response_headers.update(extra_headers) response_headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - response_headers.setdefault("Server", USER_AGENT) + if self.server_header is not None: + response_headers.setdefault("Server", self.server_header) self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers) @@ -658,9 +658,9 @@ class WebSocketServer: when shutting down. Args: - logger: logger for this server; - defaults to ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../topics/logging>` for details. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. + See the :doc:`logging guide <../../topics/logging>` for details. """ @@ -707,7 +707,7 @@ class WebSocketServer: self.logger.info("server listening on %s", name) # Initialized here because we need a reference to the event loop. - # This should be moved back to __init__ in Python 3.10. + # This should be moved back to __init__ when dropping Python < 3.10. self.closed_waiter = server.get_loop().create_future() def register(self, protocol: WebSocketServerProtocol) -> None: @@ -724,26 +724,30 @@ class WebSocketServer: """ self.websockets.remove(protocol) - def close(self) -> None: + def close(self, close_connections: bool = True) -> None: """ Close the server. - This method: + * Close the underlying :class:`~asyncio.Server`. + * When ``close_connections`` is :obj:`True`, which is the default, + close existing connections. Specifically: - * closes the underlying :class:`~asyncio.Server`; - * rejects new WebSocket connections with an HTTP 503 (service - unavailable) error; this happens when the server accepted the TCP - connection but didn't complete the WebSocket opening handshake prior - to closing; - * closes open WebSocket connections with close code 1001 (going away). + * Reject opening WebSocket connections with an HTTP 503 (service + unavailable) error. This happens when the server accepted the TCP + connection but didn't complete the opening handshake before closing. + * Close open WebSocket connections with close code 1001 (going away). + + * Wait until all connection handlers terminate. :meth:`close` is idempotent. """ if self.close_task is None: - self.close_task = self.get_loop().create_task(self._close()) + self.close_task = self.get_loop().create_task( + self._close(close_connections) + ) - async def _close(self) -> None: + async def _close(self, close_connections: bool) -> None: """ Implementation of :meth:`close`. @@ -757,36 +761,30 @@ class WebSocketServer: # Stop accepting new connections. self.server.close() - # Wait until self.server.close() completes. - await self.server.wait_closed() - # Wait until all accepted connections reach connection_made() and call # register(). See https://bugs.python.org/issue34852 for details. - await asyncio.sleep(0, **loop_if_py_lt_38(self.get_loop())) - - # Close OPEN connections with status code 1001. Since the server was - # closed, handshake() closes OPENING connections with a HTTP 503 - # error. Wait until all connections are closed. - - close_tasks = [ - asyncio.create_task(websocket.close(1001)) - for websocket in self.websockets - if websocket.state is not State.CONNECTING - ] - # asyncio.wait doesn't accept an empty first argument. - if close_tasks: - await asyncio.wait( - close_tasks, - **loop_if_py_lt_38(self.get_loop()), - ) - - # Wait until all connection handlers are complete. + await asyncio.sleep(0) + + if close_connections: + # Close OPEN connections with close code 1001. After server.close(), + # handshake() closes OPENING connections with an HTTP 503 error. + close_tasks = [ + asyncio.create_task(websocket.close(1001)) + for websocket in self.websockets + if websocket.state is not State.CONNECTING + ] + # asyncio.wait doesn't accept an empty first argument. + if close_tasks: + await asyncio.wait(close_tasks) + + # Wait until all TCP connections are closed. + await self.server.wait_closed() + # Wait until all connection handlers terminate. # asyncio.wait doesn't accept an empty first argument. if self.websockets: await asyncio.wait( - [websocket.handler_task for websocket in self.websockets], - **loop_if_py_lt_38(self.get_loop()), + [websocket.handler_task for websocket in self.websockets] ) # Tell wait_closed() to return. @@ -829,19 +827,37 @@ class WebSocketServer: """ return self.server.is_serving() - async def start_serving(self) -> None: + async def start_serving(self) -> None: # pragma: no cover """ See :meth:`asyncio.Server.start_serving`. + Typical use:: + + server = await serve(..., start_serving=False) + # perform additional setup here... + # ... then start the server + await server.start_serving() + """ - await self.server.start_serving() # pragma: no cover + await self.server.start_serving() - async def serve_forever(self) -> None: + async def serve_forever(self) -> None: # pragma: no cover """ See :meth:`asyncio.Server.serve_forever`. + Typical use:: + + server = await serve(...) + # this coroutine doesn't return + # canceling it stops the server + await server.serve_forever() + + This is an alternative to using :func:`serve` as an asynchronous context + manager. Shutdown is triggered by canceling :meth:`serve_forever` + instead of exiting a :func:`serve` context. + """ - await self.server.serve_forever() # pragma: no cover + await self.server.serve_forever() @property def sockets(self) -> Iterable[socket.socket]: @@ -851,17 +867,17 @@ class WebSocketServer: """ return self.server.sockets - async def __aenter__(self) -> WebSocketServer: - return self # pragma: no cover + async def __aenter__(self) -> WebSocketServer: # pragma: no cover + return self async def __aexit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], - ) -> None: - self.close() # pragma: no cover - await self.wait_closed() # pragma: no cover + ) -> None: # pragma: no cover + self.close() + await self.wait_closed() class Serve: @@ -879,53 +895,61 @@ class Serve: server performs the closing handshake and closes the connection. Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object - provides :meth:`~WebSocketServer.close` and - :meth:`~WebSocketServer.wait_closed` methods for shutting down the server. + provides a :meth:`~WebSocketServer.close` method to shut down the server:: - :func:`serve` can be used as an asynchronous context manager:: + stop = asyncio.Future() # set this future to exit the server + + server = await serve(...) + await stop + await server.close() + + :func:`serve` can be used as an asynchronous context manager. Then, the + server is shut down automatically when exiting the context:: stop = asyncio.Future() # set this future to exit the server async with serve(...): await stop - The server is shut down automatically when exiting the context. - Args: - ws_handler: connection handler. It receives the WebSocket connection, + ws_handler: Connection handler. It receives the WebSocket connection, which is a :class:`WebSocketServerProtocol`, in argument. - host: network interfaces the server is bound to; - see :meth:`~asyncio.loop.create_server` for details. - port: TCP port the server listens on; - see :meth:`~asyncio.loop.create_server` for details. - create_protocol: factory for the :class:`asyncio.Protocol` managing - the connection; defaults to :class:`WebSocketServerProtocol`; may - be set to a wrapper or a subclass to customize connection handling. - logger: logger for this server; - defaults to ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../topics/logging>` for details. - compression: shortcut that enables the "permessage-deflate" extension - by default; may be set to :obj:`None` to disable compression; - see the :doc:`compression guide <../topics/compression>` for details. - origins: acceptable values of the ``Origin`` header; include - :obj:`None` in the list if the lack of an origin is acceptable. - This is useful for defending against Cross-Site WebSocket - Hijacking attacks. - extensions: list of supported extensions, in order in which they - should be tried. - subprotocols: list of supported subprotocols, in order of decreasing + host: Network interfaces the server binds to. + See :meth:`~asyncio.loop.create_server` for details. + port: TCP port the server listens on. + See :meth:`~asyncio.loop.create_server` for details. + create_protocol: Factory for the :class:`asyncio.Protocol` managing + the connection. It defaults to :class:`WebSocketServerProtocol`. + Set it to a wrapper or a subclass to customize connection handling. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. + See the :doc:`logging guide <../../topics/logging>` for details. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + origins: Acceptable values of the ``Origin`` header, for defending + against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` + in the list if the lack of an origin is acceptable. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing preference. extra_headers (Union[HeadersLike, Callable[[str, Headers], HeadersLike]]): - arbitrary HTTP headers to add to the request; this can be + Arbitrary HTTP headers to add to the response. This can be a :data:`~websockets.datastructures.HeadersLike` or a callable taking the request path and headers in arguments and returning a :data:`~websockets.datastructures.HeadersLike`. + server_header: Value of the ``Server`` response header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. process_request (Optional[Callable[[str, Headers], \ - Awaitable[Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]]]]): - intercept HTTP request before the opening handshake; - see :meth:`~WebSocketServerProtocol.process_request` for details. - select_subprotocol: select a subprotocol supported by the client; - see :meth:`~WebSocketServerProtocol.select_subprotocol` for details. + Awaitable[Optional[Tuple[StatusLike, HeadersLike, bytes]]]]]): + Intercept HTTP request before the opening handshake. + See :meth:`~WebSocketServerProtocol.process_request` for details. + select_subprotocol: Select a subprotocol supported by the client. + See :meth:`~WebSocketServerProtocol.select_subprotocol` for details. + open_timeout: Timeout for opening connections in seconds. + :obj:`None` disables the timeout. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -955,19 +979,21 @@ class Serve: host: Optional[Union[str, Sequence[str]]] = None, port: Optional[int] = None, *, - create_protocol: Optional[Callable[[Any], WebSocketServerProtocol]] = None, + create_protocol: Optional[Callable[..., WebSocketServerProtocol]] = None, logger: Optional[LoggerLike] = None, compression: Optional[str] = "deflate", origins: Optional[Sequence[Optional[Origin]]] = None, extensions: Optional[Sequence[ServerExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLikeOrCallable] = None, + server_header: Optional[str] = USER_AGENT, process_request: Optional[ Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]] ] = None, select_subprotocol: Optional[ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] ] = None, + open_timeout: Optional[float] = 10, ping_interval: Optional[float] = 20, ping_timeout: Optional[float] = 20, close_timeout: Optional[float] = None, @@ -1030,6 +1056,7 @@ class Serve: host=host, port=port, secure=secure, + open_timeout=open_timeout, ping_interval=ping_interval, ping_timeout=ping_timeout, close_timeout=close_timeout, @@ -1043,6 +1070,7 @@ class Serve: extensions=extensions, subprotocols=subprotocols, extra_headers=extra_headers, + server_header=server_header, process_request=process_request, select_subprotocol=select_subprotocol, logger=logger, @@ -1106,17 +1134,18 @@ def unix_serve( **kwargs: Any, ) -> Serve: """ - Similar to :func:`serve`, but for listening on Unix sockets. + Start a WebSocket server listening on a Unix socket. - This function builds upon the event - loop's :meth:`~asyncio.loop.create_unix_server` method. + This function is identical to :func:`serve`, except the ``host`` and + ``port`` arguments are replaced by ``path``. It is only available on Unix. - It is only available on Unix. + Unrecognized keyword arguments are passed the event loop's + :meth:`~asyncio.loop.create_unix_server` method. It's useful for deploying a server behind a reverse proxy such as nginx. Args: - path: file system path to the Unix socket. + path: File system path to the Unix socket. """ return serve(ws_handler, path=path, unix=True, **kwargs) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py new file mode 100644 index 0000000000..765e6b9bb4 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py @@ -0,0 +1,708 @@ +from __future__ import annotations + +import enum +import logging +import uuid +from typing import Generator, List, Optional, Type, Union + +from .exceptions import ( + ConnectionClosed, + ConnectionClosedError, + ConnectionClosedOK, + InvalidState, + PayloadTooBig, + ProtocolError, +) +from .extensions import Extension +from .frames import ( + OK_CLOSE_CODES, + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Frame, +) +from .http11 import Request, Response +from .streams import StreamReader +from .typing import LoggerLike, Origin, Subprotocol + + +__all__ = [ + "Protocol", + "Side", + "State", + "SEND_EOF", +] + +Event = Union[Request, Response, Frame] +"""Events that :meth:`~Protocol.events_received` may return.""" + + +class Side(enum.IntEnum): + """A WebSocket connection is either a server or a client.""" + + SERVER, CLIENT = range(2) + + +SERVER = Side.SERVER +CLIENT = Side.CLIENT + + +class State(enum.IntEnum): + """A WebSocket connection is in one of these four states.""" + + CONNECTING, OPEN, CLOSING, CLOSED = range(4) + + +CONNECTING = State.CONNECTING +OPEN = State.OPEN +CLOSING = State.CLOSING +CLOSED = State.CLOSED + + +SEND_EOF = b"" +"""Sentinel signaling that the TCP connection must be half-closed.""" + + +class Protocol: + """ + Sans-I/O implementation of a WebSocket connection. + + Args: + side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`. + state: initial state of the WebSocket connection. + max_size: maximum size of incoming messages in bytes; + :obj:`None` disables the limit. + logger: logger for this connection; depending on ``side``, + defaults to ``logging.getLogger("websockets.client")`` + or ``logging.getLogger("websockets.server")``; + see the :doc:`logging guide <../../topics/logging>` for details. + + """ + + def __init__( + self, + side: Side, + *, + state: State = OPEN, + max_size: Optional[int] = 2**20, + logger: Optional[LoggerLike] = None, + ) -> None: + # Unique identifier. For logs. + self.id: uuid.UUID = uuid.uuid4() + """Unique identifier of the connection. Useful in logs.""" + + # Logger or LoggerAdapter for this connection. + if logger is None: + logger = logging.getLogger(f"websockets.{side.name.lower()}") + self.logger: LoggerLike = logger + """Logger for this connection.""" + + # Track if DEBUG is enabled. Shortcut logging calls if it isn't. + self.debug = logger.isEnabledFor(logging.DEBUG) + + # Connection side. CLIENT or SERVER. + self.side = side + + # Connection state. Initially OPEN because subclasses handle CONNECTING. + self.state = state + + # Maximum size of incoming messages in bytes. + self.max_size = max_size + + # Current size of incoming message in bytes. Only set while reading a + # fragmented message i.e. a data frames with the FIN bit not set. + self.cur_size: Optional[int] = None + + # True while sending a fragmented message i.e. a data frames with the + # FIN bit not set. + self.expect_continuation_frame = False + + # WebSocket protocol parameters. + self.origin: Optional[Origin] = None + self.extensions: List[Extension] = [] + self.subprotocol: Optional[Subprotocol] = None + + # Close code and reason, set when a close frame is sent or received. + self.close_rcvd: Optional[Close] = None + self.close_sent: Optional[Close] = None + self.close_rcvd_then_sent: Optional[bool] = None + + # Track if an exception happened during the handshake. + self.handshake_exc: Optional[Exception] = None + """ + Exception to raise if the opening handshake failed. + + :obj:`None` if the opening handshake succeeded. + + """ + + # Track if send_eof() was called. + self.eof_sent = False + + # Parser state. + self.reader = StreamReader() + self.events: List[Event] = [] + self.writes: List[bytes] = [] + self.parser = self.parse() + next(self.parser) # start coroutine + self.parser_exc: Optional[Exception] = None + + @property + def state(self) -> State: + """ + WebSocket connection state. + + Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`. + + """ + return self._state + + @state.setter + def state(self, state: State) -> None: + if self.debug: + self.logger.debug("= connection is %s", state.name) + self._state = state + + @property + def close_code(self) -> Optional[int]: + """ + `WebSocket close code`_. + + .. _WebSocket close code: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not CLOSED: + return None + elif self.close_rcvd is None: + return CloseCode.ABNORMAL_CLOSURE + else: + return self.close_rcvd.code + + @property + def close_reason(self) -> Optional[str]: + """ + `WebSocket close reason`_. + + .. _WebSocket close reason: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not CLOSED: + return None + elif self.close_rcvd is None: + return "" + else: + return self.close_rcvd.reason + + @property + def close_exc(self) -> ConnectionClosed: + """ + Exception to raise when trying to interact with a closed connection. + + Don't raise this exception while the connection :attr:`state` + is :attr:`~websockets.protocol.State.CLOSING`; wait until + it's :attr:`~websockets.protocol.State.CLOSED`. + + Indeed, the exception includes the close code and reason, which are + known only once the connection is closed. + + Raises: + AssertionError: if the connection isn't closed yet. + + """ + assert self.state is CLOSED, "connection isn't closed yet" + exc_type: Type[ConnectionClosed] + if ( + self.close_rcvd is not None + and self.close_sent is not None + and self.close_rcvd.code in OK_CLOSE_CODES + and self.close_sent.code in OK_CLOSE_CODES + ): + exc_type = ConnectionClosedOK + else: + exc_type = ConnectionClosedError + exc: ConnectionClosed = exc_type( + self.close_rcvd, + self.close_sent, + self.close_rcvd_then_sent, + ) + # Chain to the exception raised in the parser, if any. + exc.__cause__ = self.parser_exc + return exc + + # Public methods for receiving data. + + def receive_data(self, data: bytes) -> None: + """ + Receive data from the network. + + After calling this method: + + - You must call :meth:`data_to_send` and send this data to the network. + - You should call :meth:`events_received` and process resulting events. + + Raises: + EOFError: if :meth:`receive_eof` was called earlier. + + """ + self.reader.feed_data(data) + next(self.parser) + + def receive_eof(self) -> None: + """ + Receive the end of the data stream from the network. + + After calling this method: + + - You must call :meth:`data_to_send` and send this data to the network; + it will return ``[b""]``, signaling the end of the stream, or ``[]``. + - You aren't expected to call :meth:`events_received`; it won't return + any new events. + + Raises: + EOFError: if :meth:`receive_eof` was called earlier. + + """ + self.reader.feed_eof() + next(self.parser) + + # Public methods for sending events. + + def send_continuation(self, data: bytes, fin: bool) -> None: + """ + Send a `Continuation frame`_. + + .. _Continuation frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing the same kind of data + as the initial frame. + fin: FIN bit; set it to :obj:`True` if this is the last frame + of a fragmented message and to :obj:`False` otherwise. + + Raises: + ProtocolError: if a fragmented message isn't in progress. + + """ + if not self.expect_continuation_frame: + raise ProtocolError("unexpected continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_CONT, data, fin)) + + def send_text(self, data: bytes, fin: bool = True) -> None: + """ + Send a `Text frame`_. + + .. _Text frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing text encoded with UTF-8. + fin: FIN bit; set it to :obj:`False` if this is the first frame of + a fragmented message. + + Raises: + ProtocolError: if a fragmented message is in progress. + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_TEXT, data, fin)) + + def send_binary(self, data: bytes, fin: bool = True) -> None: + """ + Send a `Binary frame`_. + + .. _Binary frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing arbitrary binary data. + fin: FIN bit; set it to :obj:`False` if this is the first frame of + a fragmented message. + + Raises: + ProtocolError: if a fragmented message is in progress. + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_BINARY, data, fin)) + + def send_close(self, code: Optional[int] = None, reason: str = "") -> None: + """ + Send a `Close frame`_. + + .. _Close frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 + + Parameters: + code: close code. + reason: close reason. + + Raises: + ProtocolError: if a fragmented message is being sent, if the code + isn't valid, or if a reason is provided without a code + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + if code is None: + if reason != "": + raise ProtocolError("cannot send a reason without a code") + close = Close(CloseCode.NO_STATUS_RCVD, "") + data = b"" + else: + close = Close(code, reason) + data = close.serialize() + # send_frame() guarantees that self.state is OPEN at this point. + # 7.1.3. The WebSocket Closing Handshake is Started + self.send_frame(Frame(OP_CLOSE, data)) + self.close_sent = close + self.state = CLOSING + + def send_ping(self, data: bytes) -> None: + """ + Send a `Ping frame`_. + + .. _Ping frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 + + Parameters: + data: payload containing arbitrary binary data. + + """ + self.send_frame(Frame(OP_PING, data)) + + def send_pong(self, data: bytes) -> None: + """ + Send a `Pong frame`_. + + .. _Pong frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 + + Parameters: + data: payload containing arbitrary binary data. + + """ + self.send_frame(Frame(OP_PONG, data)) + + def fail(self, code: int, reason: str = "") -> None: + """ + `Fail the WebSocket connection`_. + + .. _Fail the WebSocket connection: + https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7 + + Parameters: + code: close code + reason: close reason + + Raises: + ProtocolError: if the code isn't valid. + """ + # 7.1.7. Fail the WebSocket Connection + + # Send a close frame when the state is OPEN (a close frame was already + # sent if it's CLOSING), except when failing the connection because + # of an error reading from or writing to the network. + if self.state is OPEN: + if code != CloseCode.ABNORMAL_CLOSURE: + close = Close(code, reason) + data = close.serialize() + self.send_frame(Frame(OP_CLOSE, data)) + self.close_sent = close + self.state = CLOSING + + # When failing the connection, a server closes the TCP connection + # without waiting for the client to complete the handshake, while a + # client waits for the server to close the TCP connection, possibly + # after sending a close frame that the client will ignore. + if self.side is SERVER and not self.eof_sent: + self.send_eof() + + # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue + # to attempt to process data(including a responding Close frame) from + # the remote endpoint after being instructed to _Fail the WebSocket + # Connection_." + self.parser = self.discard() + next(self.parser) # start coroutine + + # Public method for getting incoming events after receiving data. + + def events_received(self) -> List[Event]: + """ + Fetch events generated from data received from the network. + + Call this method immediately after any of the ``receive_*()`` methods. + + Process resulting events, likely by passing them to the application. + + Returns: + List[Event]: Events read from the connection. + """ + events, self.events = self.events, [] + return events + + # Public method for getting outgoing data after receiving data or sending events. + + def data_to_send(self) -> List[bytes]: + """ + Obtain data to send to the network. + + Call this method immediately after any of the ``receive_*()``, + ``send_*()``, or :meth:`fail` methods. + + Write resulting data to the connection. + + The empty bytestring :data:`~websockets.protocol.SEND_EOF` signals + the end of the data stream. When you receive it, half-close the TCP + connection. + + Returns: + List[bytes]: Data to write to the connection. + + """ + writes, self.writes = self.writes, [] + return writes + + def close_expected(self) -> bool: + """ + Tell if the TCP connection is expected to close soon. + + Call this method immediately after any of the ``receive_*()``, + ``send_close()``, or :meth:`fail` methods. + + If it returns :obj:`True`, schedule closing the TCP connection after a + short timeout if the other side hasn't already closed it. + + Returns: + bool: Whether the TCP connection is expected to close soon. + + """ + # We expect a TCP close if and only if we sent a close frame: + # * Normal closure: once we send a close frame, we expect a TCP close: + # server waits for client to complete the TCP closing handshake; + # client waits for server to initiate the TCP closing handshake. + # * Abnormal closure: we always send a close frame and the same logic + # applies, except on EOFError where we don't send a close frame + # because we already received the TCP close, so we don't expect it. + # We already got a TCP Close if and only if the state is CLOSED. + return self.state is CLOSING or self.handshake_exc is not None + + # Private methods for receiving data. + + def parse(self) -> Generator[None, None, None]: + """ + Parse incoming data into frames. + + :meth:`receive_data` and :meth:`receive_eof` run this generator + coroutine until it needs more data or reaches EOF. + + :meth:`parse` never raises an exception. Instead, it sets the + :attr:`parser_exc` and yields control. + + """ + try: + while True: + if (yield from self.reader.at_eof()): + if self.debug: + self.logger.debug("< EOF") + # If the WebSocket connection is closed cleanly, with a + # closing handhshake, recv_frame() substitutes parse() + # with discard(). This branch is reached only when the + # connection isn't closed cleanly. + raise EOFError("unexpected end of stream") + + if self.max_size is None: + max_size = None + elif self.cur_size is None: + max_size = self.max_size + else: + max_size = self.max_size - self.cur_size + + # During a normal closure, execution ends here on the next + # iteration of the loop after receiving a close frame. At + # this point, recv_frame() replaced parse() by discard(). + frame = yield from Frame.parse( + self.reader.read_exact, + mask=self.side is SERVER, + max_size=max_size, + extensions=self.extensions, + ) + + if self.debug: + self.logger.debug("< %s", frame) + + self.recv_frame(frame) + + except ProtocolError as exc: + self.fail(CloseCode.PROTOCOL_ERROR, str(exc)) + self.parser_exc = exc + + except EOFError as exc: + self.fail(CloseCode.ABNORMAL_CLOSURE, str(exc)) + self.parser_exc = exc + + except UnicodeDecodeError as exc: + self.fail(CloseCode.INVALID_DATA, f"{exc.reason} at position {exc.start}") + self.parser_exc = exc + + except PayloadTooBig as exc: + self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc)) + self.parser_exc = exc + + except Exception as exc: + self.logger.error("parser failed", exc_info=True) + # Don't include exception details, which may be security-sensitive. + self.fail(CloseCode.INTERNAL_ERROR) + self.parser_exc = exc + + # During an abnormal closure, execution ends here after catching an + # exception. At this point, fail() replaced parse() by discard(). + yield + raise AssertionError("parse() shouldn't step after error") + + def discard(self) -> Generator[None, None, None]: + """ + Discard incoming data. + + This coroutine replaces :meth:`parse`: + + - after receiving a close frame, during a normal closure (1.4); + - after sending a close frame, during an abnormal closure (7.1.7). + + """ + # The server close the TCP connection in the same circumstances where + # discard() replaces parse(). The client closes the connection later, + # after the server closes the connection or a timeout elapses. + # (The latter case cannot be handled in this Sans-I/O layer.) + assert (self.side is SERVER) == (self.eof_sent) + while not (yield from self.reader.at_eof()): + self.reader.discard() + if self.debug: + self.logger.debug("< EOF") + # A server closes the TCP connection immediately, while a client + # waits for the server to close the TCP connection. + if self.side is CLIENT: + self.send_eof() + self.state = CLOSED + # If discard() completes normally, execution ends here. + yield + # Once the reader reaches EOF, its feed_data/eof() methods raise an + # error, so our receive_data/eof() methods don't step the generator. + raise AssertionError("discard() shouldn't step after EOF") + + def recv_frame(self, frame: Frame) -> None: + """ + Process an incoming frame. + + """ + if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY: + if self.cur_size is not None: + raise ProtocolError("expected a continuation frame") + if frame.fin: + self.cur_size = None + else: + self.cur_size = len(frame.data) + + elif frame.opcode is OP_CONT: + if self.cur_size is None: + raise ProtocolError("unexpected continuation frame") + if frame.fin: + self.cur_size = None + else: + self.cur_size += len(frame.data) + + elif frame.opcode is OP_PING: + # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST + # send a Pong frame in response" + pong_frame = Frame(OP_PONG, frame.data) + self.send_frame(pong_frame) + + elif frame.opcode is OP_PONG: + # 5.5.3 Pong: "A response to an unsolicited Pong frame is not + # expected." + pass + + elif frame.opcode is OP_CLOSE: + # 7.1.5. The WebSocket Connection Close Code + # 7.1.6. The WebSocket Connection Close Reason + self.close_rcvd = Close.parse(frame.data) + if self.state is CLOSING: + assert self.close_sent is not None + self.close_rcvd_then_sent = False + + if self.cur_size is not None: + raise ProtocolError("incomplete fragmented message") + + # 5.5.1 Close: "If an endpoint receives a Close frame and did + # not previously send a Close frame, the endpoint MUST send a + # Close frame in response. (When sending a Close frame in + # response, the endpoint typically echos the status code it + # received.)" + + if self.state is OPEN: + # Echo the original data instead of re-serializing it with + # Close.serialize() because that fails when the close frame + # is empty and Close.parse() synthesizes a 1005 close code. + # The rest is identical to send_close(). + self.send_frame(Frame(OP_CLOSE, frame.data)) + self.close_sent = self.close_rcvd + self.close_rcvd_then_sent = True + self.state = CLOSING + + # 7.1.2. Start the WebSocket Closing Handshake: "Once an + # endpoint has both sent and received a Close control frame, + # that endpoint SHOULD _Close the WebSocket Connection_" + + # A server closes the TCP connection immediately, while a client + # waits for the server to close the TCP connection. + if self.side is SERVER: + self.send_eof() + + # 1.4. Closing Handshake: "after receiving a control frame + # indicating the connection should be closed, a peer discards + # any further data received." + self.parser = self.discard() + next(self.parser) # start coroutine + + else: + # This can't happen because Frame.parse() validates opcodes. + raise AssertionError(f"unexpected opcode: {frame.opcode:02x}") + + self.events.append(frame) + + # Private methods for sending events. + + def send_frame(self, frame: Frame) -> None: + if self.state is not OPEN: + raise InvalidState( + f"cannot write to a WebSocket in the {self.state.name} state" + ) + + if self.debug: + self.logger.debug("> %s", frame) + self.writes.append( + frame.serialize(mask=self.side is CLIENT, extensions=self.extensions) + ) + + def send_eof(self) -> None: + assert not self.eof_sent + self.eof_sent = True + if self.debug: + self.logger.debug("> EOF") + self.writes.append(SEND_EOF) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py index 5dad50b6a1..191660553f 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py @@ -4,9 +4,9 @@ import base64 import binascii import email.utils import http -from typing import Generator, List, Optional, Sequence, Tuple, cast +import warnings +from typing import Any, Callable, Generator, List, Optional, Sequence, Tuple, cast -from .connection import CONNECTING, OPEN, SERVER, Connection, State from .datastructures import Headers, MultipleValuesError from .exceptions import ( InvalidHandshake, @@ -25,13 +25,14 @@ from .headers import ( parse_subprotocol, parse_upgrade, ) -from .http import USER_AGENT from .http11 import Request, Response +from .protocol import CONNECTING, OPEN, SERVER, Protocol, State from .typing import ( ConnectionOption, ExtensionHeader, LoggerLike, Origin, + StatusLike, Subprotocol, UpgradeProtocol, ) @@ -39,13 +40,15 @@ from .utils import accept_key # See #940 for why lazy_import isn't used here for backwards compatibility. -from .legacy.server import * # isort:skip # noqa +# See #1400 for why listing compatibility imports in __all__ helps PyCharm. +from .legacy.server import * # isort:skip # noqa: I001 +from .legacy.server import __all__ as legacy__all__ -__all__ = ["ServerConnection"] +__all__ = ["ServerProtocol"] + legacy__all__ -class ServerConnection(Connection): +class ServerProtocol(Protocol): """ Sans-I/O implementation of a WebSocket server connection. @@ -58,20 +61,31 @@ class ServerConnection(Connection): should be tried. subprotocols: list of supported subprotocols, in order of decreasing preference. + select_subprotocol: Callback for selecting a subprotocol among + those supported by the client and the server. It has the same + signature as the :meth:`select_subprotocol` method, including a + :class:`ServerProtocol` instance as first argument. state: initial state of the WebSocket connection. max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. + :obj:`None` disables the limit. logger: logger for this connection; defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../topics/logging>` for details. + see the :doc:`logging guide <../../topics/logging>` for details. """ def __init__( self, + *, origins: Optional[Sequence[Optional[Origin]]] = None, extensions: Optional[Sequence[ServerExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, + select_subprotocol: Optional[ + Callable[ + [ServerProtocol, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None, state: State = CONNECTING, max_size: Optional[int] = 2**20, logger: Optional[LoggerLike] = None, @@ -85,6 +99,14 @@ class ServerConnection(Connection): self.origins = origins self.available_extensions = extensions self.available_subprotocols = subprotocols + if select_subprotocol is not None: + # Bind select_subprotocol then shadow self.select_subprotocol. + # Use setattr to work around https://github.com/python/mypy/issues/2427. + setattr( + self, + "select_subprotocol", + select_subprotocol.__get__(self, self.__class__), + ) def accept(self, request: Request) -> Response: """ @@ -95,13 +117,13 @@ class ServerConnection(Connection): You must send the handshake response with :meth:`send_response`. - You can modify it before sending it, for example to add HTTP headers. + You may modify it before sending it, for example to add HTTP headers. Args: request: WebSocket handshake request event received from the client. Returns: - Response: WebSocket handshake response event to send to the client. + WebSocket handshake response event to send to the client. """ try: @@ -145,6 +167,8 @@ class ServerConnection(Connection): f"Failed to open a WebSocket connection: {exc}.\n", ) except Exception as exc: + # Handle exceptions raised by user-provided select_subprotocol and + # unexpected errors. request._exception = exc self.handshake_exc = exc self.logger.error("opening handshake failed", exc_info=True) @@ -170,13 +194,12 @@ class ServerConnection(Connection): if protocol_header is not None: headers["Sec-WebSocket-Protocol"] = protocol_header - headers["Server"] = USER_AGENT - self.logger.info("connection open") return Response(101, "Switching Protocols", headers) def process_request( - self, request: Request + self, + request: Request, ) -> Tuple[str, Optional[str], Optional[str]]: """ Check a handshake request and negotiate extensions and subprotocol. @@ -274,6 +297,7 @@ class ServerConnection(Connection): Optional[Origin]: origin, if it is acceptable. Raises: + InvalidHandshake: if the Origin header is invalid. InvalidOrigin: if the origin isn't acceptable. """ @@ -298,8 +322,8 @@ class ServerConnection(Connection): Accept or reject each extension proposed in the client request. Negotiate parameters for accepted extensions. - :rfc:`6455` leaves the rules up to the specification of each - :extension. + Per :rfc:`6455`, negotiation rules are defined by the specification of + each extension. To provide this level of flexibility, for each extension proposed by the client, we check for a match with each extension available in the @@ -324,7 +348,7 @@ class ServerConnection(Connection): HTTP response header and list of accepted extensions. Raises: - InvalidHandshake: to abort the handshake with an HTTP 400 error. + InvalidHandshake: if the Sec-WebSocket-Extensions header is invalid. """ response_header_value: Optional[str] = None @@ -335,15 +359,12 @@ class ServerConnection(Connection): header_values = headers.get_all("Sec-WebSocket-Extensions") if header_values and self.available_extensions: - parsed_header_values: List[ExtensionHeader] = sum( [parse_extension(header_value) for header_value in header_values], [] ) for name, request_params in parsed_header_values: - for ext_factory in self.available_extensions: - # Skip non-matching extensions based on their name. if ext_factory.name != name: continue @@ -384,64 +405,83 @@ class ServerConnection(Connection): also the value of the ``Sec-WebSocket-Protocol`` response header. Raises: - InvalidHandshake: to abort the handshake with an HTTP 400 error. + InvalidHandshake: if the Sec-WebSocket-Subprotocol header is invalid. """ - subprotocol: Optional[Subprotocol] = None - - header_values = headers.get_all("Sec-WebSocket-Protocol") - - if header_values and self.available_subprotocols: - - parsed_header_values: List[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in header_values], [] - ) - - subprotocol = self.select_subprotocol( - parsed_header_values, self.available_subprotocols - ) + subprotocols: Sequence[Subprotocol] = sum( + [ + parse_subprotocol(header_value) + for header_value in headers.get_all("Sec-WebSocket-Protocol") + ], + [], + ) - return subprotocol + return self.select_subprotocol(subprotocols) def select_subprotocol( self, - client_subprotocols: Sequence[Subprotocol], - server_subprotocols: Sequence[Subprotocol], + subprotocols: Sequence[Subprotocol], ) -> Optional[Subprotocol]: """ Pick a subprotocol among those offered by the client. - If several subprotocols are supported by the client and the server, - the default implementation selects the preferred subprotocols by - giving equal value to the priorities of the client and the server. + If several subprotocols are supported by both the client and the server, + pick the first one in the list declared the server. + + If the server doesn't support any subprotocols, continue without a + subprotocol, regardless of what the client offers. + + If the server supports at least one subprotocol and the client doesn't + offer any, abort the handshake with an HTTP 400 error. - If no common subprotocol is supported by the client and the server, it - proceeds without a subprotocol. + You provide a ``select_subprotocol`` argument to :class:`ServerProtocol` + to override this logic. For example, you could accept the connection + even if client doesn't offer a subprotocol, rather than reject it. - This is unlikely to be the most useful implementation in practice, as - many servers providing a subprotocol will require that the client uses - that subprotocol. + Here's how to negotiate the ``chat`` subprotocol if the client supports + it and continue without a subprotocol otherwise:: + + def select_subprotocol(protocol, subprotocols): + if "chat" in subprotocols: + return "chat" Args: - client_subprotocols: list of subprotocols offered by the client. - server_subprotocols: list of subprotocols available on the server. + subprotocols: list of subprotocols offered by the client. Returns: - Optional[Subprotocol]: Subprotocol, if a common subprotocol was - found. + Optional[Subprotocol]: Selected subprotocol, if a common subprotocol + was found. + + :obj:`None` to continue without a subprotocol. + + Raises: + NegotiationError: custom implementations may raise this exception + to abort the handshake with an HTTP 400 error. """ - subprotocols = set(client_subprotocols) & set(server_subprotocols) - if not subprotocols: + # Server doesn't offer any subprotocols. + if not self.available_subprotocols: # None or empty list return None - priority = lambda p: ( - client_subprotocols.index(p) + server_subprotocols.index(p) + + # Server offers at least one subprotocol but client doesn't offer any. + if not subprotocols: + raise NegotiationError("missing subprotocol") + + # Server and client both offer subprotocols. Look for a shared one. + proposed_subprotocols = set(subprotocols) + for subprotocol in self.available_subprotocols: + if subprotocol in proposed_subprotocols: + return subprotocol + + # No common subprotocol was found. + raise NegotiationError( + "invalid subprotocol; expected one of " + + ", ".join(self.available_subprotocols) ) - return sorted(subprotocols, key=priority)[0] def reject( self, - status: http.HTTPStatus, + status: StatusLike, text: str, ) -> Response: """ @@ -462,6 +502,8 @@ class ServerConnection(Connection): Response: WebSocket handshake response event to send to the client. """ + # If a user passes an int instead of a HTTPStatus, fix it automatically. + status = http.HTTPStatus(status) body = text.encode() headers = Headers( [ @@ -469,16 +511,15 @@ class ServerConnection(Connection): ("Connection", "close"), ("Content-Length", str(len(body))), ("Content-Type", "text/plain; charset=utf-8"), - ("Server", USER_AGENT), ] ) response = Response(status.value, status.phrase, headers, body) # When reject() is called from accept(), handshake_exc is already set. # If a user calls reject(), set handshake_exc to guarantee invariant: - # "handshake_exc is None if and only if opening handshake succeded." + # "handshake_exc is None if and only if opening handshake succeeded." if self.handshake_exc is None: self.handshake_exc = InvalidStatus(response) - self.logger.info("connection failed (%d %s)", status.value, status.phrase) + self.logger.info("connection rejected (%d %s)", status.value, status.phrase) return response def send_response(self, response: Response) -> None: @@ -509,7 +550,16 @@ class ServerConnection(Connection): def parse(self) -> Generator[None, None, None]: if self.state is CONNECTING: - request = yield from Request.parse(self.reader.read_line) + try: + request = yield from Request.parse( + self.reader.read_line, + ) + except Exception as exc: + self.handshake_exc = exc + self.send_eof() + self.parser = self.discard() + next(self.parser) # start coroutine + yield if self.debug: self.logger.debug("< GET %s HTTP/1.1", request.path) @@ -519,3 +569,12 @@ class ServerConnection(Connection): self.events.append(request) yield from super().parse() + + +class ServerConnection(ServerProtocol): + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "ServerConnection was renamed to ServerProtocol", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi new file mode 100644 index 0000000000..821438a064 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi @@ -0,0 +1 @@ +def apply_mask(data: bytes, mask: bytes) -> bytes: ... diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py new file mode 100644 index 0000000000..087ff5f569 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +import socket +import ssl +import threading +from typing import Any, Optional, Sequence, Type + +from ..client import ClientProtocol +from ..datastructures import HeadersLike +from ..extensions.base import ClientExtensionFactory +from ..extensions.permessage_deflate import enable_client_permessage_deflate +from ..headers import validate_subprotocols +from ..http import USER_AGENT +from ..http11 import Response +from ..protocol import CONNECTING, OPEN, Event +from ..typing import LoggerLike, Origin, Subprotocol +from ..uri import parse_uri +from .connection import Connection +from .utils import Deadline + + +__all__ = ["connect", "unix_connect", "ClientConnection"] + + +class ClientConnection(Connection): + """ + Threaded implementation of a WebSocket client connection. + + :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for + receiving and sending messages. + + It supports iteration to receive messages:: + + for message in websocket: + process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is + closed with any other code. + + Args: + socket: Socket connected to a WebSocket server. + protocol: Sans-I/O connection. + close_timeout: Timeout for closing the connection in seconds. + + """ + + def __init__( + self, + socket: socket.socket, + protocol: ClientProtocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.protocol: ClientProtocol + self.response_rcvd = threading.Event() + super().__init__( + socket, + protocol, + close_timeout=close_timeout, + ) + + def handshake( + self, + additional_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + timeout: Optional[float] = None, + ) -> None: + """ + Perform the opening handshake. + + """ + with self.send_context(expected_state=CONNECTING): + self.request = self.protocol.connect() + if additional_headers is not None: + self.request.headers.update(additional_headers) + if user_agent_header is not None: + self.request.headers["User-Agent"] = user_agent_header + self.protocol.send_request(self.request) + + if not self.response_rcvd.wait(timeout): + self.close_socket() + self.recv_events_thread.join() + raise TimeoutError("timed out during handshake") + + if self.response is None: + self.close_socket() + self.recv_events_thread.join() + raise ConnectionError("connection closed during handshake") + + if self.protocol.state is not OPEN: + self.recv_events_thread.join(self.close_timeout) + self.close_socket() + self.recv_events_thread.join() + + if self.protocol.handshake_exc is not None: + raise self.protocol.handshake_exc + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + """ + # First event - handshake response. + if self.response is None: + assert isinstance(event, Response) + self.response = event + self.response_rcvd.set() + # Later events - frames. + else: + super().process_event(event) + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + """ + try: + super().recv_events() + finally: + # If the connection is closed during the handshake, unblock it. + self.response_rcvd.set() + + +def connect( + uri: str, + *, + # TCP/TLS — unix and path are only for unix_connect() + sock: Optional[socket.socket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + server_hostname: Optional[str] = None, + unix: bool = False, + path: Optional[str] = None, + # WebSocket + origin: Optional[Origin] = None, + extensions: Optional[Sequence[ClientExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + additional_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + compression: Optional[str] = "deflate", + # Timeouts + open_timeout: Optional[float] = 10, + close_timeout: Optional[float] = 10, + # Limits + max_size: Optional[int] = 2**20, + # Logging + logger: Optional[LoggerLike] = None, + # Escape hatch for advanced customization + create_connection: Optional[Type[ClientConnection]] = None, +) -> ClientConnection: + """ + Connect to the WebSocket server at ``uri``. + + This function returns a :class:`ClientConnection` instance, which you can + use to send and receive messages. + + :func:`connect` may be used as a context manager:: + + async with websockets.sync.client.connect(...) as websocket: + ... + + The connection is closed automatically when exiting the context. + + Args: + uri: URI of the WebSocket server. + sock: Preexisting TCP socket. ``sock`` overrides the host and port + from ``uri``. You may call :func:`socket.create_connection` to + create a suitable TCP socket. + ssl_context: Configuration for enabling TLS on the connection. + server_hostname: Host name for the TLS handshake. ``server_hostname`` + overrides the host name from ``uri``. + origin: Value of the ``Origin`` header, for servers that require it. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + additional_headers (HeadersLike | None): Arbitrary HTTP headers to add + to the handshake request. + user_agent_header: Value of the ``User-Agent`` request header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + open_timeout: Timeout for opening the connection in seconds. + :obj:`None` disables the timeout. + close_timeout: Timeout for closing the connection in seconds. + :obj:`None` disables the timeout. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + logger: Logger for this client. + It defaults to ``logging.getLogger("websockets.client")``. + See the :doc:`logging guide <../../topics/logging>` for details. + create_connection: Factory for the :class:`ClientConnection` managing + the connection. Set it to a wrapper or a subclass to customize + connection handling. + + Raises: + InvalidURI: If ``uri`` isn't a valid WebSocket URI. + OSError: If the TCP connection fails. + InvalidHandshake: If the opening handshake fails. + TimeoutError: If the opening handshake times out. + + """ + + # Process parameters + + wsuri = parse_uri(uri) + if not wsuri.secure and ssl_context is not None: + raise TypeError("ssl_context argument is incompatible with a ws:// URI") + + if unix: + if path is None and sock is None: + raise TypeError("missing path argument") + elif path is not None and sock is not None: + raise TypeError("path and sock arguments are incompatible") + else: + assert path is None # private argument, only set by unix_connect() + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + if compression == "deflate": + extensions = enable_client_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + # Calculate timeouts on the TCP, TLS, and WebSocket handshakes. + # The TCP and TLS timeouts must be set on the socket, then removed + # to avoid conflicting with the WebSocket timeout in handshake(). + deadline = Deadline(open_timeout) + + if create_connection is None: + create_connection = ClientConnection + + try: + # Connect socket + + if sock is None: + if unix: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(deadline.timeout()) + assert path is not None # validated above -- this is for mpypy + sock.connect(path) + else: + sock = socket.create_connection( + (wsuri.host, wsuri.port), + deadline.timeout(), + ) + sock.settimeout(None) + + # Disable Nagle algorithm + + if not unix: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + + # Initialize TLS wrapper and perform TLS handshake + + if wsuri.secure: + if ssl_context is None: + ssl_context = ssl.create_default_context() + if server_hostname is None: + server_hostname = wsuri.host + sock.settimeout(deadline.timeout()) + sock = ssl_context.wrap_socket(sock, server_hostname=server_hostname) + sock.settimeout(None) + + # Initialize WebSocket connection + + protocol = ClientProtocol( + wsuri, + origin=origin, + extensions=extensions, + subprotocols=subprotocols, + state=CONNECTING, + max_size=max_size, + logger=logger, + ) + + # Initialize WebSocket protocol + + connection = create_connection( + sock, + protocol, + close_timeout=close_timeout, + ) + # On failure, handshake() closes the socket and raises an exception. + connection.handshake( + additional_headers, + user_agent_header, + deadline.timeout(), + ) + + except Exception: + if sock is not None: + sock.close() + raise + + return connection + + +def unix_connect( + path: Optional[str] = None, + uri: Optional[str] = None, + **kwargs: Any, +) -> ClientConnection: + """ + Connect to a WebSocket server listening on a Unix socket. + + This function is identical to :func:`connect`, except for the additional + ``path`` argument. It's only available on Unix. + + It's mainly useful for debugging servers listening on Unix sockets. + + Args: + path: File system path to the Unix socket. + uri: URI of the WebSocket server. ``uri`` defaults to + ``ws://localhost/`` or, when a ``ssl_context`` is provided, to + ``wss://localhost/``. + + """ + if uri is None: + if kwargs.get("ssl_context") is None: + uri = "ws://localhost/" + else: + uri = "wss://localhost/" + return connect(uri=uri, unix=True, path=path, **kwargs) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py new file mode 100644 index 0000000000..4a8879e370 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py @@ -0,0 +1,773 @@ +from __future__ import annotations + +import contextlib +import logging +import random +import socket +import struct +import threading +import uuid +from types import TracebackType +from typing import Any, Dict, Iterable, Iterator, Mapping, Optional, Type, Union + +from ..exceptions import ConnectionClosed, ConnectionClosedOK, ProtocolError +from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode, prepare_ctrl +from ..http11 import Request, Response +from ..protocol import CLOSED, OPEN, Event, Protocol, State +from ..typing import Data, LoggerLike, Subprotocol +from .messages import Assembler +from .utils import Deadline + + +__all__ = ["Connection"] + +logger = logging.getLogger(__name__) + + +class Connection: + """ + Threaded implementation of a WebSocket connection. + + :class:`Connection` provides APIs shared between WebSocket servers and + clients. + + You shouldn't use it directly. Instead, use + :class:`~websockets.sync.client.ClientConnection` or + :class:`~websockets.sync.server.ServerConnection`. + + """ + + recv_bufsize = 65536 + + def __init__( + self, + socket: socket.socket, + protocol: Protocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.socket = socket + self.protocol = protocol + self.close_timeout = close_timeout + + # Inject reference to this instance in the protocol's logger. + self.protocol.logger = logging.LoggerAdapter( + self.protocol.logger, + {"websocket": self}, + ) + + # Copy attributes from the protocol for convenience. + self.id: uuid.UUID = self.protocol.id + """Unique identifier of the connection. Useful in logs.""" + self.logger: LoggerLike = self.protocol.logger + """Logger for this connection.""" + self.debug = self.protocol.debug + + # HTTP handshake request and response. + self.request: Optional[Request] = None + """Opening handshake request.""" + self.response: Optional[Response] = None + """Opening handshake response.""" + + # Mutex serializing interactions with the protocol. + self.protocol_mutex = threading.Lock() + + # Assembler turning frames into messages and serializing reads. + self.recv_messages = Assembler() + + # Whether we are busy sending a fragmented message. + self.send_in_progress = False + + # Deadline for the closing handshake. + self.close_deadline: Optional[Deadline] = None + + # Mapping of ping IDs to pong waiters, in chronological order. + self.pings: Dict[bytes, threading.Event] = {} + + # Receiving events from the socket. + self.recv_events_thread = threading.Thread(target=self.recv_events) + self.recv_events_thread.start() + + # Exception raised in recv_events, to be chained to ConnectionClosed + # in the user thread in order to show why the TCP connection dropped. + self.recv_events_exc: Optional[BaseException] = None + + # Public attributes + + @property + def local_address(self) -> Any: + """ + Local address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family. + See :meth:`~socket.socket.getsockname`. + + """ + return self.socket.getsockname() + + @property + def remote_address(self) -> Any: + """ + Remote address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family. + See :meth:`~socket.socket.getpeername`. + + """ + return self.socket.getpeername() + + @property + def subprotocol(self) -> Optional[Subprotocol]: + """ + Subprotocol negotiated during the opening handshake. + + :obj:`None` if no subprotocol was negotiated. + + """ + return self.protocol.subprotocol + + # Public methods + + def __enter__(self) -> Connection: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + if exc_type is None: + self.close() + else: + self.close(CloseCode.INTERNAL_ERROR) + + def __iter__(self) -> Iterator[Data]: + """ + Iterate on incoming messages. + + The iterator calls :meth:`recv` and yields messages in an infinite loop. + + It exits when the connection is closed normally. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` exception after a + protocol error or a network failure. + + """ + try: + while True: + yield self.recv() + except ConnectionClosedOK: + return + + def recv(self, timeout: Optional[float] = None) -> Data: + """ + Receive the next message. + + When the connection is closed, :meth:`recv` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises + :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure + and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. This is how you detect the end of the + message stream. + + If ``timeout`` is :obj:`None`, block until a message is received. If + ``timeout`` is set and no message is received within ``timeout`` + seconds, raise :exc:`TimeoutError`. Set ``timeout`` to ``0`` to check if + a message was already received. + + If the message is fragmented, wait until all fragments are received, + reassemble them, and return the whole message. + + Returns: + A string (:class:`str`) for a Text_ frame or a bytestring + (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If two threads call :meth:`recv` or + :meth:`recv_streaming` concurrently. + + """ + try: + return self.recv_messages.get(timeout) + except EOFError: + raise self.protocol.close_exc from self.recv_events_exc + except RuntimeError: + raise RuntimeError( + "cannot call recv while another thread " + "is already running recv or recv_streaming" + ) from None + + def recv_streaming(self) -> Iterator[Data]: + """ + Receive the next message frame by frame. + + If the message is fragmented, yield each fragment as it is received. + The iterator must be fully consumed, or else the connection will become + unusable. + + :meth:`recv_streaming` raises the same exceptions as :meth:`recv`. + + Returns: + An iterator of strings (:class:`str`) for a Text_ frame or + bytestrings (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If two threads call :meth:`recv` or + :meth:`recv_streaming` concurrently. + + """ + try: + yield from self.recv_messages.get_iter() + except EOFError: + raise self.protocol.close_exc from self.recv_events_exc + except RuntimeError: + raise RuntimeError( + "cannot call recv_streaming while another thread " + "is already running recv or recv_streaming" + ) from None + + def send(self, message: Union[Data, Iterable[Data]]) -> None: + """ + Send a message. + + A string (:class:`str`) is sent as a Text_ frame. A bytestring or + bytes-like object (:class:`bytes`, :class:`bytearray`, or + :class:`memoryview`) is sent as a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + :meth:`send` also accepts an iterable of strings, bytestrings, or + bytes-like objects to enable fragmentation_. Each item is treated as a + message fragment and sent in its own frame. All items must be of the + same type, or else :meth:`send` will raise a :exc:`TypeError` and the + connection will be closed. + + .. _fragmentation: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4 + + :meth:`send` rejects dict-like objects because this is often an error. + (If you really want to send the keys of a dict-like object as fragments, + call its :meth:`~dict.keys` method and pass the result to :meth:`send`.) + + When the connection is closed, :meth:`send` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it + raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + connection closure and + :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. + + Args: + message: Message to send. + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If a connection is busy sending a fragmented message. + TypeError: If ``message`` doesn't have a supported type. + + """ + # Unfragmented message -- this case must be handled first because + # strings and bytes-like objects are iterable. + + if isinstance(message, str): + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.protocol.send_text(message.encode("utf-8")) + + elif isinstance(message, BytesLike): + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.protocol.send_binary(message) + + # Catch a common mistake -- passing a dict to send(). + + elif isinstance(message, Mapping): + raise TypeError("data is a dict-like object") + + # Fragmented message -- regular iterator. + + elif isinstance(message, Iterable): + chunks = iter(message) + try: + chunk = next(chunks) + except StopIteration: + return + + try: + # First fragment. + if isinstance(chunk, str): + text = True + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.send_in_progress = True + self.protocol.send_text( + chunk.encode("utf-8"), + fin=False, + ) + elif isinstance(chunk, BytesLike): + text = False + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.send_in_progress = True + self.protocol.send_binary( + chunk, + fin=False, + ) + else: + raise TypeError("data iterable must contain bytes or str") + + # Other fragments + for chunk in chunks: + if isinstance(chunk, str) and text: + with self.send_context(): + assert self.send_in_progress + self.protocol.send_continuation( + chunk.encode("utf-8"), + fin=False, + ) + elif isinstance(chunk, BytesLike) and not text: + with self.send_context(): + assert self.send_in_progress + self.protocol.send_continuation( + chunk, + fin=False, + ) + else: + raise TypeError("data iterable must contain uniform types") + + # Final fragment. + with self.send_context(): + self.protocol.send_continuation(b"", fin=True) + self.send_in_progress = False + + except RuntimeError: + # We didn't start sending a fragmented message. + raise + + except Exception: + # We're half-way through a fragmented message and we can't + # complete it. This makes the connection unusable. + with self.send_context(): + self.protocol.fail( + CloseCode.INTERNAL_ERROR, + "error in fragmented message", + ) + raise + + else: + raise TypeError("data must be bytes, str, or iterable") + + def close(self, code: int = CloseCode.NORMAL_CLOSURE, reason: str = "") -> None: + """ + Perform the closing handshake. + + :meth:`close` waits for the other end to complete the handshake, for the + TCP connection to terminate, and for all incoming messages to be read + with :meth:`recv`. + + :meth:`close` is idempotent: it doesn't do anything once the + connection is closed. + + Args: + code: WebSocket close code. + reason: WebSocket close reason. + + """ + try: + # The context manager takes care of waiting for the TCP connection + # to terminate after calling a method that sends a close frame. + with self.send_context(): + if self.send_in_progress: + self.protocol.fail( + CloseCode.INTERNAL_ERROR, + "close during fragmented message", + ) + else: + self.protocol.send_close(code, reason) + except ConnectionClosed: + # Ignore ConnectionClosed exceptions raised from send_context(). + # They mean that the connection is closed, which was the goal. + pass + + def ping(self, data: Optional[Data] = None) -> threading.Event: + """ + Send a Ping_. + + .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 + + A ping may serve as a keepalive or as a check that the remote endpoint + received all messages up to this point + + Args: + data: Payload of the ping. A :class:`str` will be encoded to UTF-8. + If ``data`` is :obj:`None`, the payload is four random bytes. + + Returns: + An event that will be set when the corresponding pong is received. + You can ignore it if you don't intend to wait. + + :: + + pong_event = ws.ping() + pong_event.wait() # only if you want to wait for the pong + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If another ping was sent with the same data and + the corresponding pong wasn't received yet. + + """ + if data is not None: + data = prepare_ctrl(data) + + with self.send_context(): + # Protect against duplicates if a payload is explicitly set. + if data in self.pings: + raise RuntimeError("already waiting for a pong with the same data") + + # Generate a unique random payload otherwise. + while data is None or data in self.pings: + data = struct.pack("!I", random.getrandbits(32)) + + pong_waiter = threading.Event() + self.pings[data] = pong_waiter + self.protocol.send_ping(data) + return pong_waiter + + def pong(self, data: Data = b"") -> None: + """ + Send a Pong_. + + .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + + An unsolicited pong may serve as a unidirectional heartbeat. + + Args: + data: Payload of the pong. A :class:`str` will be encoded to UTF-8. + + Raises: + ConnectionClosed: When the connection is closed. + + """ + data = prepare_ctrl(data) + + with self.send_context(): + self.protocol.send_pong(data) + + # Private methods + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + This method is overridden in subclasses to handle the handshake. + + """ + assert isinstance(event, Frame) + if event.opcode in DATA_OPCODES: + self.recv_messages.put(event) + + if event.opcode is Opcode.PONG: + self.acknowledge_pings(bytes(event.data)) + + def acknowledge_pings(self, data: bytes) -> None: + """ + Acknowledge pings when receiving a pong. + + """ + with self.protocol_mutex: + # Ignore unsolicited pong. + if data not in self.pings: + return + # Sending a pong for only the most recent ping is legal. + # Acknowledge all previous pings too in that case. + ping_id = None + ping_ids = [] + for ping_id, ping in self.pings.items(): + ping_ids.append(ping_id) + ping.set() + if ping_id == data: + break + else: + raise AssertionError("solicited pong not found in pings") + # Remove acknowledged pings from self.pings. + for ping_id in ping_ids: + del self.pings[ping_id] + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + Run this method in a thread as long as the connection is alive. + + ``recv_events()`` exits immediately when the ``self.socket`` is closed. + + """ + try: + while True: + try: + if self.close_deadline is not None: + self.socket.settimeout(self.close_deadline.timeout()) + data = self.socket.recv(self.recv_bufsize) + except Exception as exc: + if self.debug: + self.logger.debug("error while receiving data", exc_info=True) + # When the closing handshake is initiated by our side, + # recv() may block until send_context() closes the socket. + # In that case, send_context() already set recv_events_exc. + # Calling set_recv_events_exc() avoids overwriting it. + with self.protocol_mutex: + self.set_recv_events_exc(exc) + break + + if data == b"": + break + + # Acquire the connection lock. + with self.protocol_mutex: + # Feed incoming data to the connection. + self.protocol.receive_data(data) + + # This isn't expected to raise an exception. + events = self.protocol.events_received() + + # Write outgoing data to the socket. + try: + self.send_data() + except Exception as exc: + if self.debug: + self.logger.debug("error while sending data", exc_info=True) + # Similarly to the above, avoid overriding an exception + # set by send_context(), in case of a race condition + # i.e. send_context() closes the socket after recv() + # returns above but before send_data() calls send(). + self.set_recv_events_exc(exc) + break + + if self.protocol.close_expected(): + # If the connection is expected to close soon, set the + # close deadline based on the close timeout. + if self.close_deadline is None: + self.close_deadline = Deadline(self.close_timeout) + + # Unlock conn_mutex before processing events. Else, the + # application can't send messages in response to events. + + # If self.send_data raised an exception, then events are lost. + # Given that automatic responses write small amounts of data, + # this should be uncommon, so we don't handle the edge case. + + try: + for event in events: + # This may raise EOFError if the closing handshake + # times out while a message is waiting to be read. + self.process_event(event) + except EOFError: + break + + # Breaking out of the while True: ... loop means that we believe + # that the socket doesn't work anymore. + with self.protocol_mutex: + # Feed the end of the data stream to the connection. + self.protocol.receive_eof() + + # This isn't expected to generate events. + assert not self.protocol.events_received() + + # There is no error handling because send_data() can only write + # the end of the data stream here and it handles errors itself. + self.send_data() + + except Exception as exc: + # This branch should never run. It's a safety net in case of bugs. + self.logger.error("unexpected internal error", exc_info=True) + with self.protocol_mutex: + self.set_recv_events_exc(exc) + # We don't know where we crashed. Force protocol state to CLOSED. + self.protocol.state = CLOSED + finally: + # This isn't expected to raise an exception. + self.close_socket() + + @contextlib.contextmanager + def send_context( + self, + *, + expected_state: State = OPEN, # CONNECTING during the opening handshake + ) -> Iterator[None]: + """ + Create a context for writing to the connection from user code. + + On entry, :meth:`send_context` acquires the connection lock and checks + that the connection is open; on exit, it writes outgoing data to the + socket:: + + with self.send_context(): + self.protocol.send_text(message.encode("utf-8")) + + When the connection isn't open on entry, when the connection is expected + to close on exit, or when an unexpected error happens, terminating the + connection, :meth:`send_context` waits until the connection is closed + then raises :exc:`~websockets.exceptions.ConnectionClosed`. + + """ + # Should we wait until the connection is closed? + wait_for_close = False + # Should we close the socket and raise ConnectionClosed? + raise_close_exc = False + # What exception should we chain ConnectionClosed to? + original_exc: Optional[BaseException] = None + + # Acquire the protocol lock. + with self.protocol_mutex: + if self.protocol.state is expected_state: + # Let the caller interact with the protocol. + try: + yield + except (ProtocolError, RuntimeError): + # The protocol state wasn't changed. Exit immediately. + raise + except Exception as exc: + self.logger.error("unexpected internal error", exc_info=True) + # This branch should never run. It's a safety net in case of + # bugs. Since we don't know what happened, we will close the + # connection and raise the exception to the caller. + wait_for_close = False + raise_close_exc = True + original_exc = exc + else: + # Check if the connection is expected to close soon. + if self.protocol.close_expected(): + wait_for_close = True + # If the connection is expected to close soon, set the + # close deadline based on the close timeout. + + # Since we tested earlier that protocol.state was OPEN + # (or CONNECTING) and we didn't release protocol_mutex, + # it is certain that self.close_deadline is still None. + assert self.close_deadline is None + self.close_deadline = Deadline(self.close_timeout) + # Write outgoing data to the socket. + try: + self.send_data() + except Exception as exc: + if self.debug: + self.logger.debug("error while sending data", exc_info=True) + # While the only expected exception here is OSError, + # other exceptions would be treated identically. + wait_for_close = False + raise_close_exc = True + original_exc = exc + + else: # self.protocol.state is not expected_state + # Minor layering violation: we assume that the connection + # will be closing soon if it isn't in the expected state. + wait_for_close = True + raise_close_exc = True + + # To avoid a deadlock, release the connection lock by exiting the + # context manager before waiting for recv_events() to terminate. + + # If the connection is expected to close soon and the close timeout + # elapses, close the socket to terminate the connection. + if wait_for_close: + if self.close_deadline is None: + timeout = self.close_timeout + else: + # Thread.join() returns immediately if timeout is negative. + timeout = self.close_deadline.timeout(raise_if_elapsed=False) + self.recv_events_thread.join(timeout) + + if self.recv_events_thread.is_alive(): + # There's no risk to overwrite another error because + # original_exc is never set when wait_for_close is True. + assert original_exc is None + original_exc = TimeoutError("timed out while closing connection") + # Set recv_events_exc before closing the socket in order to get + # proper exception reporting. + raise_close_exc = True + with self.protocol_mutex: + self.set_recv_events_exc(original_exc) + + # If an error occurred, close the socket to terminate the connection and + # raise an exception. + if raise_close_exc: + self.close_socket() + self.recv_events_thread.join() + raise self.protocol.close_exc from original_exc + + def send_data(self) -> None: + """ + Send outgoing data. + + This method requires holding protocol_mutex. + + Raises: + OSError: When a socket operations fails. + + """ + assert self.protocol_mutex.locked() + for data in self.protocol.data_to_send(): + if data: + if self.close_deadline is not None: + self.socket.settimeout(self.close_deadline.timeout()) + self.socket.sendall(data) + else: + try: + self.socket.shutdown(socket.SHUT_WR) + except OSError: # socket already closed + pass + + def set_recv_events_exc(self, exc: Optional[BaseException]) -> None: + """ + Set recv_events_exc, if not set yet. + + This method requires holding protocol_mutex. + + """ + assert self.protocol_mutex.locked() + if self.recv_events_exc is None: + self.recv_events_exc = exc + + def close_socket(self) -> None: + """ + Shutdown and close socket. Close message assembler. + + Calling close_socket() guarantees that recv_events() terminates. Indeed, + recv_events() may block only on socket.recv() or on recv_messages.put(). + + """ + # shutdown() is required to interrupt recv() on Linux. + try: + self.socket.shutdown(socket.SHUT_RDWR) + except OSError: + pass # socket is already closed + self.socket.close() + self.recv_messages.close() diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py new file mode 100644 index 0000000000..67a22313ca --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import codecs +import queue +import threading +from typing import Iterator, List, Optional, cast + +from ..frames import Frame, Opcode +from ..typing import Data + + +__all__ = ["Assembler"] + +UTF8Decoder = codecs.getincrementaldecoder("utf-8") + + +class Assembler: + """ + Assemble messages from frames. + + """ + + def __init__(self) -> None: + # Serialize reads and writes -- except for reads via synchronization + # primitives provided by the threading and queue modules. + self.mutex = threading.Lock() + + # We create a latch with two events to ensure proper interleaving of + # writing and reading messages. + # put() sets this event to tell get() that a message can be fetched. + self.message_complete = threading.Event() + # get() sets this event to let put() that the message was fetched. + self.message_fetched = threading.Event() + + # This flag prevents concurrent calls to get() by user code. + self.get_in_progress = False + # This flag prevents concurrent calls to put() by library code. + self.put_in_progress = False + + # Decoder for text frames, None for binary frames. + self.decoder: Optional[codecs.IncrementalDecoder] = None + + # Buffer of frames belonging to the same message. + self.chunks: List[Data] = [] + + # When switching from "buffering" to "streaming", we use a thread-safe + # queue for transferring frames from the writing thread (library code) + # to the reading thread (user code). We're buffering when chunks_queue + # is None and streaming when it's a SimpleQueue. None is a sentinel + # value marking the end of the stream, superseding message_complete. + + # Stream data from frames belonging to the same message. + # Remove quotes around type when dropping Python < 3.9. + self.chunks_queue: Optional["queue.SimpleQueue[Optional[Data]]"] = None + + # This flag marks the end of the stream. + self.closed = False + + def get(self, timeout: Optional[float] = None) -> Data: + """ + Read the next message. + + :meth:`get` returns a single :class:`str` or :class:`bytes`. + + If the message is fragmented, :meth:`get` waits until the last frame is + received, then it reassembles the message and returns it. To receive + messages frame by frame, use :meth:`get_iter` instead. + + Args: + timeout: If a timeout is provided and elapses before a complete + message is received, :meth:`get` raises :exc:`TimeoutError`. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`get` or :meth:``get_iter` + concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.get_in_progress: + raise RuntimeError("get or get_iter is already running") + + self.get_in_progress = True + + # If the message_complete event isn't set yet, release the lock to + # allow put() to run and eventually set it. + # Locking with get_in_progress ensures only one thread can get here. + completed = self.message_complete.wait(timeout) + + with self.mutex: + self.get_in_progress = False + + # Waiting for a complete message timed out. + if not completed: + raise TimeoutError(f"timed out in {timeout:.1f}s") + + # get() was unblocked by close() rather than put(). + if self.closed: + raise EOFError("stream of frames ended") + + assert self.message_complete.is_set() + self.message_complete.clear() + + joiner: Data = b"" if self.decoder is None else "" + # mypy cannot figure out that chunks have the proper type. + message: Data = joiner.join(self.chunks) # type: ignore + + assert not self.message_fetched.is_set() + self.message_fetched.set() + + self.chunks = [] + assert self.chunks_queue is None + + return message + + def get_iter(self) -> Iterator[Data]: + """ + Stream the next message. + + Iterating the return value of :meth:`get_iter` yields a :class:`str` or + :class:`bytes` for each frame in the message. + + The iterator must be fully consumed before calling :meth:`get_iter` or + :meth:`get` again. Else, :exc:`RuntimeError` is raised. + + This method only makes sense for fragmented messages. If messages aren't + fragmented, use :meth:`get` instead. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`get` or :meth:``get_iter` + concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.get_in_progress: + raise RuntimeError("get or get_iter is already running") + + chunks = self.chunks + self.chunks = [] + self.chunks_queue = cast( + # Remove quotes around type when dropping Python < 3.9. + "queue.SimpleQueue[Optional[Data]]", + queue.SimpleQueue(), + ) + + # Sending None in chunk_queue supersedes setting message_complete + # when switching to "streaming". If message is already complete + # when the switch happens, put() didn't send None, so we have to. + if self.message_complete.is_set(): + self.chunks_queue.put(None) + + self.get_in_progress = True + + # Locking with get_in_progress ensures only one thread can get here. + yield from chunks + while True: + chunk = self.chunks_queue.get() + if chunk is None: + break + yield chunk + + with self.mutex: + self.get_in_progress = False + + assert self.message_complete.is_set() + self.message_complete.clear() + + # get_iter() was unblocked by close() rather than put(). + if self.closed: + raise EOFError("stream of frames ended") + + assert not self.message_fetched.is_set() + self.message_fetched.set() + + assert self.chunks == [] + self.chunks_queue = None + + def put(self, frame: Frame) -> None: + """ + Add ``frame`` to the next message. + + When ``frame`` is the final frame in a message, :meth:`put` waits until + the message is fetched, either by calling :meth:`get` or by fully + consuming the return value of :meth:`get_iter`. + + :meth:`put` assumes that the stream of frames respects the protocol. If + it doesn't, the behavior is undefined. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`put` concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.put_in_progress: + raise RuntimeError("put is already running") + + if frame.opcode is Opcode.TEXT: + self.decoder = UTF8Decoder(errors="strict") + elif frame.opcode is Opcode.BINARY: + self.decoder = None + elif frame.opcode is Opcode.CONT: + pass + else: + # Ignore control frames. + return + + data: Data + if self.decoder is not None: + data = self.decoder.decode(frame.data, frame.fin) + else: + data = frame.data + + if self.chunks_queue is None: + self.chunks.append(data) + else: + self.chunks_queue.put(data) + + if not frame.fin: + return + + # Message is complete. Wait until it's fetched to return. + + assert not self.message_complete.is_set() + self.message_complete.set() + + if self.chunks_queue is not None: + self.chunks_queue.put(None) + + assert not self.message_fetched.is_set() + + self.put_in_progress = True + + # Release the lock to allow get() to run and eventually set the event. + self.message_fetched.wait() + + with self.mutex: + self.put_in_progress = False + + assert self.message_fetched.is_set() + self.message_fetched.clear() + + # put() was unblocked by close() rather than get() or get_iter(). + if self.closed: + raise EOFError("stream of frames ended") + + self.decoder = None + + def close(self) -> None: + """ + End the stream of frames. + + Callling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`, + or :meth:`put` is safe. They will raise :exc:`EOFError`. + + """ + with self.mutex: + if self.closed: + return + + self.closed = True + + # Unblock get or get_iter. + if self.get_in_progress: + self.message_complete.set() + if self.chunks_queue is not None: + self.chunks_queue.put(None) + + # Unblock put(). + if self.put_in_progress: + self.message_fetched.set() diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py new file mode 100644 index 0000000000..14767968c9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py @@ -0,0 +1,530 @@ +from __future__ import annotations + +import http +import logging +import os +import selectors +import socket +import ssl +import sys +import threading +from types import TracebackType +from typing import Any, Callable, Optional, Sequence, Type + +from websockets.frames import CloseCode + +from ..extensions.base import ServerExtensionFactory +from ..extensions.permessage_deflate import enable_server_permessage_deflate +from ..headers import validate_subprotocols +from ..http import USER_AGENT +from ..http11 import Request, Response +from ..protocol import CONNECTING, OPEN, Event +from ..server import ServerProtocol +from ..typing import LoggerLike, Origin, Subprotocol +from .connection import Connection +from .utils import Deadline + + +__all__ = ["serve", "unix_serve", "ServerConnection", "WebSocketServer"] + + +class ServerConnection(Connection): + """ + Threaded implementation of a WebSocket server connection. + + :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for + receiving and sending messages. + + It supports iteration to receive messages:: + + for message in websocket: + process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is + closed with any other code. + + Args: + socket: Socket connected to a WebSocket client. + protocol: Sans-I/O connection. + close_timeout: Timeout for closing the connection in seconds. + + """ + + def __init__( + self, + socket: socket.socket, + protocol: ServerProtocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.protocol: ServerProtocol + self.request_rcvd = threading.Event() + super().__init__( + socket, + protocol, + close_timeout=close_timeout, + ) + + def handshake( + self, + process_request: Optional[ + Callable[ + [ServerConnection, Request], + Optional[Response], + ] + ] = None, + process_response: Optional[ + Callable[ + [ServerConnection, Request, Response], + Optional[Response], + ] + ] = None, + server_header: Optional[str] = USER_AGENT, + timeout: Optional[float] = None, + ) -> None: + """ + Perform the opening handshake. + + """ + if not self.request_rcvd.wait(timeout): + self.close_socket() + self.recv_events_thread.join() + raise TimeoutError("timed out during handshake") + + if self.request is None: + self.close_socket() + self.recv_events_thread.join() + raise ConnectionError("connection closed during handshake") + + with self.send_context(expected_state=CONNECTING): + self.response = None + + if process_request is not None: + try: + self.response = process_request(self, self.request) + except Exception as exc: + self.protocol.handshake_exc = exc + self.logger.error("opening handshake failed", exc_info=True) + self.response = self.protocol.reject( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ( + "Failed to open a WebSocket connection.\n" + "See server log for more information.\n" + ), + ) + + if self.response is None: + self.response = self.protocol.accept(self.request) + + if server_header is not None: + self.response.headers["Server"] = server_header + + if process_response is not None: + try: + response = process_response(self, self.request, self.response) + except Exception as exc: + self.protocol.handshake_exc = exc + self.logger.error("opening handshake failed", exc_info=True) + self.response = self.protocol.reject( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ( + "Failed to open a WebSocket connection.\n" + "See server log for more information.\n" + ), + ) + else: + if response is not None: + self.response = response + + self.protocol.send_response(self.response) + + if self.protocol.state is not OPEN: + self.recv_events_thread.join(self.close_timeout) + self.close_socket() + self.recv_events_thread.join() + + if self.protocol.handshake_exc is not None: + raise self.protocol.handshake_exc + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + """ + # First event - handshake request. + if self.request is None: + assert isinstance(event, Request) + self.request = event + self.request_rcvd.set() + # Later events - frames. + else: + super().process_event(event) + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + """ + try: + super().recv_events() + finally: + # If the connection is closed during the handshake, unblock it. + self.request_rcvd.set() + + +class WebSocketServer: + """ + WebSocket server returned by :func:`serve`. + + This class mirrors the API of :class:`~socketserver.BaseServer`, notably the + :meth:`~socketserver.BaseServer.serve_forever` and + :meth:`~socketserver.BaseServer.shutdown` methods, as well as the context + manager protocol. + + Args: + socket: Server socket listening for new connections. + handler: Handler for one connection. Receives the socket and address + returned by :meth:`~socket.socket.accept`. + logger: Logger for this server. + + """ + + def __init__( + self, + socket: socket.socket, + handler: Callable[[socket.socket, Any], None], + logger: Optional[LoggerLike] = None, + ): + self.socket = socket + self.handler = handler + if logger is None: + logger = logging.getLogger("websockets.server") + self.logger = logger + if sys.platform != "win32": + self.shutdown_watcher, self.shutdown_notifier = os.pipe() + + def serve_forever(self) -> None: + """ + See :meth:`socketserver.BaseServer.serve_forever`. + + This method doesn't return. Calling :meth:`shutdown` from another thread + stops the server. + + Typical use:: + + with serve(...) as server: + server.serve_forever() + + """ + poller = selectors.DefaultSelector() + poller.register(self.socket, selectors.EVENT_READ) + if sys.platform != "win32": + poller.register(self.shutdown_watcher, selectors.EVENT_READ) + + while True: + poller.select() + try: + # If the socket is closed, this will raise an exception and exit + # the loop. So we don't need to check the return value of select(). + sock, addr = self.socket.accept() + except OSError: + break + thread = threading.Thread(target=self.handler, args=(sock, addr)) + thread.start() + + def shutdown(self) -> None: + """ + See :meth:`socketserver.BaseServer.shutdown`. + + """ + self.socket.close() + if sys.platform != "win32": + os.write(self.shutdown_notifier, b"x") + + def fileno(self) -> int: + """ + See :meth:`socketserver.BaseServer.fileno`. + + """ + return self.socket.fileno() + + def __enter__(self) -> WebSocketServer: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self.shutdown() + + +def serve( + handler: Callable[[ServerConnection], None], + host: Optional[str] = None, + port: Optional[int] = None, + *, + # TCP/TLS — unix and path are only for unix_serve() + sock: Optional[socket.socket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + unix: bool = False, + path: Optional[str] = None, + # WebSocket + origins: Optional[Sequence[Optional[Origin]]] = None, + extensions: Optional[Sequence[ServerExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + select_subprotocol: Optional[ + Callable[ + [ServerConnection, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None, + process_request: Optional[ + Callable[ + [ServerConnection, Request], + Optional[Response], + ] + ] = None, + process_response: Optional[ + Callable[ + [ServerConnection, Request, Response], + Optional[Response], + ] + ] = None, + server_header: Optional[str] = USER_AGENT, + compression: Optional[str] = "deflate", + # Timeouts + open_timeout: Optional[float] = 10, + close_timeout: Optional[float] = 10, + # Limits + max_size: Optional[int] = 2**20, + # Logging + logger: Optional[LoggerLike] = None, + # Escape hatch for advanced customization + create_connection: Optional[Type[ServerConnection]] = None, +) -> WebSocketServer: + """ + Create a WebSocket server listening on ``host`` and ``port``. + + Whenever a client connects, the server creates a :class:`ServerConnection`, + performs the opening handshake, and delegates to the ``handler``. + + The handler receives a :class:`ServerConnection` instance, which you can use + to send and receive messages. + + Once the handler completes, either normally or with an exception, the server + performs the closing handshake and closes the connection. + + :class:`WebSocketServer` mirrors the API of + :class:`~socketserver.BaseServer`. Treat it as a context manager to ensure + that it will be closed and call the :meth:`~WebSocketServer.serve_forever` + method to serve requests:: + + def handler(websocket): + ... + + with websockets.sync.server.serve(handler, ...) as server: + server.serve_forever() + + Args: + handler: Connection handler. It receives the WebSocket connection, + which is a :class:`ServerConnection`, in argument. + host: Network interfaces the server binds to. + See :func:`~socket.create_server` for details. + port: TCP port the server listens on. + See :func:`~socket.create_server` for details. + sock: Preexisting TCP socket. ``sock`` replaces ``host`` and ``port``. + You may call :func:`socket.create_server` to create a suitable TCP + socket. + ssl_context: Configuration for enabling TLS on the connection. + origins: Acceptable values of the ``Origin`` header, for defending + against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` + in the list if the lack of an origin is acceptable. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + select_subprotocol: Callback for selecting a subprotocol among + those supported by the client and the server. It receives a + :class:`ServerConnection` (not a + :class:`~websockets.server.ServerProtocol`!) instance and a list of + subprotocols offered by the client. Other than the first argument, + it has the same behavior as the + :meth:`ServerProtocol.select_subprotocol + <websockets.server.ServerProtocol.select_subprotocol>` method. + process_request: Intercept the request during the opening handshake. + Return an HTTP response to force the response or :obj:`None` to + continue normally. When you force an HTTP 101 Continue response, + the handshake is successful. Else, the connection is aborted. + process_response: Intercept the response during the opening handshake. + Return an HTTP response to force the response or :obj:`None` to + continue normally. When you force an HTTP 101 Continue response, + the handshake is successful. Else, the connection is aborted. + server_header: Value of the ``Server`` response header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to + :obj:`None` removes the header. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + open_timeout: Timeout for opening connections in seconds. + :obj:`None` disables the timeout. + close_timeout: Timeout for closing connections in seconds. + :obj:`None` disables the timeout. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. See the + :doc:`logging guide <../../topics/logging>` for details. + create_connection: Factory for the :class:`ServerConnection` managing + the connection. Set it to a wrapper or a subclass to customize + connection handling. + """ + + # Process parameters + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + if compression == "deflate": + extensions = enable_server_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + if create_connection is None: + create_connection = ServerConnection + + # Bind socket and listen + + if sock is None: + if unix: + if path is None: + raise TypeError("missing path argument") + sock = socket.create_server(path, family=socket.AF_UNIX) + else: + sock = socket.create_server((host, port)) + else: + if path is not None: + raise TypeError("path and sock arguments are incompatible") + + # Initialize TLS wrapper + + if ssl_context is not None: + sock = ssl_context.wrap_socket( + sock, + server_side=True, + # Delay TLS handshake until after we set a timeout on the socket. + do_handshake_on_connect=False, + ) + + # Define request handler + + def conn_handler(sock: socket.socket, addr: Any) -> None: + # Calculate timeouts on the TLS and WebSocket handshakes. + # The TLS timeout must be set on the socket, then removed + # to avoid conflicting with the WebSocket timeout in handshake(). + deadline = Deadline(open_timeout) + + try: + # Disable Nagle algorithm + + if not unix: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + + # Perform TLS handshake + + if ssl_context is not None: + sock.settimeout(deadline.timeout()) + assert isinstance(sock, ssl.SSLSocket) # mypy cannot figure this out + sock.do_handshake() + sock.settimeout(None) + + # Create a closure so that select_subprotocol has access to self. + + protocol_select_subprotocol: Optional[ + Callable[ + [ServerProtocol, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None + + if select_subprotocol is not None: + + def protocol_select_subprotocol( + protocol: ServerProtocol, + subprotocols: Sequence[Subprotocol], + ) -> Optional[Subprotocol]: + # mypy doesn't know that select_subprotocol is immutable. + assert select_subprotocol is not None + # Ensure this function is only used in the intended context. + assert protocol is connection.protocol + return select_subprotocol(connection, subprotocols) + + # Initialize WebSocket connection + + protocol = ServerProtocol( + origins=origins, + extensions=extensions, + subprotocols=subprotocols, + select_subprotocol=protocol_select_subprotocol, + state=CONNECTING, + max_size=max_size, + logger=logger, + ) + + # Initialize WebSocket protocol + + assert create_connection is not None # help mypy + connection = create_connection( + sock, + protocol, + close_timeout=close_timeout, + ) + # On failure, handshake() closes the socket, raises an exception, and + # logs it. + connection.handshake( + process_request, + process_response, + server_header, + deadline.timeout(), + ) + + except Exception: + sock.close() + return + + try: + handler(connection) + except Exception: + protocol.logger.error("connection handler failed", exc_info=True) + connection.close(CloseCode.INTERNAL_ERROR) + else: + connection.close() + + # Initialize server + + return WebSocketServer(sock, conn_handler, logger) + + +def unix_serve( + handler: Callable[[ServerConnection], Any], + path: Optional[str] = None, + **kwargs: Any, +) -> WebSocketServer: + """ + Create a WebSocket server listening on a Unix socket. + + This function is identical to :func:`serve`, except the ``host`` and + ``port`` arguments are replaced by ``path``. It's only available on Unix. + + It's useful for deploying a server behind a reverse proxy such as nginx. + + Args: + handler: Connection handler. It receives the WebSocket connection, + which is a :class:`ServerConnection`, in argument. + path: File system path to the Unix socket. + + """ + return serve(handler, path=path, unix=True, **kwargs) diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py new file mode 100644 index 0000000000..471f32e19d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import time +from typing import Optional + + +__all__ = ["Deadline"] + + +class Deadline: + """ + Manage timeouts across multiple steps. + + Args: + timeout: Time available in seconds or :obj:`None` if there is no limit. + + """ + + def __init__(self, timeout: Optional[float]) -> None: + self.deadline: Optional[float] + if timeout is None: + self.deadline = None + else: + self.deadline = time.monotonic() + timeout + + def timeout(self, *, raise_if_elapsed: bool = True) -> Optional[float]: + """ + Calculate a timeout from a deadline. + + Args: + raise_if_elapsed (bool): Whether to raise :exc:`TimeoutError` + if the deadline lapsed. + + Raises: + TimeoutError: If the deadline lapsed. + + Returns: + Time left in seconds or :obj:`None` if there is no limit. + + """ + if self.deadline is None: + return None + timeout = self.deadline - time.monotonic() + if raise_if_elapsed and timeout <= 0: + raise TimeoutError("timed out") + return timeout diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py index e672ba0069..cc3e3ec0d9 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py @@ -1,5 +1,6 @@ from __future__ import annotations +import http import logging from typing import List, NewType, Optional, Tuple, Union @@ -7,6 +8,7 @@ from typing import List, NewType, Optional, Tuple, Union __all__ = [ "Data", "LoggerLike", + "StatusLike", "Origin", "Subprotocol", "ExtensionName", @@ -30,6 +32,11 @@ LoggerLike = Union[logging.Logger, logging.LoggerAdapter] """Types accepted where a :class:`~logging.Logger` is expected.""" +StatusLike = Union[http.HTTPStatus, int] +""" +Types accepted where an :class:`~http.HTTPStatus` is expected.""" + + Origin = NewType("Origin", str) """Value of a ``Origin`` header.""" diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py index fff0c38064..385090f66a 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py @@ -33,8 +33,8 @@ class WebSocketURI: port: int path: str query: str - username: Optional[str] - password: Optional[str] + username: Optional[str] = None + password: Optional[str] = None @property def resource_name(self) -> str: diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py index c30bfd68f3..d1c99458e2 100644 --- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py +++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py @@ -1,5 +1,7 @@ from __future__ import annotations +import importlib.metadata + __all__ = ["tag", "version", "commit"] @@ -18,7 +20,7 @@ __all__ = ["tag", "version", "commit"] released = True -tag = version = commit = "10.3" +tag = version = commit = "12.0" if not released: # pragma: no cover @@ -44,7 +46,11 @@ if not released: # pragma: no cover text=True, ).stdout.strip() # subprocess.run raises FileNotFoundError if git isn't on $PATH. - except (FileNotFoundError, subprocess.CalledProcessError): + except ( + FileNotFoundError, + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + ): pass else: description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)" @@ -56,8 +62,6 @@ if not released: # pragma: no cover # Read version from package metadata if it is installed. try: - import importlib.metadata # move up when dropping Python 3.7 - return importlib.metadata.version("websockets") except ImportError: pass diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py new file mode 100644 index 0000000000..dd78609f5b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py @@ -0,0 +1,5 @@ +import logging + + +# Avoid displaying stack traces at the ERROR logging level. +logging.basicConfig(level=logging.CRITICAL) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py new file mode 100644 index 0000000000..b18ffb6fb8 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py @@ -0,0 +1,4 @@ +from websockets.extensions.base import * + + +# Abstract classes don't provide any behavior to test. diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py new file mode 100644 index 0000000000..0e698566fb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py @@ -0,0 +1,977 @@ +import dataclasses +import unittest + +from websockets.exceptions import ( + DuplicateParameter, + InvalidParameterName, + InvalidParameterValue, + NegotiationError, + PayloadTooBig, + ProtocolError, +) +from websockets.extensions.permessage_deflate import * +from websockets.frames import ( + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Frame, +) + +from .utils import ClientNoOpExtensionFactory, ServerNoOpExtensionFactory + + +class PerMessageDeflateTestsMixin: + def assertExtensionEqual(self, extension1, extension2): + self.assertEqual( + extension1.remote_no_context_takeover, + extension2.remote_no_context_takeover, + ) + self.assertEqual( + extension1.local_no_context_takeover, + extension2.local_no_context_takeover, + ) + self.assertEqual( + extension1.remote_max_window_bits, + extension2.remote_max_window_bits, + ) + self.assertEqual( + extension1.local_max_window_bits, + extension2.local_max_window_bits, + ) + + +class PerMessageDeflateTests(unittest.TestCase, PerMessageDeflateTestsMixin): + def setUp(self): + # Set up an instance of the permessage-deflate extension with the most + # common settings. Since the extension is symmetrical, this instance + # may be used for testing both encoding and decoding. + self.extension = PerMessageDeflate(False, False, 15, 15) + + def test_name(self): + assert self.extension.name == "permessage-deflate" + + def test_repr(self): + self.assertExtensionEqual(eval(repr(self.extension)), self.extension) + + # Control frames aren't encoded or decoded. + + def test_no_encode_decode_ping_frame(self): + frame = Frame(OP_PING, b"") + + self.assertEqual(self.extension.encode(frame), frame) + + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_encode_decode_pong_frame(self): + frame = Frame(OP_PONG, b"") + + self.assertEqual(self.extension.encode(frame), frame) + + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_encode_decode_close_frame(self): + frame = Frame(OP_CLOSE, Close(CloseCode.NORMAL_CLOSURE, "").serialize()) + + self.assertEqual(self.extension.encode(frame), frame) + + self.assertEqual(self.extension.decode(frame), frame) + + # Data frames are encoded and decoded. + + def test_encode_decode_text_frame(self): + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame = self.extension.encode(frame) + + self.assertEqual( + enc_frame, + dataclasses.replace(frame, rsv1=True, data=b"JNL;\xbc\x12\x00"), + ) + + dec_frame = self.extension.decode(enc_frame) + + self.assertEqual(dec_frame, frame) + + def test_encode_decode_binary_frame(self): + frame = Frame(OP_BINARY, b"tea") + + enc_frame = self.extension.encode(frame) + + self.assertEqual( + enc_frame, + dataclasses.replace(frame, rsv1=True, data=b"*IM\x04\x00"), + ) + + dec_frame = self.extension.decode(enc_frame) + + self.assertEqual(dec_frame, frame) + + def test_encode_decode_fragmented_text_frame(self): + frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False) + frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False) + frame3 = Frame(OP_CONT, "croissants".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame1) + enc_frame2 = self.extension.encode(frame2) + enc_frame3 = self.extension.encode(frame3) + + self.assertEqual( + enc_frame1, + dataclasses.replace( + frame1, rsv1=True, data=b"JNL;\xbc\x12\x00\x00\x00\xff\xff" + ), + ) + self.assertEqual( + enc_frame2, + dataclasses.replace(frame2, data=b"RPS\x00\x00\x00\x00\xff\xff"), + ) + self.assertEqual( + enc_frame3, + dataclasses.replace(frame3, data=b"J.\xca\xcf,.N\xcc+)\x06\x00"), + ) + + dec_frame1 = self.extension.decode(enc_frame1) + dec_frame2 = self.extension.decode(enc_frame2) + dec_frame3 = self.extension.decode(enc_frame3) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + self.assertEqual(dec_frame3, frame3) + + def test_encode_decode_fragmented_binary_frame(self): + frame1 = Frame(OP_TEXT, b"tea ", fin=False) + frame2 = Frame(OP_CONT, b"time") + + enc_frame1 = self.extension.encode(frame1) + enc_frame2 = self.extension.encode(frame2) + + self.assertEqual( + enc_frame1, + dataclasses.replace( + frame1, rsv1=True, data=b"*IMT\x00\x00\x00\x00\xff\xff" + ), + ) + self.assertEqual( + enc_frame2, + dataclasses.replace(frame2, data=b"*\xc9\xccM\x05\x00"), + ) + + dec_frame1 = self.extension.decode(enc_frame1) + dec_frame2 = self.extension.decode(enc_frame2) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + + def test_no_decode_text_frame(self): + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + # Try decoding a frame that wasn't encoded. + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_decode_binary_frame(self): + frame = Frame(OP_TEXT, b"tea") + + # Try decoding a frame that wasn't encoded. + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_decode_fragmented_text_frame(self): + frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False) + frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False) + frame3 = Frame(OP_CONT, "croissants".encode("utf-8")) + + dec_frame1 = self.extension.decode(frame1) + dec_frame2 = self.extension.decode(frame2) + dec_frame3 = self.extension.decode(frame3) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + self.assertEqual(dec_frame3, frame3) + + def test_no_decode_fragmented_binary_frame(self): + frame1 = Frame(OP_TEXT, b"tea ", fin=False) + frame2 = Frame(OP_CONT, b"time") + + dec_frame1 = self.extension.decode(frame1) + dec_frame2 = self.extension.decode(frame2) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + + def test_context_takeover(self): + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame) + enc_frame2 = self.extension.encode(frame) + + self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00") + self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00") + + def test_remote_no_context_takeover(self): + # No context takeover when decoding messages. + self.extension = PerMessageDeflate(True, False, 15, 15) + + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame) + enc_frame2 = self.extension.encode(frame) + + self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00") + self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00") + + dec_frame1 = self.extension.decode(enc_frame1) + self.assertEqual(dec_frame1, frame) + + with self.assertRaises(ProtocolError): + self.extension.decode(enc_frame2) + + def test_local_no_context_takeover(self): + # No context takeover when encoding and decoding messages. + self.extension = PerMessageDeflate(True, True, 15, 15) + + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame) + enc_frame2 = self.extension.encode(frame) + + self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00") + self.assertEqual(enc_frame2.data, b"JNL;\xbc\x12\x00") + + dec_frame1 = self.extension.decode(enc_frame1) + dec_frame2 = self.extension.decode(enc_frame2) + + self.assertEqual(dec_frame1, frame) + self.assertEqual(dec_frame2, frame) + + # Compression settings can be customized. + + def test_compress_settings(self): + # Configure an extension so that no compression actually occurs. + extension = PerMessageDeflate(False, False, 15, 15, {"level": 0}) + + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame = extension.encode(frame) + + self.assertEqual( + enc_frame, + dataclasses.replace( + frame, + rsv1=True, + data=b"\x00\x05\x00\xfa\xffcaf\xc3\xa9\x00", # not compressed + ), + ) + + # Frames aren't decoded beyond max_size. + + def test_decompress_max_size(self): + frame = Frame(OP_TEXT, ("a" * 20).encode("utf-8")) + + enc_frame = self.extension.encode(frame) + + self.assertEqual(enc_frame.data, b"JL\xc4\x04\x00\x00") + + with self.assertRaises(PayloadTooBig): + self.extension.decode(enc_frame, max_size=10) + + +class ClientPerMessageDeflateFactoryTests( + unittest.TestCase, PerMessageDeflateTestsMixin +): + def test_name(self): + assert ClientPerMessageDeflateFactory.name == "permessage-deflate" + + def test_init(self): + for config in [ + (False, False, 8, None), # server_max_window_bits ≥ 8 + (False, True, 15, None), # server_max_window_bits ≤ 15 + (True, False, None, 8), # client_max_window_bits ≥ 8 + (True, True, None, 15), # client_max_window_bits ≤ 15 + (False, False, None, True), # client_max_window_bits + (False, False, None, None, {"memLevel": 4}), + ]: + with self.subTest(config=config): + # This does not raise an exception. + ClientPerMessageDeflateFactory(*config) + + def test_init_error(self): + for config in [ + (False, False, 7, 8), # server_max_window_bits < 8 + (False, True, 8, 7), # client_max_window_bits < 8 + (True, False, 16, 15), # server_max_window_bits > 15 + (True, True, 15, 16), # client_max_window_bits > 15 + (False, False, True, None), # server_max_window_bits + (False, False, None, None, {"wbits": 11}), + ]: + with self.subTest(config=config): + with self.assertRaises(ValueError): + ClientPerMessageDeflateFactory(*config) + + def test_get_request_params(self): + for config, result in [ + # Test without any parameter + ( + (False, False, None, None), + [], + ), + # Test server_no_context_takeover + ( + (True, False, None, None), + [("server_no_context_takeover", None)], + ), + # Test client_no_context_takeover + ( + (False, True, None, None), + [("client_no_context_takeover", None)], + ), + # Test server_max_window_bits + ( + (False, False, 10, None), + [("server_max_window_bits", "10")], + ), + # Test client_max_window_bits + ( + (False, False, None, 10), + [("client_max_window_bits", "10")], + ), + ( + (False, False, None, True), + [("client_max_window_bits", None)], + ), + # Test all parameters together + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "12"), + ("client_max_window_bits", "12"), + ], + ), + ]: + with self.subTest(config=config): + factory = ClientPerMessageDeflateFactory(*config) + self.assertEqual(factory.get_request_params(), result) + + def test_process_response_params(self): + for config, response_params, result in [ + # Test without any parameter + ( + (False, False, None, None), + [], + (False, False, 15, 15), + ), + ( + (False, False, None, None), + [("unknown", None)], + InvalidParameterName, + ), + # Test server_no_context_takeover + ( + (False, False, None, None), + [("server_no_context_takeover", None)], + (True, False, 15, 15), + ), + ( + (True, False, None, None), + [], + NegotiationError, + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)], + (True, False, 15, 15), + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)] * 2, + DuplicateParameter, + ), + ( + (True, False, None, None), + [("server_no_context_takeover", "42")], + InvalidParameterValue, + ), + # Test client_no_context_takeover + ( + (False, False, None, None), + [("client_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (False, True, None, None), + [], + (False, True, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)] * 2, + DuplicateParameter, + ), + ( + (False, True, None, None), + [("client_no_context_takeover", "42")], + InvalidParameterValue, + ), + # Test server_max_window_bits + ( + (False, False, None, None), + [("server_max_window_bits", "7")], + NegotiationError, + ), + ( + (False, False, None, None), + [("server_max_window_bits", "10")], + (False, False, 10, 15), + ), + ( + (False, False, None, None), + [("server_max_window_bits", "16")], + NegotiationError, + ), + ( + (False, False, 12, None), + [], + NegotiationError, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "10")], + (False, False, 10, 15), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")], + (False, False, 12, 15), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "13")], + NegotiationError, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")] * 2, + DuplicateParameter, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "42")], + InvalidParameterValue, + ), + # Test client_max_window_bits + ( + (False, False, None, None), + [("client_max_window_bits", "10")], + NegotiationError, + ), + ( + (False, False, None, True), + [], + (False, False, 15, 15), + ), + ( + (False, False, None, True), + [("client_max_window_bits", "7")], + NegotiationError, + ), + ( + (False, False, None, True), + [("client_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, None, True), + [("client_max_window_bits", "16")], + NegotiationError, + ), + ( + (False, False, None, 12), + [], + (False, False, 15, 12), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "13")], + NegotiationError, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")] * 2, + DuplicateParameter, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "42")], + InvalidParameterValue, + ), + # Test all parameters together + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (False, False, None, True), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("server_max_window_bits", "12"), + ], + (True, True, 12, 12), + ), + ]: + with self.subTest(config=config, response_params=response_params): + factory = ClientPerMessageDeflateFactory(*config) + if isinstance(result, type) and issubclass(result, Exception): + with self.assertRaises(result): + factory.process_response_params(response_params, []) + else: + extension = factory.process_response_params(response_params, []) + expected = PerMessageDeflate(*result) + self.assertExtensionEqual(extension, expected) + + def test_process_response_params_deduplication(self): + factory = ClientPerMessageDeflateFactory(False, False, None, None) + with self.assertRaises(NegotiationError): + factory.process_response_params( + [], [PerMessageDeflate(False, False, 15, 15)] + ) + + def test_enable_client_permessage_deflate(self): + for extensions, ( + expected_len, + expected_position, + expected_compress_settings, + ) in [ + ( + None, + (1, 0, {"memLevel": 5}), + ), + ( + [], + (1, 0, {"memLevel": 5}), + ), + ( + [ClientNoOpExtensionFactory()], + (2, 1, {"memLevel": 5}), + ), + ( + [ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7})], + (1, 0, {"memLevel": 7}), + ), + ( + [ + ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ClientNoOpExtensionFactory(), + ], + (2, 0, {"memLevel": 7}), + ), + ( + [ + ClientNoOpExtensionFactory(), + ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ], + (2, 1, {"memLevel": 7}), + ), + ]: + with self.subTest(extensions=extensions): + extensions = enable_client_permessage_deflate(extensions) + self.assertEqual(len(extensions), expected_len) + extension = extensions[expected_position] + self.assertIsInstance(extension, ClientPerMessageDeflateFactory) + self.assertEqual( + extension.compress_settings, + expected_compress_settings, + ) + + +class ServerPerMessageDeflateFactoryTests( + unittest.TestCase, PerMessageDeflateTestsMixin +): + def test_name(self): + assert ServerPerMessageDeflateFactory.name == "permessage-deflate" + + def test_init(self): + for config in [ + (False, False, 8, None), # server_max_window_bits ≥ 8 + (False, True, 15, None), # server_max_window_bits ≤ 15 + (True, False, None, 8), # client_max_window_bits ≥ 8 + (True, True, None, 15), # client_max_window_bits ≤ 15 + (False, False, None, None, {"memLevel": 4}), + (False, False, None, 12, {}, True), # require_client_max_window_bits + ]: + with self.subTest(config=config): + # This does not raise an exception. + ServerPerMessageDeflateFactory(*config) + + def test_init_error(self): + for config in [ + (False, False, 7, 8), # server_max_window_bits < 8 + (False, True, 8, 7), # client_max_window_bits < 8 + (True, False, 16, 15), # server_max_window_bits > 15 + (True, True, 15, 16), # client_max_window_bits > 15 + (False, False, None, True), # client_max_window_bits + (False, False, True, None), # server_max_window_bits + (False, False, None, None, {"wbits": 11}), + (False, False, None, None, {}, True), # require_client_max_window_bits + ]: + with self.subTest(config=config): + with self.assertRaises(ValueError): + ServerPerMessageDeflateFactory(*config) + + def test_process_request_params(self): + # Parameters in result appear swapped vs. config because the order is + # (remote, local) vs. (server, client). + for config, request_params, response_params, result in [ + # Test without any parameter + ( + (False, False, None, None), + [], + [], + (False, False, 15, 15), + ), + ( + (False, False, None, None), + [("unknown", None)], + None, + InvalidParameterName, + ), + # Test server_no_context_takeover + ( + (False, False, None, None), + [("server_no_context_takeover", None)], + [("server_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (True, False, None, None), + [], + [("server_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)], + [("server_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)] * 2, + None, + DuplicateParameter, + ), + ( + (True, False, None, None), + [("server_no_context_takeover", "42")], + None, + InvalidParameterValue, + ), + # Test client_no_context_takeover + ( + (False, False, None, None), + [("client_no_context_takeover", None)], + [("client_no_context_takeover", None)], # doesn't matter + (True, False, 15, 15), + ), + ( + (False, True, None, None), + [], + [("client_no_context_takeover", None)], + (True, False, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)], + [("client_no_context_takeover", None)], # doesn't matter + (True, False, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)] * 2, + None, + DuplicateParameter, + ), + ( + (False, True, None, None), + [("client_no_context_takeover", "42")], + None, + InvalidParameterValue, + ), + # Test server_max_window_bits + ( + (False, False, None, None), + [("server_max_window_bits", "7")], + None, + NegotiationError, + ), + ( + (False, False, None, None), + [("server_max_window_bits", "10")], + [("server_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, None, None), + [("server_max_window_bits", "16")], + None, + NegotiationError, + ), + ( + (False, False, 12, None), + [], + [("server_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "10")], + [("server_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")], + [("server_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "13")], + [("server_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")] * 2, + None, + DuplicateParameter, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "42")], + None, + InvalidParameterValue, + ), + # Test client_max_window_bits + ( + (False, False, None, None), + [("client_max_window_bits", None)], + [], + (False, False, 15, 15), + ), + ( + (False, False, None, None), + [("client_max_window_bits", "7")], + None, + InvalidParameterValue, + ), + ( + (False, False, None, None), + [("client_max_window_bits", "10")], + [("client_max_window_bits", "10")], # doesn't matter + (False, False, 10, 15), + ), + ( + (False, False, None, None), + [("client_max_window_bits", "16")], + None, + InvalidParameterValue, + ), + ( + (False, False, None, 12), + [], + [], + (False, False, 15, 15), + ), + ( + (False, False, None, 12, {}, True), + [], + None, + NegotiationError, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", None)], + [("client_max_window_bits", "12")], + (False, False, 12, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "10")], + [("client_max_window_bits", "10")], + (False, False, 10, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")], + [("client_max_window_bits", "12")], # doesn't matter + (False, False, 12, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "13")], + [("client_max_window_bits", "12")], # doesn't matter + (False, False, 12, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")] * 2, + None, + DuplicateParameter, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "42")], + None, + InvalidParameterValue, + ), + # Test all parameters together + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (False, False, None, None), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (True, True, 12, 12), + [("client_max_window_bits", None)], + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "12"), + ("client_max_window_bits", "12"), + ], + (True, True, 12, 12), + ), + ]: + with self.subTest( + config=config, + request_params=request_params, + response_params=response_params, + ): + factory = ServerPerMessageDeflateFactory(*config) + if isinstance(result, type) and issubclass(result, Exception): + with self.assertRaises(result): + factory.process_request_params(request_params, []) + else: + params, extension = factory.process_request_params( + request_params, [] + ) + self.assertEqual(params, response_params) + expected = PerMessageDeflate(*result) + self.assertExtensionEqual(extension, expected) + + def test_process_response_params_deduplication(self): + factory = ServerPerMessageDeflateFactory(False, False, None, None) + with self.assertRaises(NegotiationError): + factory.process_request_params( + [], [PerMessageDeflate(False, False, 15, 15)] + ) + + def test_enable_server_permessage_deflate(self): + for extensions, ( + expected_len, + expected_position, + expected_compress_settings, + ) in [ + ( + None, + (1, 0, {"memLevel": 5}), + ), + ( + [], + (1, 0, {"memLevel": 5}), + ), + ( + [ServerNoOpExtensionFactory()], + (2, 1, {"memLevel": 5}), + ), + ( + [ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7})], + (1, 0, {"memLevel": 7}), + ), + ( + [ + ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ServerNoOpExtensionFactory(), + ], + (2, 0, {"memLevel": 7}), + ), + ( + [ + ServerNoOpExtensionFactory(), + ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ], + (2, 1, {"memLevel": 7}), + ), + ]: + with self.subTest(extensions=extensions): + extensions = enable_server_permessage_deflate(extensions) + self.assertEqual(len(extensions), expected_len) + extension = extensions[expected_position] + self.assertIsInstance(extension, ServerPerMessageDeflateFactory) + self.assertEqual( + extension.compress_settings, + expected_compress_settings, + ) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/utils.py new file mode 100644 index 0000000000..24fb74b4e6 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/utils.py @@ -0,0 +1,113 @@ +import dataclasses + +from websockets.exceptions import NegotiationError + + +class OpExtension: + name = "x-op" + + def __init__(self, op=None): + self.op = op + + def decode(self, frame, *, max_size=None): + return frame # pragma: no cover + + def encode(self, frame): + return frame # pragma: no cover + + def __eq__(self, other): + return isinstance(other, OpExtension) and self.op == other.op + + +class ClientOpExtensionFactory: + name = "x-op" + + def __init__(self, op=None): + self.op = op + + def get_request_params(self): + return [("op", self.op)] + + def process_response_params(self, params, accepted_extensions): + if params != [("op", self.op)]: + raise NegotiationError() + return OpExtension(self.op) + + +class ServerOpExtensionFactory: + name = "x-op" + + def __init__(self, op=None): + self.op = op + + def process_request_params(self, params, accepted_extensions): + if params != [("op", self.op)]: + raise NegotiationError() + return [("op", self.op)], OpExtension(self.op) + + +class NoOpExtension: + name = "x-no-op" + + def __repr__(self): + return "NoOpExtension()" + + def decode(self, frame, *, max_size=None): + return frame + + def encode(self, frame): + return frame + + +class ClientNoOpExtensionFactory: + name = "x-no-op" + + def get_request_params(self): + return [] + + def process_response_params(self, params, accepted_extensions): + if params: + raise NegotiationError() + return NoOpExtension() + + +class ServerNoOpExtensionFactory: + name = "x-no-op" + + def __init__(self, params=None): + self.params = params or [] + + def process_request_params(self, params, accepted_extensions): + return self.params, NoOpExtension() + + +class Rsv2Extension: + name = "x-rsv2" + + def decode(self, frame, *, max_size=None): + assert frame.rsv2 + return dataclasses.replace(frame, rsv2=False) + + def encode(self, frame): + assert not frame.rsv2 + return dataclasses.replace(frame, rsv2=True) + + def __eq__(self, other): + return isinstance(other, Rsv2Extension) + + +class ClientRsv2ExtensionFactory: + name = "x-rsv2" + + def get_request_params(self): + return [] + + def process_response_params(self, params, accepted_extensions): + return Rsv2Extension() + + +class ServerRsv2ExtensionFactory: + name = "x-rsv2" + + def process_request_params(self, params, accepted_extensions): + return [], Rsv2Extension() diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/__init__.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_auth.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_auth.py new file mode 100644 index 0000000000..3754bcf3a5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_auth.py @@ -0,0 +1,184 @@ +import hmac +import unittest +import urllib.error + +from websockets.exceptions import InvalidStatusCode +from websockets.headers import build_authorization_basic +from websockets.legacy.auth import * +from websockets.legacy.auth import is_credentials + +from .test_client_server import ClientServerTestsMixin, with_client, with_server +from .utils import AsyncioTestCase + + +class AuthTests(unittest.TestCase): + def test_is_credentials(self): + self.assertTrue(is_credentials(("username", "password"))) + + def test_is_not_credentials(self): + self.assertFalse(is_credentials(None)) + self.assertFalse(is_credentials("username")) + + +class CustomWebSocketServerProtocol(BasicAuthWebSocketServerProtocol): + async def process_request(self, path, request_headers): + type(self).used = True + return await super().process_request(path, request_headers) + + +class CheckWebSocketServerProtocol(BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + return hmac.compare_digest(password, "letmein") + + +class AuthClientServerTests(ClientServerTestsMixin, AsyncioTestCase): + create_protocol = basic_auth_protocol_factory( + realm="auth-tests", credentials=("hello", "iloveyou") + ) + + @with_server(create_protocol=create_protocol) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth(self): + req_headers = self.client.request_headers + resp_headers = self.client.response_headers + self.assertEqual(req_headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=") + self.assertNotIn("WWW-Authenticate", resp_headers) + + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + def test_basic_auth_server_no_credentials(self): + with self.assertRaises(TypeError) as raised: + basic_auth_protocol_factory(realm="auth-tests", credentials=None) + self.assertEqual( + str(raised.exception), "provide either credentials or check_credentials" + ) + + def test_basic_auth_server_bad_credentials(self): + with self.assertRaises(TypeError) as raised: + basic_auth_protocol_factory(realm="auth-tests", credentials=42) + self.assertEqual(str(raised.exception), "invalid credentials argument: 42") + + create_protocol_multiple_credentials = basic_auth_protocol_factory( + realm="auth-tests", + credentials=[("hello", "iloveyou"), ("goodbye", "stillloveu")], + ) + + @with_server(create_protocol=create_protocol_multiple_credentials) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth_server_multiple_credentials(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + def test_basic_auth_bad_multiple_credentials(self): + with self.assertRaises(TypeError) as raised: + basic_auth_protocol_factory( + realm="auth-tests", credentials=[("hello", "iloveyou"), 42] + ) + self.assertEqual( + str(raised.exception), + "invalid credentials argument: [('hello', 'iloveyou'), 42]", + ) + + async def check_credentials(username, password): + return hmac.compare_digest(password, "iloveyou") + + create_protocol_check_credentials = basic_auth_protocol_factory( + realm="auth-tests", + check_credentials=check_credentials, + ) + + @with_server(create_protocol=create_protocol_check_credentials) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth_check_credentials(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + create_protocol_custom_protocol = basic_auth_protocol_factory( + realm="auth-tests", + credentials=[("hello", "iloveyou")], + create_protocol=CustomWebSocketServerProtocol, + ) + + @with_server(create_protocol=create_protocol_custom_protocol) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth_custom_protocol(self): + self.assertTrue(CustomWebSocketServerProtocol.used) + del CustomWebSocketServerProtocol.used + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + @with_server(create_protocol=CheckWebSocketServerProtocol) + @with_client(user_info=("hello", "letmein")) + def test_basic_auth_custom_protocol_subclass(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + # CustomWebSocketServerProtocol doesn't override check_credentials + @with_server(create_protocol=CustomWebSocketServerProtocol) + def test_basic_auth_defaults_to_deny_all(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(user_info=("hello", "iloveyou")) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_missing_credentials(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_missing_credentials_details(self): + with self.assertRaises(urllib.error.HTTPError) as raised: + self.loop.run_until_complete(self.make_http_request()) + self.assertEqual(raised.exception.code, 401) + self.assertEqual( + raised.exception.headers["WWW-Authenticate"], + 'Basic realm="auth-tests", charset="UTF-8"', + ) + self.assertEqual(raised.exception.read().decode(), "Missing credentials\n") + + @with_server(create_protocol=create_protocol) + def test_basic_auth_unsupported_credentials(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(extra_headers={"Authorization": "Digest ..."}) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_unsupported_credentials_details(self): + with self.assertRaises(urllib.error.HTTPError) as raised: + self.loop.run_until_complete( + self.make_http_request(headers={"Authorization": "Digest ..."}) + ) + self.assertEqual(raised.exception.code, 401) + self.assertEqual( + raised.exception.headers["WWW-Authenticate"], + 'Basic realm="auth-tests", charset="UTF-8"', + ) + self.assertEqual(raised.exception.read().decode(), "Unsupported credentials\n") + + @with_server(create_protocol=create_protocol) + def test_basic_auth_invalid_username(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(user_info=("goodbye", "iloveyou")) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_invalid_password(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(user_info=("hello", "ihateyou")) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_invalid_credentials_details(self): + with self.assertRaises(urllib.error.HTTPError) as raised: + authorization = build_authorization_basic("hello", "ihateyou") + self.loop.run_until_complete( + self.make_http_request(headers={"Authorization": authorization}) + ) + self.assertEqual(raised.exception.code, 401) + self.assertEqual( + raised.exception.headers["WWW-Authenticate"], + 'Basic realm="auth-tests", charset="UTF-8"', + ) + self.assertEqual(raised.exception.read().decode(), "Invalid credentials\n") diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_client_server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_client_server.py new file mode 100644 index 0000000000..c49d91b707 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_client_server.py @@ -0,0 +1,1636 @@ +import asyncio +import contextlib +import functools +import http +import logging +import platform +import random +import socket +import ssl +import sys +import unittest +import unittest.mock +import urllib.error +import urllib.request +import warnings + +from websockets.datastructures import Headers +from websockets.exceptions import ( + ConnectionClosed, + InvalidHandshake, + InvalidHeader, + InvalidStatusCode, + NegotiationError, +) +from websockets.extensions.permessage_deflate import ( + ClientPerMessageDeflateFactory, + PerMessageDeflate, + ServerPerMessageDeflateFactory, +) +from websockets.frames import CloseCode +from websockets.http import USER_AGENT +from websockets.legacy.client import * +from websockets.legacy.compatibility import asyncio_timeout +from websockets.legacy.handshake import build_response +from websockets.legacy.http import read_response +from websockets.legacy.server import * +from websockets.protocol import State +from websockets.uri import parse_uri + +from ..extensions.utils import ( + ClientNoOpExtensionFactory, + NoOpExtension, + ServerNoOpExtensionFactory, +) +from ..utils import CERTIFICATE, MS, temp_unix_socket_path +from .utils import AsyncioTestCase + + +async def default_handler(ws): + if ws.path == "/deprecated_attributes": + await ws.recv() # delay that allows catching warnings + await ws.send(repr((ws.host, ws.port, ws.secure))) + elif ws.path == "/close_timeout": + await ws.send(repr(ws.close_timeout)) + elif ws.path == "/path": + await ws.send(str(ws.path)) + elif ws.path == "/headers": + await ws.send(repr(ws.request_headers)) + await ws.send(repr(ws.response_headers)) + elif ws.path == "/extensions": + await ws.send(repr(ws.extensions)) + elif ws.path == "/subprotocol": + await ws.send(repr(ws.subprotocol)) + elif ws.path == "/slow_stop": + await ws.wait_closed() + await asyncio.sleep(2 * MS) + else: + await ws.send((await ws.recv())) + + +async def redirect_request(path, headers, test, status): + if path == "/absolute_redirect": + location = get_server_uri(test.server, test.secure, "/") + elif path == "/relative_redirect": + location = "/" + elif path == "/infinite": + location = get_server_uri(test.server, test.secure, "/infinite") + elif path == "/force_insecure": + location = get_server_uri(test.server, False, "/") + elif path == "/missing_location": + return status, {}, b"" + else: + return None + return status, {"Location": location}, b"" + + +@contextlib.contextmanager +def temp_test_server(test, **kwargs): + test.start_server(**kwargs) + try: + yield + finally: + test.stop_server() + + +def temp_test_redirecting_server(test, status=http.HTTPStatus.FOUND, **kwargs): + process_request = functools.partial(redirect_request, test=test, status=status) + return temp_test_server(test, process_request=process_request, **kwargs) + + +@contextlib.contextmanager +def temp_test_client(test, *args, **kwargs): + test.start_client(*args, **kwargs) + try: + yield + finally: + test.stop_client() + + +def with_manager(manager, *args, **kwargs): + """ + Return a decorator that wraps a function with a context manager. + + """ + + def decorate(func): + @functools.wraps(func) + def _decorate(self, *_args, **_kwargs): + with manager(self, *args, **kwargs): + return func(self, *_args, **_kwargs) + + return _decorate + + return decorate + + +def with_server(**kwargs): + """ + Return a decorator for TestCase methods that starts and stops a server. + + """ + return with_manager(temp_test_server, **kwargs) + + +def with_client(*args, **kwargs): + """ + Return a decorator for TestCase methods that starts and stops a client. + + """ + return with_manager(temp_test_client, *args, **kwargs) + + +def get_server_address(server): + """ + Return an address on which the given server listens. + + """ + # Pick a random socket in order to test both IPv4 and IPv6 on systems + # where both are available. Randomizing tests is usually a bad idea. If + # needed, either use the first socket, or test separately IPv4 and IPv6. + server_socket = random.choice(server.sockets) + + if server_socket.family == socket.AF_INET6: # pragma: no cover + return server_socket.getsockname()[:2] # (no IPv6 on CI) + elif server_socket.family == socket.AF_INET: + return server_socket.getsockname() + else: # pragma: no cover + raise ValueError("expected an IPv6, IPv4, or Unix socket") + + +def get_server_uri(server, secure=False, resource_name="/", user_info=None): + """ + Return a WebSocket URI for connecting to the given server. + + """ + proto = "wss" if secure else "ws" + user_info = ":".join(user_info) + "@" if user_info else "" + host, port = get_server_address(server) + if ":" in host: # IPv6 address + host = f"[{host}]" + return f"{proto}://{user_info}{host}:{port}{resource_name}" + + +class UnauthorizedServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + # Test returning headers as a Headers instance (1/3) + return http.HTTPStatus.UNAUTHORIZED, Headers([("X-Access", "denied")]), b"" + + +class ForbiddenServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + # Test returning headers as a dict (2/3) + return http.HTTPStatus.FORBIDDEN, {"X-Access": "denied"}, b"" + + +class HealthCheckServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + # Test returning headers as a list of pairs (3/3) + if path == "/__health__/": + return http.HTTPStatus.OK, [("X-Access", "OK")], b"status = green\n" + + +class ProcessRequestReturningIntProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + assert path == "/__health__/" + return 200, [], b"OK\n" + + +class SlowOpeningHandshakeProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + await asyncio.sleep(10 * MS) + + +class FooClientProtocol(WebSocketClientProtocol): + pass + + +class BarClientProtocol(WebSocketClientProtocol): + pass + + +class ClientServerTestsMixin: + secure = False + + def setUp(self): + super().setUp() + self.server = None + + def start_server(self, deprecation_warnings=None, **kwargs): + handler = kwargs.pop("handler", default_handler) + # Disable compression by default in tests. + kwargs.setdefault("compression", None) + # Disable pings by default in tests. + kwargs.setdefault("ping_interval", None) + + # This logic is encapsulated in a coroutine to prevent it from executing + # before the event loop is running which causes asyncio.get_event_loop() + # to raise a DeprecationWarning on Python ≥ 3.10. + async def start_server(): + return await serve(handler, "localhost", 0, **kwargs) + + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.server = self.loop.run_until_complete(start_server()) + + expected_warnings = [] if deprecation_warnings is None else deprecation_warnings + self.assertDeprecationWarnings(recorded_warnings, expected_warnings) + + def start_client( + self, resource_name="/", user_info=None, deprecation_warnings=None, **kwargs + ): + # Disable compression by default in tests. + kwargs.setdefault("compression", None) + # Disable pings by default in tests. + kwargs.setdefault("ping_interval", None) + + secure = kwargs.get("ssl") is not None + try: + server_uri = kwargs.pop("uri") + except KeyError: + server_uri = get_server_uri(self.server, secure, resource_name, user_info) + + # This logic is encapsulated in a coroutine to prevent it from executing + # before the event loop is running which causes asyncio.get_event_loop() + # to raise a DeprecationWarning on Python ≥ 3.10. + async def start_client(): + return await connect(server_uri, **kwargs) + + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.client = self.loop.run_until_complete(start_client()) + + expected_warnings = [] if deprecation_warnings is None else deprecation_warnings + self.assertDeprecationWarnings(recorded_warnings, expected_warnings) + + def stop_client(self): + self.loop.run_until_complete( + asyncio.wait_for(self.client.close_connection_task, timeout=1) + ) + + def stop_server(self): + self.server.close() + self.loop.run_until_complete( + asyncio.wait_for(self.server.wait_closed(), timeout=1) + ) + + @contextlib.contextmanager + def temp_server(self, **kwargs): + with temp_test_server(self, **kwargs): + yield + + @contextlib.contextmanager + def temp_client(self, *args, **kwargs): + with temp_test_client(self, *args, **kwargs): + yield + + def make_http_request(self, path="/", headers=None): + if headers is None: + headers = {} + + # Set url to 'https?://<host>:<port><path>'. + url = get_server_uri( + self.server, resource_name=path, secure=self.secure + ).replace("ws", "http") + + request = urllib.request.Request(url=url, headers=headers) + + if self.secure: + open_health_check = functools.partial( + urllib.request.urlopen, request, context=self.client_context + ) + else: + open_health_check = functools.partial(urllib.request.urlopen, request) + + return self.loop.run_in_executor(None, open_health_check) + + +class SecureClientServerTestsMixin(ClientServerTestsMixin): + secure = True + + @property + def server_context(self): + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(CERTIFICATE) + return ssl_context + + @property + def client_context(self): + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.load_verify_locations(CERTIFICATE) + return ssl_context + + def start_server(self, **kwargs): + kwargs.setdefault("ssl", self.server_context) + super().start_server(**kwargs) + + def start_client(self, path="/", **kwargs): + kwargs.setdefault("ssl", self.client_context) + super().start_client(path, **kwargs) + + +class CommonClientServerTests: + """ + Mixin that defines most tests but doesn't inherit unittest.TestCase. + + Tests are run by the ClientServerTests and SecureClientServerTests subclasses. + + """ + + @with_server() + @with_client() + def test_basic(self): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + def test_redirect(self): + redirect_statuses = [ + http.HTTPStatus.MOVED_PERMANENTLY, + http.HTTPStatus.FOUND, + http.HTTPStatus.SEE_OTHER, + http.HTTPStatus.TEMPORARY_REDIRECT, + http.HTTPStatus.PERMANENT_REDIRECT, + ] + for status in redirect_statuses: + with temp_test_redirecting_server(self, status): + with self.temp_client("/absolute_redirect"): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + def test_redirect_relative_location(self): + with temp_test_redirecting_server(self): + with self.temp_client("/relative_redirect"): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + def test_infinite_redirect(self): + with temp_test_redirecting_server(self): + with self.assertRaises(InvalidHandshake): + with self.temp_client("/infinite"): + self.fail("did not raise") + + def test_redirect_missing_location(self): + with temp_test_redirecting_server(self): + with self.assertRaises(InvalidHeader): + with self.temp_client("/missing_location"): + self.fail("did not raise") + + def test_loop_backwards_compatibility(self): + with self.temp_server( + loop=self.loop, + deprecation_warnings=["remove loop argument"], + ): + with self.temp_client( + loop=self.loop, + deprecation_warnings=["remove loop argument"], + ): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + @with_server() + def test_explicit_host_port(self): + uri = get_server_uri(self.server, self.secure) + wsuri = parse_uri(uri) + + # Change host and port to invalid values. + scheme = "wss" if wsuri.secure else "ws" + port = 65535 - wsuri.port + changed_uri = f"{scheme}://example.com:{port}/" + + with self.temp_client(uri=changed_uri, host=wsuri.host, port=wsuri.port): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + @with_server() + def test_explicit_socket(self): + class TrackedSocket(socket.socket): + def __init__(self, *args, **kwargs): + self.used_for_read = False + self.used_for_write = False + super().__init__(*args, **kwargs) + + def recv(self, *args, **kwargs): + self.used_for_read = True + return super().recv(*args, **kwargs) + + def recv_into(self, *args, **kwargs): + self.used_for_read = True + return super().recv_into(*args, **kwargs) + + def send(self, *args, **kwargs): + self.used_for_write = True + return super().send(*args, **kwargs) + + server_socket = [ + sock for sock in self.server.sockets if sock.family == socket.AF_INET + ][0] + client_socket = TrackedSocket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect(server_socket.getsockname()) + + try: + self.assertFalse(client_socket.used_for_read) + self.assertFalse(client_socket.used_for_write) + + with self.temp_client(sock=client_socket): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + self.assertTrue(client_socket.used_for_read) + self.assertTrue(client_socket.used_for_write) + + finally: + client_socket.close() + + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") + def test_unix_socket(self): + with temp_unix_socket_path() as path: + # Like self.start_server() but with unix_serve(). + async def start_server(): + return await unix_serve(default_handler, path) + + self.server = self.loop.run_until_complete(start_server()) + + try: + # Like self.start_client() but with unix_connect() + async def start_client(): + return await unix_connect(path) + + self.client = self.loop.run_until_complete(start_client()) + + try: + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + finally: + self.stop_client() + + finally: + self.stop_server() + + def test_ws_handler_argument_backwards_compatibility(self): + async def handler_with_path(ws, path): + await ws.send(path) + + with self.temp_server( + handler=handler_with_path, + # Enable deprecation warning and announce deprecation in 11.0. + # deprecation_warnings=["remove second argument of ws_handler"], + ): + with self.temp_client("/path"): + self.assertEqual( + self.loop.run_until_complete(self.client.recv()), + "/path", + ) + + def test_ws_handler_argument_backwards_compatibility_partial(self): + async def handler_with_path(ws, path, extra): + await ws.send(path) + + bound_handler_with_path = functools.partial(handler_with_path, extra=None) + + with self.temp_server( + handler=bound_handler_with_path, + # Enable deprecation warning and announce deprecation in 11.0. + # deprecation_warnings=["remove second argument of ws_handler"], + ): + with self.temp_client("/path"): + self.assertEqual( + self.loop.run_until_complete(self.client.recv()), + "/path", + ) + + async def process_request_OK(path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(process_request=process_request_OK) + def test_process_request_argument(self): + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + def legacy_process_request_OK(path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(process_request=legacy_process_request_OK) + def test_process_request_argument_backwards_compatibility(self): + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + self.assertDeprecationWarnings( + recorded_warnings, ["declare process_request as a coroutine"] + ) + + class ProcessRequestOKServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(create_protocol=ProcessRequestOKServerProtocol) + def test_process_request_override(self): + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + class LegacyProcessRequestOKServerProtocol(WebSocketServerProtocol): + def process_request(self, path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(create_protocol=LegacyProcessRequestOKServerProtocol) + def test_process_request_override_backwards_compatibility(self): + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + self.assertDeprecationWarnings( + recorded_warnings, ["declare process_request as a coroutine"] + ) + + def select_subprotocol_chat(client_subprotocols, server_subprotocols): + return "chat" + + @with_server( + subprotocols=["superchat", "chat"], select_subprotocol=select_subprotocol_chat + ) + @with_client("/subprotocol", subprotocols=["superchat", "chat"]) + def test_select_subprotocol_argument(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr("chat")) + self.assertEqual(self.client.subprotocol, "chat") + + class SelectSubprotocolChatServerProtocol(WebSocketServerProtocol): + def select_subprotocol(self, client_subprotocols, server_subprotocols): + return "chat" + + @with_server( + subprotocols=["superchat", "chat"], + create_protocol=SelectSubprotocolChatServerProtocol, + ) + @with_client("/subprotocol", subprotocols=["superchat", "chat"]) + def test_select_subprotocol_override(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr("chat")) + self.assertEqual(self.client.subprotocol, "chat") + + @with_server() + @with_client("/deprecated_attributes") + def test_protocol_deprecated_attributes(self): + # The test could be connecting with IPv6 or IPv4. + expected_client_attrs = [ + server_socket.getsockname()[:2] + (self.secure,) + for server_socket in self.server.sockets + ] + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + client_attrs = (self.client.host, self.client.port, self.client.secure) + self.assertDeprecationWarnings( + recorded_warnings, + [ + "use remote_address[0] instead of host", + "use remote_address[1] instead of port", + "don't use secure", + ], + ) + self.assertIn(client_attrs, expected_client_attrs) + + expected_server_attrs = ("localhost", 0, self.secure) + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.loop.run_until_complete(self.client.send("")) + server_attrs = self.loop.run_until_complete(self.client.recv()) + self.assertDeprecationWarnings( + recorded_warnings, + [ + "use local_address[0] instead of host", + "use local_address[1] instead of port", + "don't use secure", + ], + ) + self.assertEqual(server_attrs, repr(expected_server_attrs)) + + @with_server() + @with_client("/path") + def test_protocol_path(self): + client_path = self.client.path + self.assertEqual(client_path, "/path") + server_path = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_path, "/path") + + @with_server() + @with_client("/headers") + def test_protocol_headers(self): + client_req = self.client.request_headers + client_resp = self.client.response_headers + self.assertEqual(client_req["User-Agent"], USER_AGENT) + self.assertEqual(client_resp["Server"], USER_AGENT) + server_req = self.loop.run_until_complete(self.client.recv()) + server_resp = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_req, repr(client_req)) + self.assertEqual(server_resp, repr(client_resp)) + + @with_server() + @with_client("/headers", extra_headers={"X-Spam": "Eggs"}) + def test_protocol_custom_request_headers(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertIn("('X-Spam', 'Eggs')", req_headers) + + @with_server() + @with_client("/headers", extra_headers={"User-Agent": "websockets"}) + def test_protocol_custom_user_agent_header_legacy(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertEqual(req_headers.count("User-Agent"), 1) + self.assertIn("('User-Agent', 'websockets')", req_headers) + + @with_server() + @with_client("/headers", user_agent_header=None) + def test_protocol_no_user_agent_header(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertNotIn("User-Agent", req_headers) + + @with_server() + @with_client("/headers", user_agent_header="websockets") + def test_protocol_custom_user_agent_header(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertEqual(req_headers.count("User-Agent"), 1) + self.assertIn("('User-Agent', 'websockets')", req_headers) + + @with_server(extra_headers=lambda p, r: {"X-Spam": "Eggs"}) + @with_client("/headers") + def test_protocol_custom_response_headers_callable(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertIn("('X-Spam', 'Eggs')", resp_headers) + + @with_server(extra_headers=lambda p, r: None) + @with_client("/headers") + def test_protocol_custom_response_headers_callable_none(self): + self.loop.run_until_complete(self.client.recv()) # doesn't crash + self.loop.run_until_complete(self.client.recv()) # nothing to check + + @with_server(extra_headers={"X-Spam": "Eggs"}) + @with_client("/headers") + def test_protocol_custom_response_headers(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertIn("('X-Spam', 'Eggs')", resp_headers) + + @with_server(extra_headers={"Server": "websockets"}) + @with_client("/headers") + def test_protocol_custom_server_header_legacy(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(resp_headers.count("Server"), 1) + self.assertIn("('Server', 'websockets')", resp_headers) + + @with_server(server_header=None) + @with_client("/headers") + def test_protocol_no_server_header(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertNotIn("Server", resp_headers) + + @with_server(server_header="websockets") + @with_client("/headers") + def test_protocol_custom_server_header(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(resp_headers.count("Server"), 1) + self.assertIn("('Server', 'websockets')", resp_headers) + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_http_request_http_endpoint(self): + # Making an HTTP request to an HTTP endpoint succeeds. + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + self.assertEqual(response.read(), b"status = green\n") + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_http_request_ws_endpoint(self): + # Making an HTTP request to a WS endpoint fails. + with self.assertRaises(urllib.error.HTTPError) as raised: + self.loop.run_until_complete(self.make_http_request()) + + self.assertEqual(raised.exception.code, 426) + self.assertEqual(raised.exception.headers["Upgrade"], "websocket") + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_ws_connection_http_endpoint(self): + # Making a WS connection to an HTTP endpoint fails. + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client("/__health__/") + + self.assertEqual(raised.exception.status_code, 200) + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_ws_connection_ws_endpoint(self): + # Making a WS connection to a WS endpoint succeeds. + self.start_client() + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + self.stop_client() + + @with_server(create_protocol=HealthCheckServerProtocol, server_header=None) + def test_http_request_no_server_header(self): + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertNotIn("Server", response.headers) + + @with_server(create_protocol=HealthCheckServerProtocol, server_header="websockets") + def test_http_request_custom_server_header(self): + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertEqual(response.headers["Server"], "websockets") + + @with_server(create_protocol=ProcessRequestReturningIntProtocol) + def test_process_request_returns_int_status(self): + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + self.assertEqual(response.read(), b"OK\n") + + def assert_client_raises_code(self, status_code): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + self.assertEqual(raised.exception.status_code, status_code) + + @with_server(create_protocol=UnauthorizedServerProtocol) + def test_server_create_protocol(self): + self.assert_client_raises_code(401) + + def create_unauthorized_server_protocol(*args, **kwargs): + return UnauthorizedServerProtocol(*args, **kwargs) + + @with_server(create_protocol=create_unauthorized_server_protocol) + def test_server_create_protocol_function(self): + self.assert_client_raises_code(401) + + @with_server( + klass=UnauthorizedServerProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_server_klass_backwards_compatibility(self): + self.assert_client_raises_code(401) + + @with_server( + create_protocol=ForbiddenServerProtocol, + klass=UnauthorizedServerProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_server_create_protocol_over_klass(self): + self.assert_client_raises_code(403) + + @with_server() + @with_client("/path", create_protocol=FooClientProtocol) + def test_client_create_protocol(self): + self.assertIsInstance(self.client, FooClientProtocol) + + @with_server() + @with_client( + "/path", + create_protocol=(lambda *args, **kwargs: FooClientProtocol(*args, **kwargs)), + ) + def test_client_create_protocol_function(self): + self.assertIsInstance(self.client, FooClientProtocol) + + @with_server() + @with_client( + "/path", + klass=FooClientProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_client_klass(self): + self.assertIsInstance(self.client, FooClientProtocol) + + @with_server() + @with_client( + "/path", + create_protocol=BarClientProtocol, + klass=FooClientProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_client_create_protocol_over_klass(self): + self.assertIsInstance(self.client, BarClientProtocol) + + @with_server(close_timeout=7) + @with_client("/close_timeout") + def test_server_close_timeout(self): + close_timeout = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(eval(close_timeout), 7) + + @with_server(timeout=6, deprecation_warnings=["rename timeout to close_timeout"]) + @with_client("/close_timeout") + def test_server_timeout_backwards_compatibility(self): + close_timeout = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(eval(close_timeout), 6) + + @with_server( + close_timeout=7, + timeout=6, + deprecation_warnings=["rename timeout to close_timeout"], + ) + @with_client("/close_timeout") + def test_server_close_timeout_over_timeout(self): + close_timeout = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(eval(close_timeout), 7) + + @with_server() + @with_client("/close_timeout", close_timeout=7) + def test_client_close_timeout(self): + self.assertEqual(self.client.close_timeout, 7) + + @with_server() + @with_client( + "/close_timeout", + timeout=6, + deprecation_warnings=["rename timeout to close_timeout"], + ) + def test_client_timeout_backwards_compatibility(self): + self.assertEqual(self.client.close_timeout, 6) + + @with_server() + @with_client( + "/close_timeout", + close_timeout=7, + timeout=6, + deprecation_warnings=["rename timeout to close_timeout"], + ) + def test_client_close_timeout_over_timeout(self): + self.assertEqual(self.client.close_timeout, 7) + + @with_server() + @with_client("/extensions") + def test_no_extension(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + def test_extension(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([NoOpExtension()])) + self.assertEqual(repr(self.client.extensions), repr([NoOpExtension()])) + + @with_server() + @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + def test_extension_not_accepted(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @with_client("/extensions") + def test_extension_not_requested(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server(extensions=[ServerNoOpExtensionFactory([("foo", None)])]) + def test_extension_client_rejection(self): + with self.assertRaises(NegotiationError): + self.start_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + + @with_server( + extensions=[ + # No match because the client doesn't send client_max_window_bits. + ServerPerMessageDeflateFactory( + client_max_window_bits=10, + require_client_max_window_bits=True, + ), + ServerPerMessageDeflateFactory(), + ] + ) + @with_client( + "/extensions", + extensions=[ + ClientPerMessageDeflateFactory(client_max_window_bits=None), + ], + ) + def test_extension_no_match_then_match(self): + # The order requested by the client has priority. + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual( + server_extensions, repr([PerMessageDeflate(False, False, 15, 15)]) + ) + self.assertEqual( + repr(self.client.extensions), + repr([PerMessageDeflate(False, False, 15, 15)]), + ) + + @with_server(extensions=[ServerPerMessageDeflateFactory()]) + @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + def test_extension_mismatch(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server( + extensions=[ServerNoOpExtensionFactory(), ServerPerMessageDeflateFactory()] + ) + @with_client( + "/extensions", + extensions=[ClientPerMessageDeflateFactory(), ClientNoOpExtensionFactory()], + ) + def test_extension_order(self): + # The order requested by the client has priority. + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual( + server_extensions, + repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]), + ) + self.assertEqual( + repr(self.client.extensions), + repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]), + ) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions") + def test_extensions_error(self, _process_extensions): + _process_extensions.return_value = "x-no-op", [NoOpExtension()] + + with self.assertRaises(NegotiationError): + self.start_client( + "/extensions", extensions=[ClientPerMessageDeflateFactory()] + ) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions") + def test_extensions_error_no_extensions(self, _process_extensions): + _process_extensions.return_value = "x-no-op", [NoOpExtension()] + + with self.assertRaises(InvalidHandshake): + self.start_client("/extensions") + + @with_server(compression="deflate") + @with_client("/extensions", compression="deflate") + def test_compression_deflate(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual( + server_extensions, repr([PerMessageDeflate(False, False, 12, 12)]) + ) + self.assertEqual( + repr(self.client.extensions), + repr([PerMessageDeflate(False, False, 12, 12)]), + ) + + def test_compression_unsupported_server(self): + with self.assertRaises(ValueError): + self.start_server(compression="xz") + + @with_server() + def test_compression_unsupported_client(self): + with self.assertRaises(ValueError): + self.start_client(compression="xz") + + @with_server() + @with_client("/subprotocol") + def test_no_subprotocol(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server(subprotocols=["superchat", "chat"]) + @with_client("/subprotocol", subprotocols=["otherchat", "chat"]) + def test_subprotocol(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr("chat")) + self.assertEqual(self.client.subprotocol, "chat") + + def test_invalid_subprotocol_server(self): + with self.assertRaises(TypeError): + self.start_server(subprotocols="sip") + + @with_server() + def test_invalid_subprotocol_client(self): + with self.assertRaises(TypeError): + self.start_client(subprotocols="sip") + + @with_server(subprotocols=["superchat"]) + @with_client("/subprotocol", subprotocols=["otherchat"]) + def test_subprotocol_not_accepted(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server() + @with_client("/subprotocol", subprotocols=["otherchat", "chat"]) + def test_subprotocol_not_offered(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server(subprotocols=["superchat", "chat"]) + @with_client("/subprotocol") + def test_subprotocol_not_requested(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server(subprotocols=["superchat"]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol") + def test_subprotocol_error(self, _process_subprotocol): + _process_subprotocol.return_value = "superchat" + + with self.assertRaises(NegotiationError): + self.start_client("/subprotocol", subprotocols=["otherchat"]) + self.run_loop_once() + + @with_server(subprotocols=["superchat"]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol") + def test_subprotocol_error_no_subprotocols(self, _process_subprotocol): + _process_subprotocol.return_value = "superchat" + + with self.assertRaises(InvalidHandshake): + self.start_client("/subprotocol") + self.run_loop_once() + + @with_server(subprotocols=["superchat", "chat"]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol") + def test_subprotocol_error_two_subprotocols(self, _process_subprotocol): + _process_subprotocol.return_value = "superchat, chat" + + with self.assertRaises(InvalidHandshake): + self.start_client("/subprotocol", subprotocols=["superchat", "chat"]) + self.run_loop_once() + + @with_server() + @unittest.mock.patch("websockets.legacy.server.read_request") + def test_server_receives_malformed_request(self, _read_request): + _read_request.side_effect = ValueError("read_request failed") + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server() + @unittest.mock.patch("websockets.legacy.client.read_response") + def test_client_receives_malformed_response(self, _read_response): + _read_response.side_effect = ValueError("read_response failed") + + with self.assertRaises(InvalidHandshake): + self.start_client() + self.run_loop_once() + + @with_server() + @unittest.mock.patch("websockets.legacy.client.build_request") + def test_client_sends_invalid_handshake_request(self, _build_request): + def wrong_build_request(headers): + return "42" + + _build_request.side_effect = wrong_build_request + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server() + @unittest.mock.patch("websockets.legacy.server.build_response") + def test_server_sends_invalid_handshake_response(self, _build_response): + def wrong_build_response(headers, key): + return build_response(headers, "42") + + _build_response.side_effect = wrong_build_response + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server() + @unittest.mock.patch("websockets.legacy.client.read_response") + def test_server_does_not_switch_protocols(self, _read_response): + async def wrong_read_response(stream): + status_code, reason, headers = await read_response(stream) + return 400, "Bad Request", headers + + _read_response.side_effect = wrong_read_response + + with self.assertRaises(InvalidStatusCode): + self.start_client() + self.run_loop_once() + + @with_server() + @unittest.mock.patch( + "websockets.legacy.server.WebSocketServerProtocol.process_request" + ) + def test_server_error_in_handshake(self, _process_request): + _process_request.side_effect = Exception("process_request crashed") + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server(create_protocol=SlowOpeningHandshakeProtocol) + def test_client_connect_canceled_during_handshake(self): + sock = socket.create_connection(get_server_address(self.server)) + sock.send(b"") # socket is connected + + async def cancelled_client(): + start_client = connect(get_server_uri(self.server), sock=sock) + async with asyncio_timeout(5 * MS): + await start_client + + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete(cancelled_client()) + + with self.assertRaises(OSError): + sock.send(b"") # socket is closed + + @with_server() + @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.send") + def test_server_handler_crashes(self, send): + send.side_effect = ValueError("send failed") + + with self.temp_client(): + self.loop.run_until_complete(self.client.send("Hello!")) + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.client.recv()) + + # Connection ends with an unexpected error. + self.assertEqual(self.client.close_code, CloseCode.INTERNAL_ERROR) + + @with_server() + @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.close") + def test_server_close_crashes(self, close): + close.side_effect = ValueError("close failed") + + with self.temp_client(): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + # Connection ends with an abnormal closure. + self.assertEqual(self.client.close_code, CloseCode.ABNORMAL_CLOSURE) + + @with_server() + @with_client() + @unittest.mock.patch.object(WebSocketClientProtocol, "handshake") + def test_client_closes_connection_before_handshake(self, handshake): + # We have mocked the handshake() method to prevent the client from + # performing the opening handshake. Force it to close the connection. + self.client.transport.close() + # The server should stop properly anyway. It used to hang because the + # task handling the connection was waiting for the opening handshake. + + @with_server(create_protocol=SlowOpeningHandshakeProtocol) + def test_server_shuts_down_during_opening_handshake(self): + self.loop.call_later(5 * MS, self.server.close) + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + exception = raised.exception + self.assertEqual( + str(exception), "server rejected WebSocket connection: HTTP 503" + ) + self.assertEqual(exception.status_code, 503) + + @with_server() + def test_server_shuts_down_during_connection_handling(self): + with self.temp_client(): + server_ws = next(iter(self.server.websockets)) + self.server.close() + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + # Server closed the connection with 1001 Going Away. + self.assertEqual(self.client.close_code, CloseCode.GOING_AWAY) + self.assertEqual(server_ws.close_code, CloseCode.GOING_AWAY) + + @with_server() + def test_server_shuts_down_gracefully_during_connection_handling(self): + with self.temp_client(): + server_ws = next(iter(self.server.websockets)) + self.server.close(close_connections=False) + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + # Client closed the connection with 1000 OK. + self.assertEqual(self.client.close_code, CloseCode.NORMAL_CLOSURE) + self.assertEqual(server_ws.close_code, CloseCode.NORMAL_CLOSURE) + + @with_server() + def test_server_shuts_down_and_waits_until_handlers_terminate(self): + # This handler waits a bit after the connection is closed in order + # to test that wait_closed() really waits for handlers to complete. + self.start_client("/slow_stop") + server_ws = next(iter(self.server.websockets)) + + # Test that the handler task keeps running after close(). + self.server.close() + self.loop.run_until_complete(asyncio.sleep(MS)) + self.assertFalse(server_ws.handler_task.done()) + + # Test that the handler task terminates before wait_closed() returns. + self.loop.run_until_complete(self.server.wait_closed()) + self.assertTrue(server_ws.handler_task.done()) + + @with_server(create_protocol=ForbiddenServerProtocol) + def test_invalid_status_error_during_client_connect(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + exception = raised.exception + self.assertEqual( + str(exception), "server rejected WebSocket connection: HTTP 403" + ) + self.assertEqual(exception.status_code, 403) + + @with_server() + @unittest.mock.patch( + "websockets.legacy.server.WebSocketServerProtocol.write_http_response" + ) + @unittest.mock.patch( + "websockets.legacy.server.WebSocketServerProtocol.read_http_request" + ) + def test_connection_error_during_opening_handshake( + self, _read_http_request, _write_http_response + ): + _read_http_request.side_effect = ConnectionError + + # This exception is currently platform-dependent. It was observed to + # be ConnectionResetError on Linux in the non-TLS case, and + # InvalidMessage otherwise (including both Linux and macOS). This + # doesn't matter though since this test is primarily for testing a + # code path on the server side. + with self.assertRaises(Exception): + self.start_client() + + # No response must not be written if the network connection is broken. + _write_http_response.assert_not_called() + + @with_server() + @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.close") + def test_connection_error_during_closing_handshake(self, close): + close.side_effect = ConnectionError + + with self.temp_client(): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + # Connection ends with an abnormal closure. + self.assertEqual(self.client.close_code, CloseCode.ABNORMAL_CLOSURE) + + +class ClientServerTests( + CommonClientServerTests, ClientServerTestsMixin, AsyncioTestCase +): + pass + + +class SecureClientServerTests( + CommonClientServerTests, SecureClientServerTestsMixin, AsyncioTestCase +): + # The implementation of this test makes it hard to run it over TLS. + test_client_connect_canceled_during_handshake = None + + # TLS over Unix sockets doesn't make sense. + test_unix_socket = None + + # This test fails under PyPy due to a difference with CPython. + if platform.python_implementation() == "PyPy": # pragma: no cover + test_http_request_ws_endpoint = None + + @with_server() + def test_ws_uri_is_rejected(self): + with self.assertRaises(ValueError): + self.start_client( + uri=get_server_uri(self.server, secure=False), ssl=self.client_context + ) + + def test_redirect_insecure(self): + with temp_test_redirecting_server(self): + with self.assertRaises(InvalidHandshake): + with self.temp_client("/force_insecure"): + self.fail("did not raise") + + +class ClientServerOriginTests(ClientServerTestsMixin, AsyncioTestCase): + @with_server(origins=["http://localhost"]) + @with_client(origin="http://localhost") + def test_checking_origin_succeeds(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!") + + @with_server(origins=["http://localhost"]) + def test_checking_origin_fails(self): + with self.assertRaisesRegex( + InvalidHandshake, "server rejected WebSocket connection: HTTP 403" + ): + self.start_client(origin="http://otherhost") + + @with_server(origins=["http://localhost"]) + def test_checking_origins_fails_with_multiple_headers(self): + with self.assertRaisesRegex( + InvalidHandshake, "server rejected WebSocket connection: HTTP 400" + ): + self.start_client( + origin="http://localhost", + extra_headers=[("Origin", "http://otherhost")], + ) + + @with_server(origins=[None]) + @with_client() + def test_checking_lack_of_origin_succeeds(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!") + + @with_server(origins=[""]) + # The deprecation warning is raised when a client connects to the server. + @with_client(deprecation_warnings=["use None instead of '' in origins"]) + def test_checking_lack_of_origin_succeeds_backwards_compatibility(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!") + + +@unittest.skipIf( + sys.version_info[:2] >= (3, 11), "asyncio.coroutine has been removed in Python 3.11" +) +class YieldFromTests(ClientServerTestsMixin, AsyncioTestCase): # pragma: no cover + @with_server() + def test_client(self): + # @asyncio.coroutine is deprecated on Python ≥ 3.8 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + @asyncio.coroutine + def run_client(): + # Yield from connect. + client = yield from connect(get_server_uri(self.server)) + self.assertEqual(client.state, State.OPEN) + yield from client.close() + self.assertEqual(client.state, State.CLOSED) + + self.loop.run_until_complete(run_client()) + + def test_server(self): + # @asyncio.coroutine is deprecated on Python ≥ 3.8 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + @asyncio.coroutine + def run_server(): + # Yield from serve. + server = yield from serve(default_handler, "localhost", 0) + self.assertTrue(server.sockets) + server.close() + yield from server.wait_closed() + self.assertFalse(server.sockets) + + self.loop.run_until_complete(run_server()) + + +class AsyncAwaitTests(ClientServerTestsMixin, AsyncioTestCase): + @with_server() + def test_client(self): + async def run_client(): + # Await connect. + client = await connect(get_server_uri(self.server)) + self.assertEqual(client.state, State.OPEN) + await client.close() + self.assertEqual(client.state, State.CLOSED) + + self.loop.run_until_complete(run_client()) + + def test_server(self): + async def run_server(): + # Await serve. + server = await serve(default_handler, "localhost", 0) + self.assertTrue(server.sockets) + server.close() + await server.wait_closed() + self.assertFalse(server.sockets) + + self.loop.run_until_complete(run_server()) + + +class ContextManagerTests(ClientServerTestsMixin, AsyncioTestCase): + @with_server() + def test_client(self): + async def run_client(): + # Use connect as an asynchronous context manager. + async with connect(get_server_uri(self.server)) as client: + self.assertEqual(client.state, State.OPEN) + + # Check that exiting the context manager closed the connection. + self.assertEqual(client.state, State.CLOSED) + + self.loop.run_until_complete(run_client()) + + def test_server(self): + async def run_server(): + # Use serve as an asynchronous context manager. + async with serve(default_handler, "localhost", 0) as server: + self.assertTrue(server.sockets) + + # Check that exiting the context manager closed the server. + self.assertFalse(server.sockets) + + self.loop.run_until_complete(run_server()) + + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") + def test_unix_server(self): + async def run_server(path): + async with unix_serve(default_handler, path) as server: + self.assertTrue(server.sockets) + + # Check that exiting the context manager closed the server. + self.assertFalse(server.sockets) + + with temp_unix_socket_path() as path: + self.loop.run_until_complete(run_server(path)) + + +class AsyncIteratorTests(ClientServerTestsMixin, AsyncioTestCase): + # This is a protocol-level feature, but since it's a high-level API, it is + # much easier to exercise at the client or server level. + + MESSAGES = ["3", "2", "1", "Fire!"] + + async def echo_handler(ws): + for message in AsyncIteratorTests.MESSAGES: + await ws.send(message) + + @with_server(handler=echo_handler) + def test_iterate_on_messages(self): + messages = [] + + async def run_client(): + nonlocal messages + async with connect(get_server_uri(self.server)) as ws: + async for message in ws: + messages.append(message) + + self.loop.run_until_complete(run_client()) + + self.assertEqual(messages, self.MESSAGES) + + async def echo_handler_going_away(ws): + for message in AsyncIteratorTests.MESSAGES: + await ws.send(message) + await ws.close(CloseCode.GOING_AWAY) + + @with_server(handler=echo_handler_going_away) + def test_iterate_on_messages_going_away_exit_ok(self): + messages = [] + + async def run_client(): + nonlocal messages + async with connect(get_server_uri(self.server)) as ws: + async for message in ws: + messages.append(message) + + self.loop.run_until_complete(run_client()) + + self.assertEqual(messages, self.MESSAGES) + + async def echo_handler_internal_error(ws): + for message in AsyncIteratorTests.MESSAGES: + await ws.send(message) + await ws.close(CloseCode.INTERNAL_ERROR) + + @with_server(handler=echo_handler_internal_error) + def test_iterate_on_messages_internal_error_exit_not_ok(self): + messages = [] + + async def run_client(): + nonlocal messages + async with connect(get_server_uri(self.server)) as ws: + async for message in ws: + messages.append(message) + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(run_client()) + + self.assertEqual(messages, self.MESSAGES) + + +class ReconnectionTests(ClientServerTestsMixin, AsyncioTestCase): + async def echo_handler(ws): + async for msg in ws: + await ws.send(msg) + + service_available = True + + async def maybe_service_unavailable(path, headers): + if not ReconnectionTests.service_available: + return http.HTTPStatus.SERVICE_UNAVAILABLE, [], b"" + + async def disable_server(self, duration): + ReconnectionTests.service_available = False + await asyncio.sleep(duration) + ReconnectionTests.service_available = True + + @with_server(handler=echo_handler, process_request=maybe_service_unavailable) + def test_reconnect(self): + # Big, ugly integration test :-( + + async def run_client(): + iteration = 0 + connect_inst = connect(get_server_uri(self.server)) + connect_inst.BACKOFF_MIN = 10 * MS + connect_inst.BACKOFF_MAX = 99 * MS + connect_inst.BACKOFF_INITIAL = 0 + # coverage has a hard time dealing with this code - I give up. + async for ws in connect_inst: # pragma: no cover + await ws.send("spam") + msg = await ws.recv() + self.assertEqual(msg, "spam") + + iteration += 1 + if iteration == 1: + # Exit block normally. + pass + elif iteration == 2: + # Disable server for a little bit + asyncio.create_task(self.disable_server(50 * MS)) + await asyncio.sleep(0) + elif iteration == 3: + # Exit block after catching connection error. + server_ws = next(iter(self.server.websockets)) + await server_ws.close() + with self.assertRaises(ConnectionClosed): + await ws.recv() + else: + # Exit block with an exception. + raise Exception("BOOM") + pass # work around bug in coverage + + with self.assertLogs("websockets", logging.INFO) as logs: + with self.assertRaisesRegex(Exception, "BOOM"): + self.loop.run_until_complete(run_client()) + + # Iteration 1 + self.assertEqual( + [record.getMessage() for record in logs.records][:2], + [ + "connection open", + "connection closed", + ], + ) + # Iteration 2 + self.assertEqual( + [record.getMessage() for record in logs.records][2:4], + [ + "connection open", + "connection closed", + ], + ) + # Iteration 3 + self.assertEqual( + [record.getMessage() for record in logs.records][4:-1], + [ + "connection rejected (503 Service Unavailable)", + "connection closed", + "! connect failed; reconnecting in 0.0 seconds", + ] + + [ + "connection rejected (503 Service Unavailable)", + "connection closed", + "! connect failed again; retrying in 0 seconds", + ] + * ((len(logs.records) - 8) // 3) + + [ + "connection open", + "connection closed", + ], + ) + # Iteration 4 + self.assertEqual( + [record.getMessage() for record in logs.records][-1:], + [ + "connection open", + ], + ) + + +class LoggerTests(ClientServerTestsMixin, AsyncioTestCase): + def test_logger_client(self): + with self.assertLogs("test.server", logging.DEBUG) as server_logs: + self.start_server(logger=logging.getLogger("test.server")) + with self.assertLogs("test.client", logging.DEBUG) as client_logs: + self.start_client(logger=logging.getLogger("test.client")) + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + self.stop_client() + self.stop_server() + + self.assertGreater(len(server_logs.records), 0) + self.assertGreater(len(client_logs.records), 0) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_framing.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_framing.py new file mode 100644 index 0000000000..e1e4c891b0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_framing.py @@ -0,0 +1,206 @@ +import asyncio +import codecs +import dataclasses +import unittest +import unittest.mock +import warnings + +from websockets.exceptions import PayloadTooBig, ProtocolError +from websockets.frames import OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, OP_TEXT, CloseCode +from websockets.legacy.framing import * + +from .utils import AsyncioTestCase + + +class FramingTests(AsyncioTestCase): + def decode(self, message, mask=False, max_size=None, extensions=None): + stream = asyncio.StreamReader(loop=self.loop) + stream.feed_data(message) + stream.feed_eof() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + frame = self.loop.run_until_complete( + Frame.read( + stream.readexactly, + mask=mask, + max_size=max_size, + extensions=extensions, + ) + ) + # Make sure all the data was consumed. + self.assertTrue(stream.at_eof()) + return frame + + def encode(self, frame, mask=False, extensions=None): + write = unittest.mock.Mock() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + frame.write(write, mask=mask, extensions=extensions) + # Ensure the entire frame is sent with a single call to write(). + # Multiple calls cause TCP fragmentation and degrade performance. + self.assertEqual(write.call_count, 1) + # The frame data is the single positional argument of that call. + self.assertEqual(len(write.call_args[0]), 1) + self.assertEqual(len(write.call_args[1]), 0) + return write.call_args[0][0] + + def round_trip(self, message, expected, mask=False, extensions=None): + decoded = self.decode(message, mask, extensions=extensions) + decoded.check() + self.assertEqual(decoded, expected) + encoded = self.encode(decoded, mask, extensions=extensions) + if mask: # non-deterministic encoding + decoded = self.decode(encoded, mask, extensions=extensions) + self.assertEqual(decoded, expected) + else: # deterministic encoding + self.assertEqual(encoded, message) + + def test_text(self): + self.round_trip(b"\x81\x04Spam", Frame(True, OP_TEXT, b"Spam")) + + def test_text_masked(self): + self.round_trip( + b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5", + Frame(True, OP_TEXT, b"Spam"), + mask=True, + ) + + def test_binary(self): + self.round_trip(b"\x82\x04Eggs", Frame(True, OP_BINARY, b"Eggs")) + + def test_binary_masked(self): + self.round_trip( + b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa", + Frame(True, OP_BINARY, b"Eggs"), + mask=True, + ) + + def test_non_ascii_text(self): + self.round_trip( + b"\x81\x05caf\xc3\xa9", Frame(True, OP_TEXT, "café".encode("utf-8")) + ) + + def test_non_ascii_text_masked(self): + self.round_trip( + b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd", + Frame(True, OP_TEXT, "café".encode("utf-8")), + mask=True, + ) + + def test_close(self): + self.round_trip(b"\x88\x00", Frame(True, OP_CLOSE, b"")) + + def test_ping(self): + self.round_trip(b"\x89\x04ping", Frame(True, OP_PING, b"ping")) + + def test_pong(self): + self.round_trip(b"\x8a\x04pong", Frame(True, OP_PONG, b"pong")) + + def test_long(self): + self.round_trip( + b"\x82\x7e\x00\x7e" + 126 * b"a", Frame(True, OP_BINARY, 126 * b"a") + ) + + def test_very_long(self): + self.round_trip( + b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a", + Frame(True, OP_BINARY, 65536 * b"a"), + ) + + def test_payload_too_big(self): + with self.assertRaises(PayloadTooBig): + self.decode(b"\x82\x7e\x04\x01" + 1025 * b"a", max_size=1024) + + def test_bad_reserved_bits(self): + for encoded in [b"\xc0\x00", b"\xa0\x00", b"\x90\x00"]: + with self.subTest(encoded=encoded): + with self.assertRaises(ProtocolError): + self.decode(encoded) + + def test_good_opcode(self): + for opcode in list(range(0x00, 0x03)) + list(range(0x08, 0x0B)): + encoded = bytes([0x80 | opcode, 0]) + with self.subTest(encoded=encoded): + self.decode(encoded) # does not raise an exception + + def test_bad_opcode(self): + for opcode in list(range(0x03, 0x08)) + list(range(0x0B, 0x10)): + encoded = bytes([0x80 | opcode, 0]) + with self.subTest(encoded=encoded): + with self.assertRaises(ProtocolError): + self.decode(encoded) + + def test_mask_flag(self): + # Mask flag correctly set. + self.decode(b"\x80\x80\x00\x00\x00\x00", mask=True) + # Mask flag incorrectly unset. + with self.assertRaises(ProtocolError): + self.decode(b"\x80\x80\x00\x00\x00\x00") + # Mask flag correctly unset. + self.decode(b"\x80\x00") + # Mask flag incorrectly set. + with self.assertRaises(ProtocolError): + self.decode(b"\x80\x00", mask=True) + + def test_control_frame_max_length(self): + # At maximum allowed length. + self.decode(b"\x88\x7e\x00\x7d" + 125 * b"a") + # Above maximum allowed length. + with self.assertRaises(ProtocolError): + self.decode(b"\x88\x7e\x00\x7e" + 126 * b"a") + + def test_fragmented_control_frame(self): + # Fin bit correctly set. + self.decode(b"\x88\x00") + # Fin bit incorrectly unset. + with self.assertRaises(ProtocolError): + self.decode(b"\x08\x00") + + def test_extensions(self): + class Rot13: + @staticmethod + def encode(frame): + assert frame.opcode == OP_TEXT + text = frame.data.decode() + data = codecs.encode(text, "rot13").encode() + return dataclasses.replace(frame, data=data) + + # This extensions is symmetrical. + @staticmethod + def decode(frame, *, max_size=None): + return Rot13.encode(frame) + + self.round_trip( + b"\x81\x05uryyb", Frame(True, OP_TEXT, b"hello"), extensions=[Rot13()] + ) + + +class ParseAndSerializeCloseTests(unittest.TestCase): + def assertCloseData(self, code, reason, data): + """ + Serializing code / reason yields data. Parsing data yields code / reason. + + """ + serialized = serialize_close(code, reason) + self.assertEqual(serialized, data) + parsed = parse_close(data) + self.assertEqual(parsed, (code, reason)) + + def test_parse_close_and_serialize_close(self): + self.assertCloseData(CloseCode.NORMAL_CLOSURE, "", b"\x03\xe8") + self.assertCloseData(CloseCode.NORMAL_CLOSURE, "OK", b"\x03\xe8OK") + + def test_parse_close_empty(self): + self.assertEqual(parse_close(b""), (CloseCode.NO_STATUS_RCVD, "")) + + def test_parse_close_errors(self): + with self.assertRaises(ProtocolError): + parse_close(b"\x03") + with self.assertRaises(ProtocolError): + parse_close(b"\x03\xe7") + with self.assertRaises(UnicodeDecodeError): + parse_close(b"\x03\xe8\xff\xff") + + def test_serialize_close_errors(self): + with self.assertRaises(ProtocolError): + serialize_close(999, "") diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_handshake.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_handshake.py new file mode 100644 index 0000000000..661ae64fc4 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_handshake.py @@ -0,0 +1,184 @@ +import contextlib +import unittest + +from websockets.datastructures import Headers +from websockets.exceptions import ( + InvalidHandshake, + InvalidHeader, + InvalidHeaderValue, + InvalidUpgrade, +) +from websockets.legacy.handshake import * +from websockets.utils import accept_key + + +class HandshakeTests(unittest.TestCase): + def test_round_trip(self): + request_headers = Headers() + request_key = build_request(request_headers) + response_key = check_request(request_headers) + self.assertEqual(request_key, response_key) + response_headers = Headers() + build_response(response_headers, response_key) + check_response(response_headers, request_key) + + @contextlib.contextmanager + def assertValidRequestHeaders(self): + """ + Provide request headers for modification. + + Assert that the transformation kept them valid. + + """ + headers = Headers() + build_request(headers) + yield headers + check_request(headers) + + @contextlib.contextmanager + def assertInvalidRequestHeaders(self, exc_type): + """ + Provide request headers for modification. + + Assert that the transformation made them invalid. + + """ + headers = Headers() + build_request(headers) + yield headers + assert issubclass(exc_type, InvalidHandshake) + with self.assertRaises(exc_type): + check_request(headers) + + def test_request_invalid_connection(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + headers["Connection"] = "Downgrade" + + def test_request_missing_connection(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + + def test_request_additional_connection(self): + with self.assertValidRequestHeaders() as headers: + headers["Connection"] = "close" + + def test_request_invalid_upgrade(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + headers["Upgrade"] = "socketweb" + + def test_request_missing_upgrade(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + + def test_request_additional_upgrade(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + headers["Upgrade"] = "socketweb" + + def test_request_invalid_key_not_base64(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Key"] + headers["Sec-WebSocket-Key"] = "!@#$%^&*()" + + def test_request_invalid_key_not_well_padded(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Key"] + headers["Sec-WebSocket-Key"] = "CSIRmL8dWYxeAdr/XpEHRw" + + def test_request_invalid_key_not_16_bytes_long(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Key"] + headers["Sec-WebSocket-Key"] = "ZLpprpvK4PE=" + + def test_request_missing_key(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + del headers["Sec-WebSocket-Key"] + + def test_request_additional_key(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + # This duplicates the Sec-WebSocket-Key header. + headers["Sec-WebSocket-Key"] = headers["Sec-WebSocket-Key"] + + def test_request_invalid_version(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Version"] + headers["Sec-WebSocket-Version"] = "42" + + def test_request_missing_version(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + del headers["Sec-WebSocket-Version"] + + def test_request_additional_version(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + # This duplicates the Sec-WebSocket-Version header. + headers["Sec-WebSocket-Version"] = headers["Sec-WebSocket-Version"] + + @contextlib.contextmanager + def assertValidResponseHeaders(self, key="CSIRmL8dWYxeAdr/XpEHRw=="): + """ + Provide response headers for modification. + + Assert that the transformation kept them valid. + + """ + headers = Headers() + build_response(headers, key) + yield headers + check_response(headers, key) + + @contextlib.contextmanager + def assertInvalidResponseHeaders(self, exc_type, key="CSIRmL8dWYxeAdr/XpEHRw=="): + """ + Provide response headers for modification. + + Assert that the transformation made them invalid. + + """ + headers = Headers() + build_response(headers, key) + yield headers + assert issubclass(exc_type, InvalidHandshake) + with self.assertRaises(exc_type): + check_response(headers, key) + + def test_response_invalid_connection(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + headers["Connection"] = "Downgrade" + + def test_response_missing_connection(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + + def test_response_additional_connection(self): + with self.assertValidResponseHeaders() as headers: + headers["Connection"] = "close" + + def test_response_invalid_upgrade(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + headers["Upgrade"] = "socketweb" + + def test_response_missing_upgrade(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + + def test_response_additional_upgrade(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + headers["Upgrade"] = "socketweb" + + def test_response_invalid_accept(self): + with self.assertInvalidResponseHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Accept"] + other_key = "1Eq4UDEFQYg3YspNgqxv5g==" + headers["Sec-WebSocket-Accept"] = accept_key(other_key) + + def test_response_missing_accept(self): + with self.assertInvalidResponseHeaders(InvalidHeader) as headers: + del headers["Sec-WebSocket-Accept"] + + def test_response_additional_accept(self): + with self.assertInvalidResponseHeaders(InvalidHeader) as headers: + # This duplicates the Sec-WebSocket-Accept header. + headers["Sec-WebSocket-Accept"] = headers["Sec-WebSocket-Accept"] diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_http.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_http.py new file mode 100644 index 0000000000..15d53e08d2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_http.py @@ -0,0 +1,135 @@ +import asyncio + +from websockets.exceptions import SecurityError +from websockets.legacy.http import * +from websockets.legacy.http import read_headers + +from .utils import AsyncioTestCase + + +class HTTPAsyncTests(AsyncioTestCase): + def setUp(self): + super().setUp() + self.stream = asyncio.StreamReader(loop=self.loop) + + async def test_read_request(self): + # Example from the protocol overview in RFC 6455 + self.stream.feed_data( + b"GET /chat HTTP/1.1\r\n" + b"Host: server.example.com\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + b"Origin: http://example.com\r\n" + b"Sec-WebSocket-Protocol: chat, superchat\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n" + ) + path, headers = await read_request(self.stream) + self.assertEqual(path, "/chat") + self.assertEqual(headers["Upgrade"], "websocket") + + async def test_read_request_empty(self): + self.stream.feed_eof() + with self.assertRaisesRegex( + EOFError, "connection closed while reading HTTP request line" + ): + await read_request(self.stream) + + async def test_read_request_invalid_request_line(self): + self.stream.feed_data(b"GET /\r\n\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP request line: GET /"): + await read_request(self.stream) + + async def test_read_request_unsupported_method(self): + self.stream.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP method: OPTIONS"): + await read_request(self.stream) + + async def test_read_request_unsupported_version(self): + self.stream.feed_data(b"GET /chat HTTP/1.0\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"): + await read_request(self.stream) + + async def test_read_request_invalid_header(self): + self.stream.feed_data(b"GET /chat HTTP/1.1\r\nOops\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"): + await read_request(self.stream) + + async def test_read_response(self): + # Example from the protocol overview in RFC 6455 + self.stream.feed_data( + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + b"Sec-WebSocket-Protocol: chat\r\n" + b"\r\n" + ) + status_code, reason, headers = await read_response(self.stream) + self.assertEqual(status_code, 101) + self.assertEqual(reason, "Switching Protocols") + self.assertEqual(headers["Upgrade"], "websocket") + + async def test_read_response_empty(self): + self.stream.feed_eof() + with self.assertRaisesRegex( + EOFError, "connection closed while reading HTTP status line" + ): + await read_response(self.stream) + + async def test_read_request_invalid_status_line(self): + self.stream.feed_data(b"Hello!\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP status line: Hello!"): + await read_response(self.stream) + + async def test_read_response_unsupported_version(self): + self.stream.feed_data(b"HTTP/1.0 400 Bad Request\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"): + await read_response(self.stream) + + async def test_read_response_invalid_status(self): + self.stream.feed_data(b"HTTP/1.1 OMG WTF\r\n\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP status code: OMG"): + await read_response(self.stream) + + async def test_read_response_unsupported_status(self): + self.stream.feed_data(b"HTTP/1.1 007 My name is Bond\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP status code: 007"): + await read_response(self.stream) + + async def test_read_response_invalid_reason(self): + self.stream.feed_data(b"HTTP/1.1 200 \x7f\r\n\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP reason phrase: \\x7f"): + await read_response(self.stream) + + async def test_read_response_invalid_header(self): + self.stream.feed_data(b"HTTP/1.1 500 Internal Server Error\r\nOops\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"): + await read_response(self.stream) + + async def test_header_name(self): + self.stream.feed_data(b"foo bar: baz qux\r\n\r\n") + with self.assertRaises(ValueError): + await read_headers(self.stream) + + async def test_header_value(self): + self.stream.feed_data(b"foo: \x00\x00\x0f\r\n\r\n") + with self.assertRaises(ValueError): + await read_headers(self.stream) + + async def test_headers_limit(self): + self.stream.feed_data(b"foo: bar\r\n" * 129 + b"\r\n") + with self.assertRaises(SecurityError): + await read_headers(self.stream) + + async def test_line_limit(self): + # Header line contains 5 + 8186 + 2 = 8193 bytes. + self.stream.feed_data(b"foo: " + b"a" * 8186 + b"\r\n\r\n") + with self.assertRaises(SecurityError): + await read_headers(self.stream) + + async def test_line_ending(self): + self.stream.feed_data(b"foo: bar\n\n") + with self.assertRaises(EOFError): + await read_headers(self.stream) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_protocol.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_protocol.py new file mode 100644 index 0000000000..f2eb0fea03 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/test_protocol.py @@ -0,0 +1,1708 @@ +import asyncio +import contextlib +import logging +import sys +import unittest +import unittest.mock +import warnings + +from websockets.exceptions import ConnectionClosed, InvalidState +from websockets.frames import ( + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, +) +from websockets.legacy.framing import Frame +from websockets.legacy.protocol import WebSocketCommonProtocol, broadcast +from websockets.protocol import State + +from ..utils import MS +from .utils import AsyncioTestCase + + +async def async_iterable(iterable): + for item in iterable: + yield item + + +class TransportMock(unittest.mock.Mock): + """ + Transport mock to control the protocol's inputs and outputs in tests. + + It calls the protocol's connection_made and connection_lost methods like + actual transports. + + It also calls the protocol's connection_open method to bypass the + WebSocket handshake. + + To simulate incoming data, tests call the protocol's data_received and + eof_received methods directly. + + They could also pause_writing and resume_writing to test flow control. + + """ + + # This should happen in __init__ but overriding Mock.__init__ is hard. + def setup_mock(self, loop, protocol): + self.loop = loop + self.protocol = protocol + self._eof = False + self._closing = False + # Simulate a successful TCP handshake. + self.protocol.connection_made(self) + # Simulate a successful WebSocket handshake. + self.protocol.connection_open() + + def can_write_eof(self): + return True + + def write_eof(self): + # When the protocol half-closes the TCP connection, it expects the + # other end to close it. Simulate that. + if not self._eof: + self.loop.call_soon(self.close) + self._eof = True + + def close(self): + # Simulate how actual transports drop the connection. + if not self._closing: + self.loop.call_soon(self.protocol.connection_lost, None) + self._closing = True + + def abort(self): + # Change this to an `if` if tests call abort() multiple times. + assert self.protocol.state is not State.CLOSED + self.loop.call_soon(self.protocol.connection_lost, None) + + +class CommonTests: + """ + Mixin that defines most tests but doesn't inherit unittest.TestCase. + + Tests are run by the ServerTests and ClientTests subclasses. + + """ + + def setUp(self): + super().setUp() + + # This logic is encapsulated in a coroutine to prevent it from executing + # before the event loop is running which causes asyncio.get_event_loop() + # to raise a DeprecationWarning on Python ≥ 3.10. + + async def create_protocol(): + # Disable pings to make it easier to test what frames are sent exactly. + return WebSocketCommonProtocol(ping_interval=None) + + self.protocol = self.loop.run_until_complete(create_protocol()) + self.transport = TransportMock() + self.transport.setup_mock(self.loop, self.protocol) + + def tearDown(self): + self.transport.close() + self.loop.run_until_complete(self.protocol.close()) + super().tearDown() + + # Utilities for writing tests. + + def make_drain_slow(self, delay=MS): + # Process connection_made in order to initialize self.protocol.transport. + self.run_loop_once() + + original_drain = self.protocol._drain + + async def delayed_drain(): + await asyncio.sleep(delay) + await original_drain() + + self.protocol._drain = delayed_drain + + close_frame = Frame( + True, + OP_CLOSE, + Close(CloseCode.NORMAL_CLOSURE, "close").serialize(), + ) + local_close = Frame( + True, + OP_CLOSE, + Close(CloseCode.NORMAL_CLOSURE, "local").serialize(), + ) + remote_close = Frame( + True, + OP_CLOSE, + Close(CloseCode.NORMAL_CLOSURE, "remote").serialize(), + ) + + def receive_frame(self, frame): + """ + Make the protocol receive a frame. + + """ + write = self.protocol.data_received + mask = not self.protocol.is_client + frame.write(write, mask=mask) + + def receive_eof(self): + """ + Make the protocol receive the end of the data stream. + + Since ``WebSocketCommonProtocol.eof_received`` returns ``None``, an + actual transport would close itself after calling it. This function + emulates that behavior. + + """ + self.protocol.eof_received() + self.loop.call_soon(self.transport.close) + + def receive_eof_if_client(self): + """ + Like receive_eof, but only if this is the client side. + + Since the server is supposed to initiate the termination of the TCP + connection, this method helps making tests work for both sides. + + """ + if self.protocol.is_client: + self.receive_eof() + + def close_connection(self, code=CloseCode.NORMAL_CLOSURE, reason="close"): + """ + Execute a closing handshake. + + This puts the connection in the CLOSED state. + + """ + close_frame_data = Close(code, reason).serialize() + # Prepare the response to the closing handshake from the remote side. + self.receive_frame(Frame(True, OP_CLOSE, close_frame_data)) + self.receive_eof_if_client() + # Trigger the closing handshake from the local side and complete it. + self.loop.run_until_complete(self.protocol.close(code, reason)) + # Empty the outgoing data stream so we can make assertions later on. + self.assertOneFrameSent(True, OP_CLOSE, close_frame_data) + + assert self.protocol.state is State.CLOSED + + def half_close_connection_local( + self, + code=CloseCode.NORMAL_CLOSURE, + reason="close", + ): + """ + Start a closing handshake but do not complete it. + + The main difference with `close_connection` is that the connection is + left in the CLOSING state until the event loop runs again. + + The current implementation returns a task that must be awaited or + canceled, else asyncio complains about destroying a pending task. + + """ + close_frame_data = Close(code, reason).serialize() + # Trigger the closing handshake from the local endpoint. + close_task = self.loop.create_task(self.protocol.close(code, reason)) + self.run_loop_once() # write_frame executes + # Empty the outgoing data stream so we can make assertions later on. + self.assertOneFrameSent(True, OP_CLOSE, close_frame_data) + + assert self.protocol.state is State.CLOSING + + # Complete the closing sequence at 1ms intervals so the test can run + # at each point even it goes back to the event loop several times. + self.loop.call_later( + MS, self.receive_frame, Frame(True, OP_CLOSE, close_frame_data) + ) + self.loop.call_later(2 * MS, self.receive_eof_if_client) + + # This task must be awaited or canceled by the caller. + return close_task + + def half_close_connection_remote( + self, + code=CloseCode.NORMAL_CLOSURE, + reason="close", + ): + """ + Receive a closing handshake but do not complete it. + + The main difference with `close_connection` is that the connection is + left in the CLOSING state until the event loop runs again. + + """ + # On the server side, websockets completes the closing handshake and + # closes the TCP connection immediately. Yield to the event loop after + # sending the close frame to run the test while the connection is in + # the CLOSING state. + if not self.protocol.is_client: + self.make_drain_slow() + + close_frame_data = Close(code, reason).serialize() + # Trigger the closing handshake from the remote endpoint. + self.receive_frame(Frame(True, OP_CLOSE, close_frame_data)) + self.run_loop_once() # read_frame executes + # Empty the outgoing data stream so we can make assertions later on. + self.assertOneFrameSent(True, OP_CLOSE, close_frame_data) + + assert self.protocol.state is State.CLOSING + + # Complete the closing sequence at 1ms intervals so the test can run + # at each point even it goes back to the event loop several times. + self.loop.call_later(2 * MS, self.receive_eof_if_client) + + def process_invalid_frames(self): + """ + Make the protocol fail quickly after simulating invalid data. + + To achieve this, this function triggers the protocol's eof_received, + which interrupts pending reads waiting for more data. + + """ + self.run_loop_once() + self.receive_eof() + self.loop.run_until_complete(self.protocol.close_connection_task) + + def sent_frames(self): + """ + Read all frames sent to the transport. + + """ + stream = asyncio.StreamReader(loop=self.loop) + + for (data,), kw in self.transport.write.call_args_list: + stream.feed_data(data) + self.transport.write.call_args_list = [] + stream.feed_eof() + + frames = [] + while not stream.at_eof(): + frames.append( + self.loop.run_until_complete( + Frame.read(stream.readexactly, mask=self.protocol.is_client) + ) + ) + return frames + + def last_sent_frame(self): + """ + Read the last frame sent to the transport. + + This method assumes that at most one frame was sent. It raises an + AssertionError otherwise. + + """ + frames = self.sent_frames() + if frames: + assert len(frames) == 1 + return frames[0] + + def assertFramesSent(self, *frames): + self.assertEqual(self.sent_frames(), [Frame(*args) for args in frames]) + + def assertOneFrameSent(self, *args): + self.assertEqual(self.last_sent_frame(), Frame(*args)) + + def assertNoFrameSent(self): + self.assertIsNone(self.last_sent_frame()) + + def assertConnectionClosed(self, code, message): + # The following line guarantees that connection_lost was called. + self.assertEqual(self.protocol.state, State.CLOSED) + # A close frame was received. + self.assertEqual(self.protocol.close_code, code) + self.assertEqual(self.protocol.close_reason, message) + + def assertConnectionFailed(self, code, message): + # The following line guarantees that connection_lost was called. + self.assertEqual(self.protocol.state, State.CLOSED) + # No close frame was received. + self.assertEqual(self.protocol.close_code, CloseCode.ABNORMAL_CLOSURE) + self.assertEqual(self.protocol.close_reason, "") + # A close frame was sent -- unless the connection was already lost. + if code == CloseCode.ABNORMAL_CLOSURE: + self.assertNoFrameSent() + else: + self.assertOneFrameSent(True, OP_CLOSE, Close(code, message).serialize()) + + @contextlib.contextmanager + def assertCompletesWithin(self, min_time, max_time): + t0 = self.loop.time() + yield + t1 = self.loop.time() + dt = t1 - t0 + self.assertGreaterEqual(dt, min_time, f"Too fast: {dt} < {min_time}") + self.assertLess(dt, max_time, f"Too slow: {dt} >= {max_time}") + + # Test constructor. + + def test_timeout_backwards_compatibility(self): + async def create_protocol(): + return WebSocketCommonProtocol(ping_interval=None, timeout=5) + + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always") + protocol = self.loop.run_until_complete(create_protocol()) + + self.assertEqual(protocol.close_timeout, 5) + self.assertDeprecationWarnings(recorded, ["rename timeout to close_timeout"]) + + def test_loop_backwards_compatibility(self): + loop = asyncio.new_event_loop() + self.addCleanup(loop.close) + + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always") + protocol = WebSocketCommonProtocol(ping_interval=None, loop=loop) + + self.assertEqual(protocol.loop, loop) + self.assertDeprecationWarnings(recorded, ["remove loop argument"]) + + # Test public attributes. + + def test_local_address(self): + get_extra_info = unittest.mock.Mock(return_value=("host", 4312)) + self.transport.get_extra_info = get_extra_info + + self.assertEqual(self.protocol.local_address, ("host", 4312)) + get_extra_info.assert_called_with("sockname") + + def test_local_address_before_connection(self): + # Emulate the situation before connection_open() runs. + _transport = self.protocol.transport + del self.protocol.transport + try: + self.assertEqual(self.protocol.local_address, None) + finally: + self.protocol.transport = _transport + + def test_remote_address(self): + get_extra_info = unittest.mock.Mock(return_value=("host", 4312)) + self.transport.get_extra_info = get_extra_info + + self.assertEqual(self.protocol.remote_address, ("host", 4312)) + get_extra_info.assert_called_with("peername") + + def test_remote_address_before_connection(self): + # Emulate the situation before connection_open() runs. + _transport = self.protocol.transport + del self.protocol.transport + try: + self.assertEqual(self.protocol.remote_address, None) + finally: + self.protocol.transport = _transport + + def test_open(self): + self.assertTrue(self.protocol.open) + self.close_connection() + self.assertFalse(self.protocol.open) + + def test_closed(self): + self.assertFalse(self.protocol.closed) + self.close_connection() + self.assertTrue(self.protocol.closed) + + def test_wait_closed(self): + wait_closed = self.loop.create_task(self.protocol.wait_closed()) + self.assertFalse(wait_closed.done()) + self.close_connection() + self.assertTrue(wait_closed.done()) + + def test_close_code(self): + self.close_connection(CloseCode.GOING_AWAY, "Bye!") + self.assertEqual(self.protocol.close_code, CloseCode.GOING_AWAY) + + def test_close_reason(self): + self.close_connection(CloseCode.GOING_AWAY, "Bye!") + self.assertEqual(self.protocol.close_reason, "Bye!") + + def test_close_code_not_set(self): + self.assertIsNone(self.protocol.close_code) + + def test_close_reason_not_set(self): + self.assertIsNone(self.protocol.close_reason) + + # Test the recv coroutine. + + def test_recv_text(self): + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + + def test_recv_binary(self): + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea") + + def test_recv_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + self.loop.run_until_complete(close_task) # cleanup + + def test_recv_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + def test_recv_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + def test_recv_protocol_error(self): + self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8"))) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "") + + def test_recv_unicode_error(self): + self.receive_frame(Frame(True, OP_TEXT, "café".encode("latin-1"))) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.INVALID_DATA, "") + + def test_recv_text_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_recv_binary_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_recv_text_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café" * 205) + + def test_recv_binary_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea" * 342) + + def test_recv_queue_empty(self): + recv = self.loop.create_task(self.protocol.recv()) + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete( + asyncio.wait_for(asyncio.shield(recv), timeout=MS) + ) + + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + data = self.loop.run_until_complete(recv) + self.assertEqual(data, "café") + + def test_recv_queue_full(self): + self.protocol.max_queue = 2 + # Test internals because it's hard to verify buffers from the outside. + self.assertEqual(list(self.protocol.messages), []) + + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), ["café"]) + + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), ["café", b"tea"]) + + self.receive_frame(Frame(True, OP_BINARY, b"milk")) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), ["café", b"tea"]) + + self.loop.run_until_complete(self.protocol.recv()) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), [b"tea", b"milk"]) + + self.loop.run_until_complete(self.protocol.recv()) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), [b"milk"]) + + self.loop.run_until_complete(self.protocol.recv()) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), []) + + def test_recv_queue_no_limit(self): + self.protocol.max_queue = None + + for _ in range(100): + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + self.run_loop_once() + + # Incoming message queue can contain at least 100 messages. + self.assertEqual(list(self.protocol.messages), ["café"] * 100) + + for _ in range(100): + self.loop.run_until_complete(self.protocol.recv()) + + self.assertEqual(list(self.protocol.messages), []) + + def test_recv_other_error(self): + async def read_message(): + raise Exception("BOOM") + + self.protocol.read_message = read_message + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.INTERNAL_ERROR, "") + + def test_recv_canceled(self): + recv = self.loop.create_task(self.protocol.recv()) + self.loop.call_soon(recv.cancel) + + with self.assertRaises(asyncio.CancelledError): + self.loop.run_until_complete(recv) + + # The next frame doesn't disappear in a vacuum (it used to). + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + + def test_recv_canceled_race_condition(self): + recv = self.loop.create_task( + asyncio.wait_for(self.protocol.recv(), timeout=0.000_001) + ) + self.loop.call_soon( + self.receive_frame, Frame(True, OP_TEXT, "café".encode("utf-8")) + ) + + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete(recv) + + # The previous frame doesn't disappear in a vacuum (it used to). + self.receive_frame(Frame(True, OP_TEXT, "tea".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + # If we're getting "tea" there, it means "café" was swallowed (ha, ha). + self.assertEqual(data, "café") + + def test_recv_when_transfer_data_cancelled(self): + # Clog incoming queue. + self.protocol.max_queue = 1 + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + self.run_loop_once() + + # Flow control kicks in (check with an implementation detail). + self.assertFalse(self.protocol._put_message_waiter.done()) + + # Schedule recv(). + recv = self.loop.create_task(self.protocol.recv()) + + # Cancel transfer_data_task (again, implementation detail). + self.protocol.fail_connection() + self.run_loop_once() + self.assertTrue(self.protocol.transfer_data_task.cancelled()) + + # recv() completes properly. + self.assertEqual(self.loop.run_until_complete(recv), "café") + + def test_recv_prevents_concurrent_calls(self): + recv = self.loop.create_task(self.protocol.recv()) + + with self.assertRaises(RuntimeError) as raised: + self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual( + str(raised.exception), + "cannot call recv while another coroutine " + "is already waiting for the next message", + ) + recv.cancel() + + # Test the send coroutine. + + def test_send_text(self): + self.loop.run_until_complete(self.protocol.send("café")) + self.assertOneFrameSent(True, OP_TEXT, "café".encode("utf-8")) + + def test_send_binary(self): + self.loop.run_until_complete(self.protocol.send(b"tea")) + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_send_binary_from_bytearray(self): + self.loop.run_until_complete(self.protocol.send(bytearray(b"tea"))) + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_send_binary_from_memoryview(self): + self.loop.run_until_complete(self.protocol.send(memoryview(b"tea"))) + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_send_dict(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send({"not": "encoded"})) + self.assertNoFrameSent() + + def test_send_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send(42)) + self.assertNoFrameSent() + + def test_send_iterable_text(self): + self.loop.run_until_complete(self.protocol.send(["ca", "fé"])) + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + ) + + def test_send_iterable_binary(self): + self.loop.run_until_complete(self.protocol.send([b"te", b"a"])) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_iterable_binary_from_bytearray(self): + self.loop.run_until_complete( + self.protocol.send([bytearray(b"te"), bytearray(b"a")]) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_iterable_binary_from_memoryview(self): + self.loop.run_until_complete( + self.protocol.send([memoryview(b"te"), memoryview(b"a")]) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_empty_iterable(self): + self.loop.run_until_complete(self.protocol.send([])) + self.assertNoFrameSent() + + def test_send_iterable_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send([42])) + self.assertNoFrameSent() + + def test_send_iterable_mixed_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send(["café", b"tea"])) + self.assertFramesSent( + (False, OP_TEXT, "café".encode("utf-8")), + (True, OP_CLOSE, Close(CloseCode.INTERNAL_ERROR, "").serialize()), + ) + + def test_send_iterable_prevents_concurrent_send(self): + self.make_drain_slow(2 * MS) + + async def send_iterable(): + await self.protocol.send(["ca", "fé"]) + + async def send_concurrent(): + await asyncio.sleep(MS) + await self.protocol.send(b"tea") + + async def run_concurrently(): + await asyncio.gather( + send_iterable(), + send_concurrent(), + ) + + self.loop.run_until_complete(run_concurrently()) + + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + (True, OP_BINARY, b"tea"), + ) + + def test_send_async_iterable_text(self): + self.loop.run_until_complete(self.protocol.send(async_iterable(["ca", "fé"]))) + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + ) + + def test_send_async_iterable_binary(self): + self.loop.run_until_complete(self.protocol.send(async_iterable([b"te", b"a"]))) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_async_iterable_binary_from_bytearray(self): + self.loop.run_until_complete( + self.protocol.send(async_iterable([bytearray(b"te"), bytearray(b"a")])) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_async_iterable_binary_from_memoryview(self): + self.loop.run_until_complete( + self.protocol.send(async_iterable([memoryview(b"te"), memoryview(b"a")])) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_empty_async_iterable(self): + self.loop.run_until_complete(self.protocol.send(async_iterable([]))) + self.assertNoFrameSent() + + def test_send_async_iterable_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send(async_iterable([42]))) + self.assertNoFrameSent() + + def test_send_async_iterable_mixed_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete( + self.protocol.send(async_iterable(["café", b"tea"])) + ) + self.assertFramesSent( + (False, OP_TEXT, "café".encode("utf-8")), + (True, OP_CLOSE, Close(CloseCode.INTERNAL_ERROR, "").serialize()), + ) + + def test_send_async_iterable_prevents_concurrent_send(self): + self.make_drain_slow(2 * MS) + + async def send_async_iterable(): + await self.protocol.send(async_iterable(["ca", "fé"])) + + async def send_concurrent(): + await asyncio.sleep(MS) + await self.protocol.send(b"tea") + + async def run_concurrently(): + await asyncio.gather( + send_async_iterable(), + send_concurrent(), + ) + + self.loop.run_until_complete(run_concurrently()) + + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + (True, OP_BINARY, b"tea"), + ) + + def test_send_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.send("foobar")) + + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_send_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.send("foobar")) + + self.assertNoFrameSent() + + def test_send_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.send("foobar")) + + self.assertNoFrameSent() + + # Test the ping coroutine. + + def test_ping_default(self): + self.loop.run_until_complete(self.protocol.ping()) + # With our testing tools, it's more convenient to extract the expected + # ping data from the library's internals than from the frame sent. + ping_data = next(iter(self.protocol.pings)) + self.assertIsInstance(ping_data, bytes) + self.assertEqual(len(ping_data), 4) + self.assertOneFrameSent(True, OP_PING, ping_data) + + def test_ping_text(self): + self.loop.run_until_complete(self.protocol.ping("café")) + self.assertOneFrameSent(True, OP_PING, "café".encode("utf-8")) + + def test_ping_binary(self): + self.loop.run_until_complete(self.protocol.ping(b"tea")) + self.assertOneFrameSent(True, OP_PING, b"tea") + + def test_ping_binary_from_bytearray(self): + self.loop.run_until_complete(self.protocol.ping(bytearray(b"tea"))) + self.assertOneFrameSent(True, OP_PING, b"tea") + + def test_ping_binary_from_memoryview(self): + self.loop.run_until_complete(self.protocol.ping(memoryview(b"tea"))) + self.assertOneFrameSent(True, OP_PING, b"tea") + + def test_ping_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.ping(42)) + self.assertNoFrameSent() + + def test_ping_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ping()) + + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_ping_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ping()) + + self.assertNoFrameSent() + + def test_ping_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ping()) + + self.assertNoFrameSent() + + # Test the pong coroutine. + + def test_pong_default(self): + self.loop.run_until_complete(self.protocol.pong()) + self.assertOneFrameSent(True, OP_PONG, b"") + + def test_pong_text(self): + self.loop.run_until_complete(self.protocol.pong("café")) + self.assertOneFrameSent(True, OP_PONG, "café".encode("utf-8")) + + def test_pong_binary(self): + self.loop.run_until_complete(self.protocol.pong(b"tea")) + self.assertOneFrameSent(True, OP_PONG, b"tea") + + def test_pong_binary_from_bytearray(self): + self.loop.run_until_complete(self.protocol.pong(bytearray(b"tea"))) + self.assertOneFrameSent(True, OP_PONG, b"tea") + + def test_pong_binary_from_memoryview(self): + self.loop.run_until_complete(self.protocol.pong(memoryview(b"tea"))) + self.assertOneFrameSent(True, OP_PONG, b"tea") + + def test_pong_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.pong(42)) + self.assertNoFrameSent() + + def test_pong_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.pong()) + + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_pong_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.pong()) + + self.assertNoFrameSent() + + def test_pong_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.pong()) + + self.assertNoFrameSent() + + # Test the protocol's logic for acknowledging pings with pongs. + + def test_answer_ping(self): + self.receive_frame(Frame(True, OP_PING, b"test")) + self.run_loop_once() + self.assertOneFrameSent(True, OP_PONG, b"test") + + def test_answer_ping_does_not_crash_if_connection_closing(self): + close_task = self.half_close_connection_local() + + self.receive_frame(Frame(True, OP_PING, b"test")) + self.run_loop_once() + + with self.assertNoLogs(): + self.loop.run_until_complete(self.protocol.close()) + + self.loop.run_until_complete(close_task) # cleanup + + def test_answer_ping_does_not_crash_if_connection_closed(self): + self.make_drain_slow() + # Drop the connection right after receiving a ping frame, + # which prevents responding with a pong frame properly. + self.receive_frame(Frame(True, OP_PING, b"test")) + self.receive_eof() + self.run_loop_once() + + with self.assertNoLogs(): + self.loop.run_until_complete(self.protocol.close()) + + def test_ignore_pong(self): + self.receive_frame(Frame(True, OP_PONG, b"test")) + self.run_loop_once() + self.assertNoFrameSent() + + def test_acknowledge_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + self.assertFalse(pong_waiter.done()) + ping_frame = self.last_sent_frame() + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + self.run_loop_once() + self.run_loop_once() + self.assertTrue(pong_waiter.done()) + + def test_abort_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + # Remove the frame from the buffer, else close_connection() complains. + self.last_sent_frame() + self.assertFalse(pong_waiter.done()) + self.close_connection() + self.assertTrue(pong_waiter.done()) + self.assertIsInstance(pong_waiter.exception(), ConnectionClosed) + + def test_abort_ping_does_not_log_exception_if_not_retreived(self): + self.loop.run_until_complete(self.protocol.ping()) + # Get the internal Future, which isn't directly returned by ping(). + ((pong_waiter, _timestamp),) = self.protocol.pings.values() + # Remove the frame from the buffer, else close_connection() complains. + self.last_sent_frame() + self.close_connection() + # Check a private attribute, for lack of a better solution. + self.assertFalse(pong_waiter._log_traceback) + + def test_acknowledge_previous_pings(self): + pings = [ + (self.loop.run_until_complete(self.protocol.ping()), self.last_sent_frame()) + for i in range(3) + ] + # Unsolicited pong doesn't acknowledge pings + self.receive_frame(Frame(True, OP_PONG, b"")) + self.run_loop_once() + self.run_loop_once() + self.assertFalse(pings[0][0].done()) + self.assertFalse(pings[1][0].done()) + self.assertFalse(pings[2][0].done()) + # Pong acknowledges all previous pings + self.receive_frame(Frame(True, OP_PONG, pings[1][1].data)) + self.run_loop_once() + self.run_loop_once() + self.assertTrue(pings[0][0].done()) + self.assertTrue(pings[1][0].done()) + self.assertFalse(pings[2][0].done()) + + def test_acknowledge_aborted_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + ping_frame = self.last_sent_frame() + # Clog incoming queue. This lets connection_lost() abort pending pings + # with a ConnectionClosed exception before transfer_data_task + # terminates and close_connection cancels keepalive_ping_task. + self.protocol.max_queue = 1 + self.receive_frame(Frame(True, OP_TEXT, b"1")) + self.receive_frame(Frame(True, OP_TEXT, b"2")) + # Add pong frame to the queue. + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + # Connection drops. + self.receive_eof() + self.loop.run_until_complete(self.protocol.wait_closed()) + # Ping receives a ConnectionClosed exception. + with self.assertRaises(ConnectionClosed): + pong_waiter.result() + + # transfer_data doesn't crash, which would be logged. + with self.assertNoLogs(): + # Unclog incoming queue. + self.loop.run_until_complete(self.protocol.recv()) + self.loop.run_until_complete(self.protocol.recv()) + + def test_canceled_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + ping_frame = self.last_sent_frame() + pong_waiter.cancel() + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + self.run_loop_once() + self.run_loop_once() + self.assertTrue(pong_waiter.cancelled()) + + def test_duplicate_ping(self): + self.loop.run_until_complete(self.protocol.ping(b"foobar")) + self.assertOneFrameSent(True, OP_PING, b"foobar") + with self.assertRaises(RuntimeError): + self.loop.run_until_complete(self.protocol.ping(b"foobar")) + self.assertNoFrameSent() + + # Test the protocol's logic for measuring latency + + def test_record_latency_on_pong(self): + self.assertEqual(self.protocol.latency, 0) + self.loop.run_until_complete(self.protocol.ping(b"test")) + self.receive_frame(Frame(True, OP_PONG, b"test")) + self.run_loop_once() + self.assertGreater(self.protocol.latency, 0) + + def test_return_latency_on_pong(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + ping_frame = self.last_sent_frame() + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + latency = self.loop.run_until_complete(pong_waiter) + self.assertGreater(latency, 0) + + # Test the protocol's logic for rebuilding fragmented messages. + + def test_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + + def test_fragmented_binary(self): + self.receive_frame(Frame(False, OP_BINARY, b"t")) + self.receive_frame(Frame(False, OP_CONT, b"e")) + self.receive_frame(Frame(True, OP_CONT, b"a")) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea") + + def test_fragmented_text_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100)) + self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_fragmented_binary_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171)) + self.receive_frame(Frame(True, OP_CONT, b"tea" * 171)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_fragmented_text_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100)) + self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café" * 205) + + def test_fragmented_binary_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171)) + self.receive_frame(Frame(True, OP_CONT, b"tea" * 171)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea" * 342) + + def test_control_frame_within_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.receive_frame(Frame(True, OP_PING, b"")) + self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + self.assertOneFrameSent(True, OP_PONG, b"") + + def test_unterminated_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + # Missing the second part of the fragmented frame. + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "") + + def test_close_handshake_in_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.receive_frame(Frame(True, OP_CLOSE, b"")) + self.process_invalid_frames() + # The RFC may have overlooked this case: it says that control frames + # can be interjected in the middle of a fragmented message and that a + # close frame must be echoed. Even though there's an unterminated + # message, technically, the closing handshake was successful. + self.assertConnectionClosed(CloseCode.NO_STATUS_RCVD, "") + + def test_connection_close_in_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "") + + # Test miscellaneous code paths to ensure full coverage. + + def test_connection_lost(self): + # Test calling connection_lost without going through close_connection. + self.protocol.connection_lost(None) + + self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_ensure_open_before_opening_handshake(self): + # Simulate a bug by forcibly reverting the protocol state. + self.protocol.state = State.CONNECTING + + with self.assertRaises(InvalidState): + self.loop.run_until_complete(self.protocol.ensure_open()) + + def test_ensure_open_during_unclean_close(self): + # Process connection_made in order to start transfer_data_task. + self.run_loop_once() + + # Ensure the test terminates quickly. + self.loop.call_later(MS, self.receive_eof_if_client) + + # Simulate the case when close() times out sending a close frame. + self.protocol.fail_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ensure_open()) + + def test_legacy_recv(self): + # By default legacy_recv in disabled. + self.assertEqual(self.protocol.legacy_recv, False) + + self.close_connection() + + # Enable legacy_recv. + self.protocol.legacy_recv = True + + # Now recv() returns None instead of raising ConnectionClosed. + self.assertIsNone(self.loop.run_until_complete(self.protocol.recv())) + + def test_connection_closed_attributes(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed) as context: + self.loop.run_until_complete(self.protocol.recv()) + + connection_closed_exc = context.exception + self.assertEqual(connection_closed_exc.code, CloseCode.NORMAL_CLOSURE) + self.assertEqual(connection_closed_exc.reason, "close") + + # Test the protocol logic for sending keepalive pings. + + def restart_protocol_with_keepalive_ping( + self, + ping_interval=3 * MS, + ping_timeout=3 * MS, + ): + initial_protocol = self.protocol + + # copied from tearDown + + self.transport.close() + self.loop.run_until_complete(self.protocol.close()) + + # copied from setUp, but enables keepalive pings + + async def create_protocol(): + return WebSocketCommonProtocol( + ping_interval=ping_interval, + ping_timeout=ping_timeout, + ) + + self.protocol = self.loop.run_until_complete(create_protocol()) + + self.transport = TransportMock() + self.transport.setup_mock(self.loop, self.protocol) + self.protocol.is_client = initial_protocol.is_client + self.protocol.side = initial_protocol.side + + def test_keepalive_ping(self): + self.restart_protocol_with_keepalive_ping() + + # Ping is sent at 3ms and acknowledged at 4ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_1,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_1) + self.receive_frame(Frame(True, OP_PONG, ping_1)) + + # Next ping is sent at 7ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_2,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_2) + + # The keepalive ping task goes on. + self.assertFalse(self.protocol.keepalive_ping_task.done()) + + def test_keepalive_ping_not_acknowledged_closes_connection(self): + self.restart_protocol_with_keepalive_ping() + + # Ping is sent at 3ms and not acknowledged. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_1,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_1) + + # Connection is closed at 6ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + self.assertOneFrameSent( + True, + OP_CLOSE, + Close(CloseCode.INTERNAL_ERROR, "keepalive ping timeout").serialize(), + ) + + # The keepalive ping task is complete. + self.assertEqual(self.protocol.keepalive_ping_task.result(), None) + + def test_keepalive_ping_stops_when_connection_closing(self): + self.restart_protocol_with_keepalive_ping() + close_task = self.half_close_connection_local() + + # No ping sent at 3ms because the closing handshake is in progress. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + self.assertNoFrameSent() + + # The keepalive ping task terminated. + self.assertTrue(self.protocol.keepalive_ping_task.cancelled()) + + self.loop.run_until_complete(close_task) # cleanup + + def test_keepalive_ping_stops_when_connection_closed(self): + self.restart_protocol_with_keepalive_ping() + self.close_connection() + + # The keepalive ping task terminated. + self.assertTrue(self.protocol.keepalive_ping_task.cancelled()) + + def test_keepalive_ping_does_not_crash_when_connection_lost(self): + self.restart_protocol_with_keepalive_ping() + # Clog incoming queue. This lets connection_lost() abort pending pings + # with a ConnectionClosed exception before transfer_data_task + # terminates and close_connection cancels keepalive_ping_task. + self.protocol.max_queue = 1 + self.receive_frame(Frame(True, OP_TEXT, b"1")) + self.receive_frame(Frame(True, OP_TEXT, b"2")) + # Ping is sent at 3ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + ((pong_waiter, _timestamp),) = self.protocol.pings.values() + # Connection drops. + self.receive_eof() + self.loop.run_until_complete(self.protocol.wait_closed()) + + # The ping waiter receives a ConnectionClosed exception. + with self.assertRaises(ConnectionClosed): + pong_waiter.result() + # The keepalive ping task terminated properly. + self.assertIsNone(self.protocol.keepalive_ping_task.result()) + + # Unclog incoming queue to terminate the test quickly. + self.loop.run_until_complete(self.protocol.recv()) + self.loop.run_until_complete(self.protocol.recv()) + + def test_keepalive_ping_with_no_ping_interval(self): + self.restart_protocol_with_keepalive_ping(ping_interval=None) + + # No ping is sent at 3ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + self.assertNoFrameSent() + + def test_keepalive_ping_with_no_ping_timeout(self): + self.restart_protocol_with_keepalive_ping(ping_timeout=None) + + # Ping is sent at 3ms and not acknowledged. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_1,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_1) + + # Next ping is sent at 7ms anyway. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + ping_1_again, ping_2 = tuple(self.protocol.pings) + self.assertEqual(ping_1, ping_1_again) + self.assertOneFrameSent(True, OP_PING, ping_2) + + # The keepalive ping task goes on. + self.assertFalse(self.protocol.keepalive_ping_task.done()) + + def test_keepalive_ping_unexpected_error(self): + self.restart_protocol_with_keepalive_ping() + + async def ping(): + raise Exception("BOOM") + + self.protocol.ping = ping + + # The keepalive ping task fails when sending a ping at 3ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + + # The keepalive ping task is complete. + # It logs and swallows the exception. + self.assertEqual(self.protocol.keepalive_ping_task.result(), None) + + # Test the protocol logic for closing the connection. + + def test_local_close(self): + # Emulate how the remote endpoint answers the closing handshake. + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + + # Run the closing handshake. + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + # Closing the connection again is a no-op. + self.loop.run_until_complete(self.protocol.close(reason="oh noes!")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertNoFrameSent() + + def test_remote_close(self): + # Emulate how the remote endpoint initiates the closing handshake. + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + + # Wait for some data in order to process the handshake. + # After recv() raises ConnectionClosed, the connection is closed. + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + # Closing the connection again is a no-op. + self.loop.run_until_complete(self.protocol.close(reason="oh noes!")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertNoFrameSent() + + def test_remote_close_and_connection_lost(self): + self.make_drain_slow() + # Drop the connection right after receiving a close frame, + # which prevents echoing the close frame properly. + self.receive_frame(self.close_frame) + self.receive_eof() + self.run_loop_once() + + with self.assertNoLogs(): + self.loop.run_until_complete(self.protocol.close(reason="oh noes!")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + def test_simultaneous_close(self): + # Receive the incoming close frame right after self.protocol.close() + # starts executing. This reproduces the error described in: + # https://github.com/python-websockets/websockets/issues/339 + self.loop.call_soon(self.receive_frame, self.remote_close) + self.loop.call_soon(self.receive_eof_if_client) + self.run_loop_once() + + self.loop.run_until_complete(self.protocol.close(reason="local")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "remote") + # The current implementation sends a close frame in response to the + # close frame received from the remote end. It skips the close frame + # that should be sent as a result of calling close(). + self.assertOneFrameSent(*self.remote_close) + + def test_close_preserves_incoming_frames(self): + self.receive_frame(Frame(True, OP_TEXT, b"hello")) + self.run_loop_once() + + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + next_message = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(next_message, "hello") + + def test_close_protocol_error(self): + invalid_close_frame = Frame(True, OP_CLOSE, b"\x00") + self.receive_frame(invalid_close_frame) + self.receive_eof_if_client() + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "") + + def test_close_connection_lost(self): + self.receive_eof() + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_local_close_during_recv(self): + recv = self.loop.create_task(self.protocol.recv()) + + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + + self.loop.run_until_complete(self.protocol.close(reason="close")) + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(recv) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + + # There is no test_remote_close_during_recv because it would be identical + # to test_remote_close. + + def test_remote_close_during_send(self): + self.make_drain_slow() + send = self.loop.create_task(self.protocol.send("hello")) + + self.receive_frame(self.close_frame) + self.receive_eof() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(send) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + + # There is no test_local_close_during_send because this cannot really + # happen, considering that writes are serialized. + + def test_broadcast_text(self): + broadcast([self.protocol], "café") + self.assertOneFrameSent(True, OP_TEXT, "café".encode("utf-8")) + + def test_broadcast_binary(self): + broadcast([self.protocol], b"tea") + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_broadcast_type_error(self): + with self.assertRaises(TypeError): + broadcast([self.protocol], ["ca", "fé"]) + + def test_broadcast_no_clients(self): + broadcast([], "café") + self.assertNoFrameSent() + + def test_broadcast_two_clients(self): + broadcast([self.protocol, self.protocol], "café") + self.assertFramesSent( + (True, OP_TEXT, "café".encode("utf-8")), + (True, OP_TEXT, "café".encode("utf-8")), + ) + + def test_broadcast_skips_closed_connection(self): + self.close_connection() + + with self.assertNoLogs(): + broadcast([self.protocol], "café") + self.assertNoFrameSent() + + def test_broadcast_skips_closing_connection(self): + close_task = self.half_close_connection_local() + + with self.assertNoLogs(): + broadcast([self.protocol], "café") + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_broadcast_skips_connection_sending_fragmented_text(self): + self.make_drain_slow() + self.loop.create_task(self.protocol.send(["ca", "fé"])) + self.run_loop_once() + self.assertOneFrameSent(False, OP_TEXT, "ca".encode("utf-8")) + + with self.assertLogs("websockets", logging.WARNING) as logs: + broadcast([self.protocol], "café") + + self.assertEqual( + [record.getMessage() for record in logs.records][:2], + ["skipped broadcast: sending a fragmented message"], + ) + + @unittest.skipIf( + sys.version_info[:2] < (3, 11), "raise_exceptions requires Python 3.11+" + ) + def test_broadcast_reports_connection_sending_fragmented_text(self): + self.make_drain_slow() + self.loop.create_task(self.protocol.send(["ca", "fé"])) + self.run_loop_once() + self.assertOneFrameSent(False, OP_TEXT, "ca".encode("utf-8")) + + with self.assertRaises(ExceptionGroup) as raised: + broadcast([self.protocol], "café", raise_exceptions=True) + + self.assertEqual(str(raised.exception), "skipped broadcast (1 sub-exception)") + self.assertEqual( + str(raised.exception.exceptions[0]), "sending a fragmented message" + ) + + def test_broadcast_skips_connection_failing_to_send(self): + # Configure mock to raise an exception when writing to the network. + self.protocol.transport.write.side_effect = RuntimeError + + with self.assertLogs("websockets", logging.WARNING) as logs: + broadcast([self.protocol], "café") + + self.assertEqual( + [record.getMessage() for record in logs.records][:2], + ["skipped broadcast: failed to write message"], + ) + + @unittest.skipIf( + sys.version_info[:2] < (3, 11), "raise_exceptions requires Python 3.11+" + ) + def test_broadcast_reports_connection_failing_to_send(self): + # Configure mock to raise an exception when writing to the network. + self.protocol.transport.write.side_effect = RuntimeError("BOOM") + + with self.assertRaises(ExceptionGroup) as raised: + broadcast([self.protocol], "café", raise_exceptions=True) + + self.assertEqual(str(raised.exception), "skipped broadcast (1 sub-exception)") + self.assertEqual(str(raised.exception.exceptions[0]), "failed to write message") + self.assertEqual(str(raised.exception.exceptions[0].__cause__), "BOOM") + + +class ServerTests(CommonTests, AsyncioTestCase): + def setUp(self): + super().setUp() + self.protocol.is_client = False + self.protocol.side = "server" + + def test_local_close_send_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + self.make_drain_slow(50 * MS) + # If we can't send a close frame, time out in 10ms. + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(9 * MS, 19 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + self.assertConnectionClosed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_local_close_receive_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't send a close frame, time out in 10ms. + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(9 * MS, 19 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + self.assertConnectionClosed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_local_close_connection_lost_timeout_after_write_eof(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't close its side of the TCP connection after we + # half-close our side with write_eof(), time out in 10ms. + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(9 * MS, 19 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) + + def test_local_close_connection_lost_timeout_after_close(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't close its side of the TCP connection after we + # half-close our side with write_eof() and close it with close(), time + # out in 20ms. + # Check the timing within -1/+9ms for robustness. + # Add another 10ms because this test is flaky and I don't understand. + with self.assertCompletesWithin(19 * MS, 39 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + # HACK: disable close => other end drops connection emulation. + self.transport._closing = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) + + +class ClientTests(CommonTests, AsyncioTestCase): + def setUp(self): + super().setUp() + self.protocol.is_client = True + self.protocol.side = "client" + + def test_local_close_send_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + self.make_drain_slow(50 * MS) + # If we can't send a close frame, time out in 20ms. + # - 10ms waiting for sending a close frame + # - 10ms waiting for receiving a half-close + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(19 * MS, 29 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.ABNORMAL_CLOSURE, + "", + ) + + def test_local_close_receive_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + # If the server doesn't send a close frame, time out in 20ms: + # - 10ms waiting for receiving a close frame + # - 10ms waiting for receiving a half-close + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(19 * MS, 29 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.ABNORMAL_CLOSURE, + "", + ) + + def test_local_close_connection_lost_timeout_after_write_eof(self): + self.protocol.close_timeout = 10 * MS + # If the server doesn't half-close its side of the TCP connection + # after we send a close frame, time out in 20ms: + # - 10ms waiting for receiving a half-close + # - 10ms waiting for receiving a close after write_eof + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(19 * MS, 29 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) + + def test_local_close_connection_lost_timeout_after_close(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't close its side of the TCP connection after we + # half-close our side with write_eof() and close it with close(), time + # out in 30ms. + # - 10ms waiting for receiving a half-close + # - 10ms waiting for receiving a close after write_eof + # - 10ms waiting for receiving a close after close + # Check the timing within -1/+9ms for robustness. + # Add another 10ms because this test is flaky and I don't understand. + with self.assertCompletesWithin(29 * MS, 49 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + # HACK: disable close => other end drops connection emulation. + self.transport._closing = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/utils.py new file mode 100644 index 0000000000..4a21dcaeb5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/legacy/utils.py @@ -0,0 +1,84 @@ +import asyncio +import contextlib +import functools +import logging +import unittest + + +class AsyncioTestCase(unittest.TestCase): + """ + Base class for tests that sets up an isolated event loop for each test. + + IsolatedAsyncioTestCase was introduced in Python 3.8 for similar purposes + but isn't a drop-in replacement. + + """ + + def __init_subclass__(cls, **kwargs): + """ + Convert test coroutines to test functions. + + This supports asynchronous tests transparently. + + """ + super().__init_subclass__(**kwargs) + for name in unittest.defaultTestLoader.getTestCaseNames(cls): + test = getattr(cls, name) + if asyncio.iscoroutinefunction(test): + setattr(cls, name, cls.convert_async_to_sync(test)) + + @staticmethod + def convert_async_to_sync(test): + """ + Convert a test coroutine to a test function. + + """ + + @functools.wraps(test) + def test_func(self, *args, **kwargs): + return self.loop.run_until_complete(test(self, *args, **kwargs)) + + return test_func + + def setUp(self): + super().setUp() + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def tearDown(self): + self.loop.close() + super().tearDown() + + def run_loop_once(self): + # Process callbacks scheduled with call_soon by appending a callback + # to stop the event loop then running it until it hits that callback. + self.loop.call_soon(self.loop.stop) + self.loop.run_forever() + + # Remove when dropping Python < 3.10 + @contextlib.contextmanager + def assertNoLogs(self, logger="websockets", level=logging.ERROR): + """ + No message is logged on the given logger with at least the given level. + + """ + with self.assertLogs(logger, level) as logs: + # We want to test that no log message is emitted + # but assertLogs expects at least one log message. + logging.getLogger(logger).log(level, "dummy") + yield + + level_name = logging.getLevelName(level) + self.assertEqual(logs.output, [f"{level_name}:{logger}:dummy"]) + + def assertDeprecationWarnings(self, recorded_warnings, expected_warnings): + """ + Check recorded deprecation warnings match a list of expected messages. + + """ + for recorded in recorded_warnings: + self.assertEqual(type(recorded.message), DeprecationWarning) + self.assertEqual( + set(str(recorded.message) for recorded in recorded_warnings), + set(expected_warnings), + ) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/maxi_cov.py b/testing/web-platform/tests/tools/third_party/websockets/tests/maxi_cov.py new file mode 100644 index 0000000000..2568dcf18b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/maxi_cov.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python + +"""Measure coverage of each module by its test module.""" + +import glob +import os.path +import subprocess +import sys + + +UNMAPPED_SRC_FILES = ["websockets/version.py"] +UNMAPPED_TEST_FILES = ["tests/test_exports.py"] + + +def check_environment(): + """Check that prerequisites for running this script are met.""" + try: + import websockets # noqa: F401 + except ImportError: + print("failed to import websockets; is src on PYTHONPATH?") + return False + try: + import coverage # noqa: F401 + except ImportError: + print("failed to locate Coverage.py; is it installed?") + return False + return True + + +def get_mapping(src_dir="src"): + """Return a dict mapping each source file to its test file.""" + + # List source and test files. + + src_files = glob.glob( + os.path.join(src_dir, "websockets/**/*.py"), + recursive=True, + ) + test_files = glob.glob( + "tests/**/*.py", + recursive=True, + ) + + src_files = [ + os.path.relpath(src_file, src_dir) + for src_file in sorted(src_files) + if "legacy" not in os.path.dirname(src_file) + if os.path.basename(src_file) != "__init__.py" + and os.path.basename(src_file) != "__main__.py" + and os.path.basename(src_file) != "compatibility.py" + ] + test_files = [ + test_file + for test_file in sorted(test_files) + if "legacy" not in os.path.dirname(test_file) + and os.path.basename(test_file) != "__init__.py" + and os.path.basename(test_file).startswith("test_") + ] + + # Map source files to test files. + + mapping = {} + unmapped_test_files = [] + + for test_file in test_files: + dir_name, file_name = os.path.split(test_file) + assert dir_name.startswith("tests") + assert file_name.startswith("test_") + src_file = os.path.join( + "websockets" + dir_name[len("tests") :], + file_name[len("test_") :], + ) + if src_file in src_files: + mapping[src_file] = test_file + else: + unmapped_test_files.append(test_file) + + unmapped_src_files = list(set(src_files) - set(mapping)) + + # Ensure that all files are mapped. + + assert unmapped_src_files == UNMAPPED_SRC_FILES + assert unmapped_test_files == UNMAPPED_TEST_FILES + + return mapping + + +def get_ignored_files(src_dir="src"): + """Return the list of files to exclude from coverage measurement.""" + + return [ + # */websockets matches src/websockets and .tox/**/site-packages/websockets. + # There are no tests for the __main__ module and for compatibility modules. + "*/websockets/__main__.py", + "*/websockets/*/compatibility.py", + # This approach isn't applicable to the test suite of the legacy + # implementation, due to the huge test_client_server test module. + "*/websockets/legacy/*", + "tests/legacy/*", + ] + [ + # Exclude test utilities that are shared between several test modules. + # Also excludes this script. + test_file + for test_file in sorted(glob.glob("tests/**/*.py", recursive=True)) + if "legacy" not in os.path.dirname(test_file) + and os.path.basename(test_file) != "__init__.py" + and not os.path.basename(test_file).startswith("test_") + ] + + +def run_coverage(mapping, src_dir="src"): + # Initialize a new coverage measurement session. The --source option + # includes all files in the report, even if they're never imported. + print("\nInitializing session\n", flush=True) + subprocess.run( + [ + sys.executable, + "-m", + "coverage", + "run", + "--source", + ",".join([os.path.join(src_dir, "websockets"), "tests"]), + "--omit", + ",".join(get_ignored_files(src_dir)), + "-m", + "unittest", + ] + + UNMAPPED_TEST_FILES, + check=True, + ) + # Append coverage of each source module by the corresponding test module. + for src_file, test_file in mapping.items(): + print(f"\nTesting {src_file} with {test_file}\n", flush=True) + subprocess.run( + [ + sys.executable, + "-m", + "coverage", + "run", + "--append", + "--include", + ",".join([os.path.join(src_dir, src_file), test_file]), + "-m", + "unittest", + test_file, + ], + check=True, + ) + + +if __name__ == "__main__": + if not check_environment(): + sys.exit(1) + src_dir = sys.argv[1] if len(sys.argv) == 2 else "src" + mapping = get_mapping(src_dir) + run_coverage(mapping, src_dir) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/tests/protocol.py new file mode 100644 index 0000000000..4e843daab3 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/protocol.py @@ -0,0 +1,29 @@ +from websockets.protocol import Protocol + + +class RecordingProtocol(Protocol): + """ + Protocol subclass that records incoming frames. + + By interfacing with this protocol, you can check easily what the component + being testing sends during a test. + + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.frames_rcvd = [] + + def get_frames_rcvd(self): + """ + Get incoming frames received up to this point. + + Calling this method clears the list. Each frame is returned only once. + + """ + frames_rcvd, self.frames_rcvd = self.frames_rcvd, [] + return frames_rcvd + + def recv_frame(self, frame): + self.frames_rcvd.append(frame) + super().recv_frame(frame) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/__init__.py diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/client.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/client.py new file mode 100644 index 0000000000..683893e88c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/client.py @@ -0,0 +1,39 @@ +import contextlib +import ssl + +from websockets.sync.client import * +from websockets.sync.server import WebSocketServer + +from ..utils import CERTIFICATE + + +__all__ = [ + "CLIENT_CONTEXT", + "run_client", + "run_unix_client", +] + + +CLIENT_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +CLIENT_CONTEXT.load_verify_locations(CERTIFICATE) + + +@contextlib.contextmanager +def run_client(wsuri_or_server, secure=None, resource_name="/", **kwargs): + if isinstance(wsuri_or_server, str): + wsuri = wsuri_or_server + else: + assert isinstance(wsuri_or_server, WebSocketServer) + if secure is None: + secure = "ssl_context" in kwargs + protocol = "wss" if secure else "ws" + host, port = wsuri_or_server.socket.getsockname() + wsuri = f"{protocol}://{host}:{port}{resource_name}" + with connect(wsuri, **kwargs) as client: + yield client + + +@contextlib.contextmanager +def run_unix_client(path, **kwargs): + with unix_connect(path, **kwargs) as client: + yield client diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/connection.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/connection.py new file mode 100644 index 0000000000..89d4909ee1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/connection.py @@ -0,0 +1,109 @@ +import contextlib +import time + +from websockets.sync.connection import Connection + + +class InterceptingConnection(Connection): + """ + Connection subclass that can intercept outgoing packets. + + By interfacing with this connection, you can simulate network conditions + affecting what the component being tested receives during a test. + + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.socket = InterceptingSocket(self.socket) + + @contextlib.contextmanager + def delay_frames_sent(self, delay): + """ + Add a delay before sending frames. + + Delays cumulate: they're added before every frame or before EOF. + + """ + assert self.socket.delay_sendall is None + self.socket.delay_sendall = delay + try: + yield + finally: + self.socket.delay_sendall = None + + @contextlib.contextmanager + def delay_eof_sent(self, delay): + """ + Add a delay before sending EOF. + + Delays cumulate: they're added before every frame or before EOF. + + """ + assert self.socket.delay_shutdown is None + self.socket.delay_shutdown = delay + try: + yield + finally: + self.socket.delay_shutdown = None + + @contextlib.contextmanager + def drop_frames_sent(self): + """ + Prevent frames from being sent. + + Since TCP is reliable, sending frames or EOF afterwards is unrealistic. + + """ + assert not self.socket.drop_sendall + self.socket.drop_sendall = True + try: + yield + finally: + self.socket.drop_sendall = False + + @contextlib.contextmanager + def drop_eof_sent(self): + """ + Prevent EOF from being sent. + + Since TCP is reliable, sending frames or EOF afterwards is unrealistic. + + """ + assert not self.socket.drop_shutdown + self.socket.drop_shutdown = True + try: + yield + finally: + self.socket.drop_shutdown = False + + +class InterceptingSocket: + """ + Socket wrapper that intercepts calls to sendall and shutdown. + + This is coupled to the implementation, which relies on these two methods. + + """ + + def __init__(self, socket): + self.socket = socket + self.delay_sendall = None + self.delay_shutdown = None + self.drop_sendall = False + self.drop_shutdown = False + + def __getattr__(self, name): + return getattr(self.socket, name) + + def sendall(self, bytes, flags=0): + if self.delay_sendall is not None: + time.sleep(self.delay_sendall) + if not self.drop_sendall: + self.socket.sendall(bytes, flags) + + def shutdown(self, how): + if self.delay_shutdown is not None: + time.sleep(self.delay_shutdown) + if not self.drop_shutdown: + self.socket.shutdown(how) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/server.py new file mode 100644 index 0000000000..a9a77438ca --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/server.py @@ -0,0 +1,65 @@ +import contextlib +import ssl +import threading + +from websockets.sync.server import * + +from ..utils import CERTIFICATE + + +SERVER_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +SERVER_CONTEXT.load_cert_chain(CERTIFICATE) + +# Work around https://github.com/openssl/openssl/issues/7967 + +# This bug causes connect() to hang in tests for the client. Including this +# workaround acknowledges that the issue could happen outside of the test suite. + +# It shouldn't happen too often, or else OpenSSL 1.1.1 would be unusable. If it +# happens, we can look for a library-level fix, but it won't be easy. + +SERVER_CONTEXT.num_tickets = 0 + + +def crash(ws): + raise RuntimeError + + +def do_nothing(ws): + pass + + +def eval_shell(ws): + for expr in ws: + value = eval(expr) + ws.send(str(value)) + + +class EvalShellMixin: + def assertEval(self, client, expr, value): + client.send(expr) + self.assertEqual(client.recv(), value) + + +@contextlib.contextmanager +def run_server(ws_handler=eval_shell, host="localhost", port=0, **kwargs): + with serve(ws_handler, host, port, **kwargs) as server: + thread = threading.Thread(target=server.serve_forever) + thread.start() + try: + yield server + finally: + server.shutdown() + thread.join() + + +@contextlib.contextmanager +def run_unix_server(path, ws_handler=eval_shell, **kwargs): + with unix_serve(ws_handler, path, **kwargs) as server: + thread = threading.Thread(target=server.serve_forever) + thread.start() + try: + yield server + finally: + server.shutdown() + thread.join() diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_client.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_client.py new file mode 100644 index 0000000000..c900f3b0fe --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_client.py @@ -0,0 +1,274 @@ +import socket +import ssl +import threading +import unittest + +from websockets.exceptions import InvalidHandshake +from websockets.extensions.permessage_deflate import PerMessageDeflate +from websockets.sync.client import * + +from ..utils import MS, temp_unix_socket_path +from .client import CLIENT_CONTEXT, run_client, run_unix_client +from .server import SERVER_CONTEXT, do_nothing, run_server, run_unix_server + + +class ClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server and the handshake succeeds.""" + with run_server() as server: + with run_client(server) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + + def test_connection_fails(self): + """Client connects to server but the handshake fails.""" + + def remove_accept_header(self, request, response): + del response.headers["Sec-WebSocket-Accept"] + + # The connection will be open for the server but failed for the client. + # Use a connection handler that exits immediately to avoid an exception. + with run_server(do_nothing, process_response=remove_accept_header) as server: + with self.assertRaisesRegex( + InvalidHandshake, + "missing Sec-WebSocket-Accept header", + ): + with run_client(server, close_timeout=MS): + self.fail("did not raise") + + def test_tcp_connection_fails(self): + """Client fails to connect to server.""" + with self.assertRaises(OSError): + with run_client("ws://localhost:54321"): # invalid port + self.fail("did not raise") + + def test_existing_socket(self): + """Client connects using a pre-existing socket.""" + with run_server() as server: + with socket.create_connection(server.socket.getsockname()) as sock: + # Use a non-existing domain to ensure we connect to the right socket. + with run_client("ws://invalid/", sock=sock) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + + def test_additional_headers(self): + """Client can set additional headers with additional_headers.""" + with run_server() as server: + with run_client( + server, additional_headers={"Authorization": "Bearer ..."} + ) as client: + self.assertEqual(client.request.headers["Authorization"], "Bearer ...") + + def test_override_user_agent(self): + """Client can override User-Agent header with user_agent_header.""" + with run_server() as server: + with run_client(server, user_agent_header="Smith") as client: + self.assertEqual(client.request.headers["User-Agent"], "Smith") + + def test_remove_user_agent(self): + """Client can remove User-Agent header with user_agent_header.""" + with run_server() as server: + with run_client(server, user_agent_header=None) as client: + self.assertNotIn("User-Agent", client.request.headers) + + def test_compression_is_enabled(self): + """Client enables compression by default.""" + with run_server() as server: + with run_client(server) as client: + self.assertEqual( + [type(ext) for ext in client.protocol.extensions], + [PerMessageDeflate], + ) + + def test_disable_compression(self): + """Client disables compression.""" + with run_server() as server: + with run_client(server, compression=None) as client: + self.assertEqual(client.protocol.extensions, []) + + def test_custom_connection_factory(self): + """Client runs ClientConnection factory provided in create_connection.""" + + def create_connection(*args, **kwargs): + client = ClientConnection(*args, **kwargs) + client.create_connection_ran = True + return client + + with run_server() as server: + with run_client(server, create_connection=create_connection) as client: + self.assertTrue(client.create_connection_ran) + + def test_timeout_during_handshake(self): + """Client times out before receiving handshake response from server.""" + gate = threading.Event() + + def stall_connection(self, request): + gate.wait() + + # The connection will be open for the server but failed for the client. + # Use a connection handler that exits immediately to avoid an exception. + with run_server(do_nothing, process_request=stall_connection) as server: + try: + with self.assertRaisesRegex( + TimeoutError, + "timed out during handshake", + ): + # While it shouldn't take 50ms to open a connection, this + # test becomes flaky in CI when setting a smaller timeout, + # even after increasing WEBSOCKETS_TESTS_TIMEOUT_FACTOR. + with run_client(server, open_timeout=5 * MS): + self.fail("did not raise") + finally: + gate.set() + + def test_connection_closed_during_handshake(self): + """Client reads EOF before receiving handshake response from server.""" + + def close_connection(self, request): + self.close_socket() + + with run_server(process_request=close_connection) as server: + with self.assertRaisesRegex( + ConnectionError, + "connection closed during handshake", + ): + with run_client(server): + self.fail("did not raise") + + +class SecureClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server securely.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with run_client(server, ssl_context=CLIENT_CONTEXT) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + self.assertEqual(client.socket.version()[:3], "TLS") + + def test_set_server_hostname_implicitly(self): + """Client sets server_hostname to the host in the WebSocket URI.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client( + path, + ssl_context=CLIENT_CONTEXT, + uri="wss://overridden/", + ) as client: + self.assertEqual(client.socket.server_hostname, "overridden") + + def test_set_server_hostname_explicitly(self): + """Client sets server_hostname to the value provided in argument.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client( + path, + ssl_context=CLIENT_CONTEXT, + server_hostname="overridden", + ) as client: + self.assertEqual(client.socket.server_hostname, "overridden") + + def test_reject_invalid_server_certificate(self): + """Client rejects certificate where server certificate isn't trusted.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with self.assertRaisesRegex( + ssl.SSLCertVerificationError, + r"certificate verify failed: self[ -]signed certificate", + ): + # The test certificate isn't trusted system-wide. + with run_client(server, secure=True): + self.fail("did not raise") + + def test_reject_invalid_server_hostname(self): + """Client rejects certificate where server hostname doesn't match.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with self.assertRaisesRegex( + ssl.SSLCertVerificationError, + r"certificate verify failed: Hostname mismatch", + ): + # This hostname isn't included in the test certificate. + with run_client( + server, ssl_context=CLIENT_CONTEXT, server_hostname="invalid" + ): + self.fail("did not raise") + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class UnixClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path): + with run_unix_client(path) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + + def test_set_host_header(self): + """Client sets the Host header to the host in the WebSocket URI.""" + # This is part of the documented behavior of unix_connect(). + with temp_unix_socket_path() as path: + with run_unix_server(path): + with run_unix_client(path, uri="ws://overridden/") as client: + self.assertEqual(client.request.headers["Host"], "overridden") + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class SecureUnixClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server securely over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client(path, ssl_context=CLIENT_CONTEXT) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + self.assertEqual(client.socket.version()[:3], "TLS") + + def test_set_server_hostname(self): + """Client sets server_hostname to the host in the WebSocket URI.""" + # This is part of the documented behavior of unix_connect(). + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client( + path, + ssl_context=CLIENT_CONTEXT, + uri="wss://overridden/", + ) as client: + self.assertEqual(client.socket.server_hostname, "overridden") + + +class ClientUsageErrorsTests(unittest.TestCase): + def test_ssl_context_without_secure_uri(self): + """Client rejects ssl_context when URI isn't secure.""" + with self.assertRaisesRegex( + TypeError, + "ssl_context argument is incompatible with a ws:// URI", + ): + connect("ws://localhost/", ssl_context=CLIENT_CONTEXT) + + def test_unix_without_path_or_sock(self): + """Unix client requires path when sock isn't provided.""" + with self.assertRaisesRegex( + TypeError, + "missing path argument", + ): + unix_connect() + + def test_unix_with_path_and_sock(self): + """Unix client rejects path when sock is provided.""" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + with self.assertRaisesRegex( + TypeError, + "path and sock arguments are incompatible", + ): + unix_connect(path="/", sock=sock) + + def test_invalid_subprotocol(self): + """Client rejects single value of subprotocols.""" + with self.assertRaisesRegex( + TypeError, + "subprotocols must be a list", + ): + connect("ws://localhost/", subprotocols="chat") + + def test_unsupported_compression(self): + """Client rejects incorrect value of compression.""" + with self.assertRaisesRegex( + ValueError, + "unsupported compression: False", + ): + connect("ws://localhost/", compression=False) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_connection.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_connection.py new file mode 100644 index 0000000000..63544d4add --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_connection.py @@ -0,0 +1,752 @@ +import contextlib +import logging +import platform +import socket +import sys +import threading +import time +import unittest +import uuid +from unittest.mock import patch + +from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK +from websockets.frames import CloseCode, Frame, Opcode +from websockets.protocol import CLIENT, SERVER, Protocol +from websockets.sync.connection import * + +from ..protocol import RecordingProtocol +from ..utils import MS +from .connection import InterceptingConnection + + +# Connection implements symmetrical behavior between clients and servers. +# All tests run on the client side and the server side to validate this. + + +class ClientConnectionTests(unittest.TestCase): + LOCAL = CLIENT + REMOTE = SERVER + + def setUp(self): + socket_, remote_socket = socket.socketpair() + protocol = Protocol(self.LOCAL) + remote_protocol = RecordingProtocol(self.REMOTE) + self.connection = Connection(socket_, protocol, close_timeout=2 * MS) + self.remote_connection = InterceptingConnection(remote_socket, remote_protocol) + + def tearDown(self): + self.remote_connection.close() + self.connection.close() + + # Test helpers built upon RecordingProtocol and InterceptingConnection. + + def assertFrameSent(self, frame): + """Check that a single frame was sent.""" + time.sleep(MS) # let the remote side process messages + self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), [frame]) + + def assertNoFrameSent(self): + """Check that no frame was sent.""" + time.sleep(MS) # let the remote side process messages + self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), []) + + @contextlib.contextmanager + def delay_frames_rcvd(self, delay): + """Delay frames before they're received by the connection.""" + with self.remote_connection.delay_frames_sent(delay): + yield + time.sleep(MS) # let the remote side process messages + + @contextlib.contextmanager + def delay_eof_rcvd(self, delay): + """Delay EOF before it's received by the connection.""" + with self.remote_connection.delay_eof_sent(delay): + yield + time.sleep(MS) # let the remote side process messages + + @contextlib.contextmanager + def drop_frames_rcvd(self): + """Drop frames before they're received by the connection.""" + with self.remote_connection.drop_frames_sent(): + yield + time.sleep(MS) # let the remote side process messages + + @contextlib.contextmanager + def drop_eof_rcvd(self): + """Drop EOF before it's received by the connection.""" + with self.remote_connection.drop_eof_sent(): + yield + time.sleep(MS) # let the remote side process messages + + # Test __enter__ and __exit__. + + def test_enter(self): + """__enter__ returns the connection itself.""" + with self.connection as connection: + self.assertIs(connection, self.connection) + + def test_exit(self): + """__exit__ closes the connection with code 1000.""" + with self.connection: + self.assertNoFrameSent() + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + def test_exit_with_exception(self): + """__exit__ with an exception closes the connection with code 1011.""" + with self.assertRaises(RuntimeError): + with self.connection: + raise RuntimeError + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xf3")) + + # Test __iter__. + + def test_iter_text(self): + """__iter__ yields text messages.""" + iterator = iter(self.connection) + self.remote_connection.send("😀") + self.assertEqual(next(iterator), "😀") + self.remote_connection.send("😀") + self.assertEqual(next(iterator), "😀") + + def test_iter_binary(self): + """__iter__ yields binary messages.""" + iterator = iter(self.connection) + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(next(iterator), b"\x01\x02\xfe\xff") + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(next(iterator), b"\x01\x02\xfe\xff") + + def test_iter_mixed(self): + """__iter__ yields a mix of text and binary messages.""" + iterator = iter(self.connection) + self.remote_connection.send("😀") + self.assertEqual(next(iterator), "😀") + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(next(iterator), b"\x01\x02\xfe\xff") + + def test_iter_connection_closed_ok(self): + """__iter__ terminates after a normal closure.""" + iterator = iter(self.connection) + self.remote_connection.close() + with self.assertRaises(StopIteration): + next(iterator) + + def test_iter_connection_closed_error(self): + """__iter__ raises ConnnectionClosedError after an error.""" + iterator = iter(self.connection) + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + next(iterator) + + # Test recv. + + def test_recv_text(self): + """recv receives a text message.""" + self.remote_connection.send("😀") + self.assertEqual(self.connection.recv(), "😀") + + def test_recv_binary(self): + """recv receives a binary message.""" + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(self.connection.recv(), b"\x01\x02\xfe\xff") + + def test_recv_fragmented_text(self): + """recv receives a fragmented text message.""" + self.remote_connection.send(["😀", "😀"]) + self.assertEqual(self.connection.recv(), "😀😀") + + def test_recv_fragmented_binary(self): + """recv receives a fragmented binary message.""" + self.remote_connection.send([b"\x01\x02", b"\xfe\xff"]) + self.assertEqual(self.connection.recv(), b"\x01\x02\xfe\xff") + + def test_recv_connection_closed_ok(self): + """recv raises ConnectionClosedOK after a normal closure.""" + self.remote_connection.close() + with self.assertRaises(ConnectionClosedOK): + self.connection.recv() + + def test_recv_connection_closed_error(self): + """recv raises ConnectionClosedError after an error.""" + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + self.connection.recv() + + def test_recv_during_recv(self): + """recv raises RuntimeError when called concurrently with itself.""" + recv_thread = threading.Thread(target=self.connection.recv) + recv_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + "cannot call recv while another thread " + "is already running recv or recv_streaming", + ): + self.connection.recv() + + self.remote_connection.send("") + recv_thread.join() + + def test_recv_during_recv_streaming(self): + """recv raises RuntimeError when called concurrently with recv_streaming.""" + recv_streaming_thread = threading.Thread( + target=lambda: list(self.connection.recv_streaming()) + ) + recv_streaming_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + "cannot call recv while another thread " + "is already running recv or recv_streaming", + ): + self.connection.recv() + + self.remote_connection.send("") + recv_streaming_thread.join() + + # Test recv_streaming. + + def test_recv_streaming_text(self): + """recv_streaming receives a text message.""" + self.remote_connection.send("😀") + self.assertEqual( + list(self.connection.recv_streaming()), + ["😀"], + ) + + def test_recv_streaming_binary(self): + """recv_streaming receives a binary message.""" + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual( + list(self.connection.recv_streaming()), + [b"\x01\x02\xfe\xff"], + ) + + def test_recv_streaming_fragmented_text(self): + """recv_streaming receives a fragmented text message.""" + self.remote_connection.send(["😀", "😀"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.connection.recv_streaming()), + ["😀", "😀", ""], + ) + + def test_recv_streaming_fragmented_binary(self): + """recv_streaming receives a fragmented binary message.""" + self.remote_connection.send([b"\x01\x02", b"\xfe\xff"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.connection.recv_streaming()), + [b"\x01\x02", b"\xfe\xff", b""], + ) + + def test_recv_streaming_connection_closed_ok(self): + """recv_streaming raises ConnectionClosedOK after a normal closure.""" + self.remote_connection.close() + with self.assertRaises(ConnectionClosedOK): + list(self.connection.recv_streaming()) + + def test_recv_streaming_connection_closed_error(self): + """recv_streaming raises ConnectionClosedError after an error.""" + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + list(self.connection.recv_streaming()) + + def test_recv_streaming_during_recv(self): + """recv_streaming raises RuntimeError when called concurrently with recv.""" + recv_thread = threading.Thread(target=self.connection.recv) + recv_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + "cannot call recv_streaming while another thread " + "is already running recv or recv_streaming", + ): + list(self.connection.recv_streaming()) + + self.remote_connection.send("") + recv_thread.join() + + def test_recv_streaming_during_recv_streaming(self): + """recv_streaming raises RuntimeError when called concurrently with itself.""" + recv_streaming_thread = threading.Thread( + target=lambda: list(self.connection.recv_streaming()) + ) + recv_streaming_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + r"cannot call recv_streaming while another thread " + r"is already running recv or recv_streaming", + ): + list(self.connection.recv_streaming()) + + self.remote_connection.send("") + recv_streaming_thread.join() + + # Test send. + + def test_send_text(self): + """send sends a text message.""" + self.connection.send("😀") + self.assertEqual(self.remote_connection.recv(), "😀") + + def test_send_binary(self): + """send sends a binary message.""" + self.connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(self.remote_connection.recv(), b"\x01\x02\xfe\xff") + + def test_send_fragmented_text(self): + """send sends a fragmented text message.""" + self.connection.send(["😀", "😀"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.remote_connection.recv_streaming()), + ["😀", "😀", ""], + ) + + def test_send_fragmented_binary(self): + """send sends a fragmented binary message.""" + self.connection.send([b"\x01\x02", b"\xfe\xff"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.remote_connection.recv_streaming()), + [b"\x01\x02", b"\xfe\xff", b""], + ) + + def test_send_connection_closed_ok(self): + """send raises ConnectionClosedOK after a normal closure.""" + self.remote_connection.close() + with self.assertRaises(ConnectionClosedOK): + self.connection.send("😀") + + def test_send_connection_closed_error(self): + """send raises ConnectionClosedError after an error.""" + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + self.connection.send("😀") + + def test_send_during_send(self): + """send raises RuntimeError when called concurrently with itself.""" + recv_thread = threading.Thread(target=self.remote_connection.recv) + recv_thread.start() + + send_gate = threading.Event() + exit_gate = threading.Event() + + def fragments(): + yield "😀" + send_gate.set() + exit_gate.wait() + yield "😀" + + send_thread = threading.Thread( + target=self.connection.send, + args=(fragments(),), + ) + send_thread.start() + + send_gate.wait() + # The check happens in four code paths, depending on the argument. + for message in [ + "😀", + b"\x01\x02\xfe\xff", + ["😀", "😀"], + [b"\x01\x02", b"\xfe\xff"], + ]: + with self.subTest(message=message): + with self.assertRaisesRegex( + RuntimeError, + "cannot call send while another thread is already running send", + ): + self.connection.send(message) + + exit_gate.set() + send_thread.join() + recv_thread.join() + + def test_send_empty_iterable(self): + """send does nothing when called with an empty iterable.""" + self.connection.send([]) + self.connection.close() + self.assertEqual(list(iter(self.remote_connection)), []) + + def test_send_mixed_iterable(self): + """send raises TypeError when called with an iterable of inconsistent types.""" + with self.assertRaises(TypeError): + self.connection.send(["😀", b"\xfe\xff"]) + + def test_send_unsupported_iterable(self): + """send raises TypeError when called with an iterable of unsupported type.""" + with self.assertRaises(TypeError): + self.connection.send([None]) + + def test_send_dict(self): + """send raises TypeError when called with a dict.""" + with self.assertRaises(TypeError): + self.connection.send({"type": "object"}) + + def test_send_unsupported_type(self): + """send raises TypeError when called with an unsupported type.""" + with self.assertRaises(TypeError): + self.connection.send(None) + + # Test close. + + def test_close(self): + """close sends a close frame.""" + self.connection.close() + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + def test_close_explicit_code_reason(self): + """close sends a close frame with a given code and reason.""" + self.connection.close(CloseCode.GOING_AWAY, "bye!") + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe9bye!")) + + def test_close_waits_for_close_frame(self): + """close waits for a close frame (then EOF) before returning.""" + with self.delay_frames_rcvd(MS): + self.connection.close() + + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + self.assertIsNone(exc.__cause__) + + def test_close_waits_for_connection_closed(self): + """close waits for EOF before returning.""" + if self.LOCAL is SERVER: + self.skipTest("only relevant on the client-side") + + with self.delay_eof_rcvd(MS): + self.connection.close() + + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + self.assertIsNone(exc.__cause__) + + def test_close_timeout_waiting_for_close_frame(self): + """close times out if no close frame is received.""" + with self.drop_eof_rcvd(), self.drop_frames_rcvd(): + self.connection.close() + + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); no close frame received") + self.assertIsInstance(exc.__cause__, TimeoutError) + + def test_close_timeout_waiting_for_connection_closed(self): + """close times out if EOF isn't received.""" + if self.LOCAL is SERVER: + self.skipTest("only relevant on the client-side") + + with self.drop_eof_rcvd(): + self.connection.close() + + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + # Remove socket.timeout when dropping Python < 3.10. + self.assertIsInstance(exc.__cause__, (socket.timeout, TimeoutError)) + + def test_close_waits_for_recv(self): + self.remote_connection.send("😀") + + close_thread = threading.Thread(target=self.connection.close) + close_thread.start() + + # Let close() initiate the closing handshake and send a close frame. + time.sleep(MS) + self.assertTrue(close_thread.is_alive()) + + # Connection isn't closed yet. + self.connection.recv() + + # Let close() receive a close frame and finish the closing handshake. + time.sleep(MS) + self.assertFalse(close_thread.is_alive()) + + # Connection is closed now. + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + self.assertIsNone(exc.__cause__) + + def test_close_timeout_waiting_for_recv(self): + self.remote_connection.send("😀") + + close_thread = threading.Thread(target=self.connection.close) + close_thread.start() + + # Let close() time out during the closing handshake. + time.sleep(3 * MS) + self.assertFalse(close_thread.is_alive()) + + # Connection is closed now. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); no close frame received") + self.assertIsInstance(exc.__cause__, TimeoutError) + + def test_close_idempotency(self): + """close does nothing if the connection is already closed.""" + self.connection.close() + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + self.connection.close() + self.assertNoFrameSent() + + @unittest.skipIf( + platform.python_implementation() == "PyPy", + "this test fails randomly due to a bug in PyPy", # see #1314 for details + ) + def test_close_idempotency_race_condition(self): + """close waits if the connection is already closing.""" + + self.connection.close_timeout = 5 * MS + + def closer(): + with self.delay_frames_rcvd(3 * MS): + self.connection.close() + + close_thread = threading.Thread(target=closer) + close_thread.start() + + # Let closer() initiate the closing handshake and send a close frame. + time.sleep(MS) + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + # Connection isn't closed yet. + with self.assertRaises(TimeoutError): + self.connection.recv(timeout=0) + + self.connection.close() + self.assertNoFrameSent() + + # Connection is closed now. + with self.assertRaises(ConnectionClosedOK): + self.connection.recv(timeout=0) + + close_thread.join() + + def test_close_during_send(self): + """close fails the connection when called concurrently with send.""" + close_gate = threading.Event() + exit_gate = threading.Event() + + def closer(): + close_gate.wait() + self.connection.close() + exit_gate.set() + + def fragments(): + yield "😀" + close_gate.set() + exit_gate.wait() + yield "😀" + + close_thread = threading.Thread(target=closer) + close_thread.start() + + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.send(fragments()) + + exc = raised.exception + self.assertEqual( + str(exc), + "sent 1011 (internal error) close during fragmented message; " + "no close frame received", + ) + self.assertIsNone(exc.__cause__) + + close_thread.join() + + # Test ping. + + @patch("random.getrandbits") + def test_ping(self, getrandbits): + """ping sends a ping frame with a random payload.""" + getrandbits.return_value = 1918987876 + self.connection.ping() + getrandbits.assert_called_once_with(32) + self.assertFrameSent(Frame(Opcode.PING, b"rand")) + + def test_ping_explicit_text(self): + """ping sends a ping frame with a payload provided as text.""" + self.connection.ping("ping") + self.assertFrameSent(Frame(Opcode.PING, b"ping")) + + def test_ping_explicit_binary(self): + """ping sends a ping frame with a payload provided as binary.""" + self.connection.ping(b"ping") + self.assertFrameSent(Frame(Opcode.PING, b"ping")) + + def test_ping_duplicate_payload(self): + """ping rejects the same payload until receiving the pong.""" + with self.remote_connection.protocol_mutex: # block response to ping + pong_waiter = self.connection.ping("idem") + with self.assertRaisesRegex( + RuntimeError, + "already waiting for a pong with the same data", + ): + self.connection.ping("idem") + self.assertTrue(pong_waiter.wait(MS)) + self.connection.ping("idem") # doesn't raise an exception + + def test_acknowledge_ping(self): + """ping is acknowledged by a pong with the same payload.""" + with self.drop_frames_rcvd(): + pong_waiter = self.connection.ping("this") + self.assertFalse(pong_waiter.wait(MS)) + self.remote_connection.pong("this") + self.assertTrue(pong_waiter.wait(MS)) + + def test_acknowledge_ping_non_matching_pong(self): + """ping isn't acknowledged by a pong with a different payload.""" + with self.drop_frames_rcvd(): + pong_waiter = self.connection.ping("this") + self.remote_connection.pong("that") + self.assertFalse(pong_waiter.wait(MS)) + + def test_acknowledge_previous_ping(self): + """ping is acknowledged by a pong with the same payload as a later ping.""" + with self.drop_frames_rcvd(): + pong_waiter = self.connection.ping("this") + self.connection.ping("that") + self.remote_connection.pong("that") + self.assertTrue(pong_waiter.wait(MS)) + + # Test pong. + + def test_pong(self): + """pong sends a pong frame.""" + self.connection.pong() + self.assertFrameSent(Frame(Opcode.PONG, b"")) + + def test_pong_explicit_text(self): + """pong sends a pong frame with a payload provided as text.""" + self.connection.pong("pong") + self.assertFrameSent(Frame(Opcode.PONG, b"pong")) + + def test_pong_explicit_binary(self): + """pong sends a pong frame with a payload provided as binary.""" + self.connection.pong(b"pong") + self.assertFrameSent(Frame(Opcode.PONG, b"pong")) + + # Test attributes. + + def test_id(self): + """Connection has an id attribute.""" + self.assertIsInstance(self.connection.id, uuid.UUID) + + def test_logger(self): + """Connection has a logger attribute.""" + self.assertIsInstance(self.connection.logger, logging.LoggerAdapter) + + def test_local_address(self): + """Connection has a local_address attribute.""" + self.assertIsNotNone(self.connection.local_address) + + def test_remote_address(self): + """Connection has a remote_address attribute.""" + self.assertIsNotNone(self.connection.remote_address) + + def test_request(self): + """Connection has a request attribute.""" + self.assertIsNone(self.connection.request) + + def test_response(self): + """Connection has a response attribute.""" + self.assertIsNone(self.connection.response) + + def test_subprotocol(self): + """Connection has a subprotocol attribute.""" + self.assertIsNone(self.connection.subprotocol) + + # Test reporting of network errors. + + @unittest.skipUnless(sys.platform == "darwin", "works only on BSD") + def test_reading_in_recv_events_fails(self): + """Error when reading incoming frames is correctly reported.""" + # Inject a fault by closing the socket. This works only on BSD. + # I cannot find a way to achieve the same effect on Linux. + self.connection.socket.close() + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + self.assertIsInstance(raised.exception.__cause__, IOError) + + def test_writing_in_recv_events_fails(self): + """Error when responding to incoming frames is correctly reported.""" + # Inject a fault by shutting down the socket for writing — but not by + # closing it because that would terminate the connection. + self.connection.socket.shutdown(socket.SHUT_WR) + # Receive a ping. Responding with a pong will fail. + self.remote_connection.ping() + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + self.assertIsInstance(raised.exception.__cause__, BrokenPipeError) + + def test_writing_in_send_context_fails(self): + """Error when sending outgoing frame is correctly reported.""" + # Inject a fault by shutting down the socket for writing — but not by + # closing it because that would terminate the connection. + self.connection.socket.shutdown(socket.SHUT_WR) + # Sending a pong will fail. + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.pong() + self.assertIsInstance(raised.exception.__cause__, BrokenPipeError) + + # Test safety nets — catching all exceptions in case of bugs. + + @patch("websockets.protocol.Protocol.events_received") + def test_unexpected_failure_in_recv_events(self, events_received): + """Unexpected internal error in recv_events() is correctly reported.""" + # Inject a fault in a random call in recv_events(). + # This test is tightly coupled to the implementation. + events_received.side_effect = AssertionError + # Receive a message to trigger the fault. + self.remote_connection.send("😀") + + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "no close frame received or sent") + self.assertIsInstance(exc.__cause__, AssertionError) + + @patch("websockets.protocol.Protocol.send_text") + def test_unexpected_failure_in_send_context(self, send_text): + """Unexpected internal error in send_context() is correctly reported.""" + # Inject a fault in a random call in send_context(). + # This test is tightly coupled to the implementation. + send_text.side_effect = AssertionError + + # Send a message to trigger the fault. + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.send("😀") + + exc = raised.exception + self.assertEqual(str(exc), "no close frame received or sent") + self.assertIsInstance(exc.__cause__, AssertionError) + + +class ServerConnectionTests(ClientConnectionTests): + LOCAL = SERVER + REMOTE = CLIENT diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_messages.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_messages.py new file mode 100644 index 0000000000..825eb87974 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_messages.py @@ -0,0 +1,479 @@ +import time + +from websockets.frames import OP_BINARY, OP_CONT, OP_PING, OP_PONG, OP_TEXT, Frame +from websockets.sync.messages import * + +from ..utils import MS +from .utils import ThreadTestCase + + +class AssemblerTests(ThreadTestCase): + """ + Tests in this class interact a lot with hidden synchronization mechanisms: + + - get() / get_iter() and put() must run in separate threads when a final + frame is set because put() waits for get() / get_iter() to fetch the + message before returning. + + - run_in_thread() lets its target run before yielding back control on entry, + which guarantees the intended execution order of test cases. + + - run_in_thread() waits for its target to finish running before yielding + back control on exit, which allows making assertions immediately. + + - When the main thread performs actions that let another thread progress, it + must wait before making assertions, to avoid depending on scheduling. + + """ + + def setUp(self): + self.assembler = Assembler() + + def tearDown(self): + """ + Check that the assembler goes back to its default state after each test. + + This removes the need for testing various sequences. + + """ + self.assertFalse(self.assembler.mutex.locked()) + self.assertFalse(self.assembler.get_in_progress) + self.assertFalse(self.assembler.put_in_progress) + if not self.assembler.closed: + self.assertFalse(self.assembler.message_complete.is_set()) + self.assertFalse(self.assembler.message_fetched.is_set()) + self.assertIsNone(self.assembler.decoder) + self.assertEqual(self.assembler.chunks, []) + self.assertIsNone(self.assembler.chunks_queue) + + # Test get + + def test_get_text_message_already_received(self): + """get returns a text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, "café") + + def test_get_binary_message_already_received(self): + """get returns a binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, b"tea") + + def test_get_text_message_not_received_yet(self): + """get returns a text message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + self.assertEqual(message, "café") + + def test_get_binary_message_not_received_yet(self): + """get returns a binary message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + self.assertEqual(message, b"tea") + + def test_get_fragmented_text_message_already_received(self): + """get reassembles a fragmented a text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, "café") + + def test_get_fragmented_binary_message_already_received(self): + """get reassembles a fragmented binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, b"tea") + + def test_get_fragmented_text_message_being_received(self): + """get reassembles a fragmented text message that is partially received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(message, "café") + + def test_get_fragmented_binary_message_being_received(self): + """get reassembles a fragmented binary message that is partially received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(message, b"tea") + + def test_get_fragmented_text_message_not_received_yet(self): + """get reassembles a fragmented text message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(message, "café") + + def test_get_fragmented_binary_message_not_received_yet(self): + """get reassembles a fragmented binary message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(message, b"tea") + + # Test get_iter + + def test_get_iter_text_message_already_received(self): + """get_iter yields a text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, ["café"]) + + def test_get_iter_binary_message_already_received(self): + """get_iter yields a binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, [b"tea"]) + + def test_get_iter_text_message_not_received_yet(self): + """get_iter yields a text message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + self.assertEqual(fragments, ["café"]) + + def test_get_iter_binary_message_not_received_yet(self): + """get_iter yields a binary message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + self.assertEqual(fragments, [b"tea"]) + + def test_get_iter_fragmented_text_message_already_received(self): + """get_iter yields a fragmented text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, ["ca", "f", "é"]) + + def test_get_iter_fragmented_binary_message_already_received(self): + """get_iter yields a fragmented binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, [b"t", b"e", b"a"]) + + def test_get_iter_fragmented_text_message_being_received(self): + """get_iter yields a fragmented text message that is partially received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + with self.run_in_thread(getter): + self.assertEqual(fragments, ["ca"]) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, ["ca", "f"]) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(fragments, ["ca", "f", "é"]) + + def test_get_iter_fragmented_binary_message_being_received(self): + """get_iter yields a fragmented binary message that is partially received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + with self.run_in_thread(getter): + self.assertEqual(fragments, [b"t"]) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, [b"t", b"e"]) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(fragments, [b"t", b"e", b"a"]) + + def test_get_iter_fragmented_text_message_not_received_yet(self): + """get_iter yields a fragmented text message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, ["ca"]) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, ["ca", "f"]) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(fragments, ["ca", "f", "é"]) + + def test_get_iter_fragmented_binary_message_not_received_yet(self): + """get_iter yields a fragmented binary message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, [b"t"]) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, [b"t", b"e"]) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(fragments, [b"t", b"e", b"a"]) + + # Test timeouts + + def test_get_with_timeout_completes(self): + """get returns a message when it is received before the timeout.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get(MS) + + self.assertEqual(message, "café") + + def test_get_with_timeout_times_out(self): + """get raises TimeoutError when no message is received before the timeout.""" + with self.assertRaises(TimeoutError): + self.assembler.get(MS) + + # Test control frames + + def test_control_frame_before_message_is_ignored(self): + """get ignores control frames between messages.""" + + def putter(): + self.assembler.put(Frame(OP_PING, b"")) + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, "café") + + def test_control_frame_in_fragmented_message_is_ignored(self): + """get ignores control frames within fragmented messages.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_PING, b"")) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_PONG, b"")) + self.assembler.put(Frame(OP_CONT, b"a")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, b"tea") + + # Test concurrency + + def test_get_fails_when_get_is_running(self): + """get cannot be called concurrently with itself.""" + with self.run_in_thread(self.assembler.get): + with self.assertRaises(RuntimeError): + self.assembler.get() + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_get_fails_when_get_iter_is_running(self): + """get cannot be called concurrently with get_iter.""" + with self.run_in_thread(lambda: list(self.assembler.get_iter())): + with self.assertRaises(RuntimeError): + self.assembler.get() + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_get_iter_fails_when_get_is_running(self): + """get_iter cannot be called concurrently with get.""" + with self.run_in_thread(self.assembler.get): + with self.assertRaises(RuntimeError): + list(self.assembler.get_iter()) + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_get_iter_fails_when_get_iter_is_running(self): + """get_iter cannot be called concurrently with itself.""" + with self.run_in_thread(lambda: list(self.assembler.get_iter())): + with self.assertRaises(RuntimeError): + list(self.assembler.get_iter()) + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_put_fails_when_put_is_running(self): + """put cannot be called concurrently with itself.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + with self.assertRaises(RuntimeError): + self.assembler.put(Frame(OP_BINARY, b"tea")) + self.assembler.get() # unblock other thread + + # Test termination + + def test_get_fails_when_interrupted_by_close(self): + """get raises EOFError when close is called.""" + + def closer(): + time.sleep(2 * MS) + self.assembler.close() + + with self.run_in_thread(closer): + with self.assertRaises(EOFError): + self.assembler.get() + + def test_get_iter_fails_when_interrupted_by_close(self): + """get_iter raises EOFError when close is called.""" + + def closer(): + time.sleep(2 * MS) + self.assembler.close() + + with self.run_in_thread(closer): + with self.assertRaises(EOFError): + list(self.assembler.get_iter()) + + def test_put_fails_when_interrupted_by_close(self): + """put raises EOFError when close is called.""" + + def closer(): + time.sleep(2 * MS) + self.assembler.close() + + with self.run_in_thread(closer): + with self.assertRaises(EOFError): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + def test_get_fails_after_close(self): + """get raises EOFError after close is called.""" + self.assembler.close() + with self.assertRaises(EOFError): + self.assembler.get() + + def test_get_iter_fails_after_close(self): + """get_iter raises EOFError after close is called.""" + self.assembler.close() + with self.assertRaises(EOFError): + list(self.assembler.get_iter()) + + def test_put_fails_after_close(self): + """put raises EOFError after close is called.""" + self.assembler.close() + with self.assertRaises(EOFError): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + def test_close_is_idempotent(self): + """close can be called multiple times safely.""" + self.assembler.close() + self.assembler.close() diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_server.py new file mode 100644 index 0000000000..f9db842468 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_server.py @@ -0,0 +1,388 @@ +import dataclasses +import http +import logging +import socket +import threading +import unittest + +from websockets.exceptions import ( + ConnectionClosedError, + ConnectionClosedOK, + InvalidStatus, + NegotiationError, +) +from websockets.http11 import Request, Response +from websockets.sync.server import * + +from ..utils import MS, temp_unix_socket_path +from .client import CLIENT_CONTEXT, run_client, run_unix_client +from .server import ( + SERVER_CONTEXT, + EvalShellMixin, + crash, + do_nothing, + eval_shell, + run_server, + run_unix_server, +) + + +class ServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives connection from client and the handshake succeeds.""" + with run_server() as server: + with run_client(server) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + + def test_connection_fails(self): + """Server receives connection from client but the handshake fails.""" + + def remove_key_header(self, request): + del request.headers["Sec-WebSocket-Key"] + + with run_server(process_request=remove_key_header) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 400", + ): + with run_client(server): + self.fail("did not raise") + + def test_connection_handler_returns(self): + """Connection handler returns.""" + with run_server(do_nothing) as server: + with run_client(server) as client: + with self.assertRaisesRegex( + ConnectionClosedOK, + r"received 1000 \(OK\); then sent 1000 \(OK\)", + ): + client.recv() + + def test_connection_handler_raises_exception(self): + """Connection handler raises an exception.""" + with run_server(crash) as server: + with run_client(server) as client: + with self.assertRaisesRegex( + ConnectionClosedError, + r"received 1011 \(internal error\); " + r"then sent 1011 \(internal error\)", + ): + client.recv() + + def test_existing_socket(self): + """Server receives connection using a pre-existing socket.""" + with socket.create_server(("localhost", 0)) as sock: + with run_server(sock=sock): + # Build WebSocket URI to ensure we connect to the right socket. + with run_client("ws://{}:{}/".format(*sock.getsockname())) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + + def test_select_subprotocol(self): + """Server selects a subprotocol with the select_subprotocol callable.""" + + def select_subprotocol(ws, subprotocols): + ws.select_subprotocol_ran = True + assert "chat" in subprotocols + return "chat" + + with run_server( + subprotocols=["chat"], + select_subprotocol=select_subprotocol, + ) as server: + with run_client(server, subprotocols=["chat"]) as client: + self.assertEval(client, "ws.select_subprotocol_ran", "True") + self.assertEval(client, "ws.subprotocol", "chat") + + def test_select_subprotocol_rejects_handshake(self): + """Server rejects handshake if select_subprotocol raises NegotiationError.""" + + def select_subprotocol(ws, subprotocols): + raise NegotiationError + + with run_server(select_subprotocol=select_subprotocol) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 400", + ): + with run_client(server): + self.fail("did not raise") + + def test_select_subprotocol_raises_exception(self): + """Server returns an error if select_subprotocol raises an exception.""" + + def select_subprotocol(ws, subprotocols): + raise RuntimeError + + with run_server(select_subprotocol=select_subprotocol) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 500", + ): + with run_client(server): + self.fail("did not raise") + + def test_process_request(self): + """Server runs process_request before processing the handshake.""" + + def process_request(ws, request): + self.assertIsInstance(request, Request) + ws.process_request_ran = True + + with run_server(process_request=process_request) as server: + with run_client(server) as client: + self.assertEval(client, "ws.process_request_ran", "True") + + def test_process_request_abort_handshake(self): + """Server aborts handshake if process_request returns a response.""" + + def process_request(ws, request): + return ws.protocol.reject(http.HTTPStatus.FORBIDDEN, "Forbidden") + + with run_server(process_request=process_request) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 403", + ): + with run_client(server): + self.fail("did not raise") + + def test_process_request_raises_exception(self): + """Server returns an error if process_request raises an exception.""" + + def process_request(ws, request): + raise RuntimeError + + with run_server(process_request=process_request) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 500", + ): + with run_client(server): + self.fail("did not raise") + + def test_process_response(self): + """Server runs process_response after processing the handshake.""" + + def process_response(ws, request, response): + self.assertIsInstance(request, Request) + self.assertIsInstance(response, Response) + ws.process_response_ran = True + + with run_server(process_response=process_response) as server: + with run_client(server) as client: + self.assertEval(client, "ws.process_response_ran", "True") + + def test_process_response_override_response(self): + """Server runs process_response after processing the handshake.""" + + def process_response(ws, request, response): + headers = response.headers.copy() + headers["X-ProcessResponse-Ran"] = "true" + return dataclasses.replace(response, headers=headers) + + with run_server(process_response=process_response) as server: + with run_client(server) as client: + self.assertEqual( + client.response.headers["X-ProcessResponse-Ran"], "true" + ) + + def test_process_response_raises_exception(self): + """Server returns an error if process_response raises an exception.""" + + def process_response(ws, request, response): + raise RuntimeError + + with run_server(process_response=process_response) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 500", + ): + with run_client(server): + self.fail("did not raise") + + def test_override_server(self): + """Server can override Server header with server_header.""" + with run_server(server_header="Neo") as server: + with run_client(server) as client: + self.assertEval(client, "ws.response.headers['Server']", "Neo") + + def test_remove_server(self): + """Server can remove Server header with server_header.""" + with run_server(server_header=None) as server: + with run_client(server) as client: + self.assertEval(client, "'Server' in ws.response.headers", "False") + + def test_compression_is_enabled(self): + """Server enables compression by default.""" + with run_server() as server: + with run_client(server) as client: + self.assertEval( + client, + "[type(ext).__name__ for ext in ws.protocol.extensions]", + "['PerMessageDeflate']", + ) + + def test_disable_compression(self): + """Server disables compression.""" + with run_server(compression=None) as server: + with run_client(server) as client: + self.assertEval(client, "ws.protocol.extensions", "[]") + + def test_custom_connection_factory(self): + """Server runs ServerConnection factory provided in create_connection.""" + + def create_connection(*args, **kwargs): + server = ServerConnection(*args, **kwargs) + server.create_connection_ran = True + return server + + with run_server(create_connection=create_connection) as server: + with run_client(server) as client: + self.assertEval(client, "ws.create_connection_ran", "True") + + def test_timeout_during_handshake(self): + """Server times out before receiving handshake request from client.""" + with run_server(open_timeout=MS) as server: + with socket.create_connection(server.socket.getsockname()) as sock: + self.assertEqual(sock.recv(4096), b"") + + def test_connection_closed_during_handshake(self): + """Server reads EOF before receiving handshake request from client.""" + with run_server() as server: + # Patch handler to record a reference to the thread running it. + server_thread = None + conn_received = threading.Event() + original_handler = server.handler + + def handler(sock, addr): + nonlocal server_thread + server_thread = threading.current_thread() + nonlocal conn_received + conn_received.set() + original_handler(sock, addr) + + server.handler = handler + + with socket.create_connection(server.socket.getsockname()): + # Wait for the server to receive the connection, then close it. + conn_received.wait() + + # Wait for the server thread to terminate. + server_thread.join() + + +class SecureServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives secure connection from client.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with run_client(server, ssl_context=CLIENT_CONTEXT) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + self.assertEval(client, "ws.socket.version()[:3]", "TLS") + + def test_timeout_during_tls_handshake(self): + """Server times out before receiving TLS handshake request from client.""" + with run_server(ssl_context=SERVER_CONTEXT, open_timeout=MS) as server: + with socket.create_connection(server.socket.getsockname()) as sock: + self.assertEqual(sock.recv(4096), b"") + + def test_connection_closed_during_tls_handshake(self): + """Server reads EOF before receiving TLS handshake request from client.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + # Patch handler to record a reference to the thread running it. + server_thread = None + conn_received = threading.Event() + original_handler = server.handler + + def handler(sock, addr): + nonlocal server_thread + server_thread = threading.current_thread() + nonlocal conn_received + conn_received.set() + original_handler(sock, addr) + + server.handler = handler + + with socket.create_connection(server.socket.getsockname()): + # Wait for the server to receive the connection, then close it. + conn_received.wait() + + # Wait for the server thread to terminate. + server_thread.join() + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class UnixServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives connection from client over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path): + with run_unix_client(path) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class SecureUnixServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives secure connection from client over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client(path, ssl_context=CLIENT_CONTEXT) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + self.assertEval(client, "ws.socket.version()[:3]", "TLS") + + +class ServerUsageErrorsTests(unittest.TestCase): + def test_unix_without_path_or_sock(self): + """Unix server requires path when sock isn't provided.""" + with self.assertRaisesRegex( + TypeError, + "missing path argument", + ): + unix_serve(eval_shell) + + def test_unix_with_path_and_sock(self): + """Unix server rejects path when sock is provided.""" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + with self.assertRaisesRegex( + TypeError, + "path and sock arguments are incompatible", + ): + unix_serve(eval_shell, path="/", sock=sock) + + def test_invalid_subprotocol(self): + """Server rejects single value of subprotocols.""" + with self.assertRaisesRegex( + TypeError, + "subprotocols must be a list", + ): + serve(eval_shell, subprotocols="chat") + + def test_unsupported_compression(self): + """Server rejects incorrect value of compression.""" + with self.assertRaisesRegex( + ValueError, + "unsupported compression: False", + ): + serve(eval_shell, compression=False) + + +class WebSocketServerTests(unittest.TestCase): + def test_logger(self): + """WebSocketServer accepts a logger argument.""" + logger = logging.getLogger("test") + with run_server(logger=logger) as server: + self.assertIs(server.logger, logger) + + def test_fileno(self): + """WebSocketServer provides a fileno attribute.""" + with run_server() as server: + self.assertIsInstance(server.fileno(), int) + + def test_shutdown(self): + """WebSocketServer provides a shutdown method.""" + with run_server() as server: + server.shutdown() + # Check that the server socket is closed. + with self.assertRaises(OSError): + server.socket.accept() diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_utils.py new file mode 100644 index 0000000000..2980a97b42 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/test_utils.py @@ -0,0 +1,33 @@ +import unittest + +from websockets.sync.utils import * + +from ..utils import MS + + +class DeadlineTests(unittest.TestCase): + def test_timeout_pending(self): + """timeout returns remaining time if deadline is in the future.""" + deadline = Deadline(MS) + timeout = deadline.timeout() + self.assertGreater(timeout, 0) + self.assertLess(timeout, MS) + + def test_timeout_elapsed_exception(self): + """timeout raises TimeoutError if deadline is in the past.""" + deadline = Deadline(-MS) + with self.assertRaises(TimeoutError): + deadline.timeout() + + def test_timeout_elapsed_no_exception(self): + """timeout doesn't raise TimeoutError when raise_if_elapsed is disabled.""" + deadline = Deadline(-MS) + timeout = deadline.timeout(raise_if_elapsed=False) + self.assertGreater(timeout, -2 * MS) + self.assertLess(timeout, -MS) + + def test_no_timeout(self): + """timeout returns None when no deadline is set.""" + deadline = Deadline(None) + timeout = deadline.timeout() + self.assertIsNone(timeout, None) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/sync/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/utils.py new file mode 100644 index 0000000000..8903cd3499 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/sync/utils.py @@ -0,0 +1,26 @@ +import contextlib +import threading +import time +import unittest + +from ..utils import MS + + +class ThreadTestCase(unittest.TestCase): + @contextlib.contextmanager + def run_in_thread(self, target): + """ + Run ``target`` function without arguments in a thread. + + In order to facilitate writing tests, this helper lets the thread run + for 1ms on entry and joins the thread with a 1ms timeout on exit. + + """ + thread = threading.Thread(target=target) + thread.start() + time.sleep(MS) + try: + yield + finally: + thread.join(MS) + self.assertFalse(thread.is_alive()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py new file mode 100644 index 0000000000..28db931552 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py @@ -0,0 +1 @@ +from websockets.auth import * diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_client.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_client.py new file mode 100644 index 0000000000..c83c87038f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_client.py @@ -0,0 +1,614 @@ +import logging +import unittest +import unittest.mock + +from websockets.client import * +from websockets.datastructures import Headers +from websockets.exceptions import InvalidHandshake, InvalidHeader +from websockets.frames import OP_TEXT, Frame +from websockets.http11 import Request, Response +from websockets.protocol import CONNECTING, OPEN +from websockets.uri import parse_uri +from websockets.utils import accept_key + +from .extensions.utils import ( + ClientOpExtensionFactory, + ClientRsv2ExtensionFactory, + OpExtension, + Rsv2Extension, +) +from .test_utils import ACCEPT, KEY +from .utils import DATE, DeprecationTestCase + + +class ConnectTests(unittest.TestCase): + def test_send_connect(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("wss://example.com/test")) + request = client.connect() + self.assertIsInstance(request, Request) + client.send_request(request) + self.assertEqual( + client.data_to_send(), + [ + f"GET /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {KEY}\r\n" + f"Sec-WebSocket-Version: 13\r\n" + f"\r\n".encode() + ], + ) + self.assertFalse(client.close_expected()) + + def test_connect_request(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("wss://example.com/test")) + request = client.connect() + self.assertEqual(request.path, "/test") + self.assertEqual( + request.headers, + Headers( + { + "Host": "example.com", + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Key": KEY, + "Sec-WebSocket-Version": "13", + } + ), + ) + + def test_path(self): + client = ClientProtocol(parse_uri("wss://example.com/endpoint?test=1")) + request = client.connect() + + self.assertEqual(request.path, "/endpoint?test=1") + + def test_port(self): + for uri, host in [ + ("ws://example.com/", "example.com"), + ("ws://example.com:80/", "example.com"), + ("ws://example.com:8080/", "example.com:8080"), + ("wss://example.com/", "example.com"), + ("wss://example.com:443/", "example.com"), + ("wss://example.com:8443/", "example.com:8443"), + ]: + with self.subTest(uri=uri): + client = ClientProtocol(parse_uri(uri)) + request = client.connect() + + self.assertEqual(request.headers["Host"], host) + + def test_user_info(self): + client = ClientProtocol(parse_uri("wss://hello:iloveyou@example.com/")) + request = client.connect() + + self.assertEqual(request.headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=") + + def test_origin(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + origin="https://example.com", + ) + request = client.connect() + + self.assertEqual(request.headers["Origin"], "https://example.com") + + def test_extensions(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory()], + ) + request = client.connect() + + self.assertEqual(request.headers["Sec-WebSocket-Extensions"], "x-op; op") + + def test_subprotocols(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["chat"], + ) + request = client.connect() + + self.assertEqual(request.headers["Sec-WebSocket-Protocol"], "chat") + + +class AcceptRejectTests(unittest.TestCase): + def test_receive_accept(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 101 Switching Protocols\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {ACCEPT}\r\n" + f"Date: {DATE}\r\n" + f"\r\n" + ).encode(), + ) + [response] = client.events_received() + self.assertIsInstance(response, Response) + self.assertEqual(client.data_to_send(), []) + self.assertFalse(client.close_expected()) + self.assertEqual(client.state, OPEN) + + def test_receive_reject(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 404 Not Found\r\n" + f"Date: {DATE}\r\n" + f"Content-Length: 13\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"Connection: close\r\n" + f"\r\n" + f"Sorry folks.\n" + ).encode(), + ) + [response] = client.events_received() + self.assertIsInstance(response, Response) + self.assertEqual(client.data_to_send(), []) + self.assertTrue(client.close_expected()) + self.assertEqual(client.state, CONNECTING) + + def test_accept_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 101 Switching Protocols\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {ACCEPT}\r\n" + f"Date: {DATE}\r\n" + f"\r\n" + ).encode(), + ) + [response] = client.events_received() + self.assertEqual(response.status_code, 101) + self.assertEqual(response.reason_phrase, "Switching Protocols") + self.assertEqual( + response.headers, + Headers( + { + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Accept": ACCEPT, + "Date": DATE, + } + ), + ) + self.assertIsNone(response.body) + + def test_reject_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 404 Not Found\r\n" + f"Date: {DATE}\r\n" + f"Content-Length: 13\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"Connection: close\r\n" + f"\r\n" + f"Sorry folks.\n" + ).encode(), + ) + [response] = client.events_received() + self.assertEqual(response.status_code, 404) + self.assertEqual(response.reason_phrase, "Not Found") + self.assertEqual( + response.headers, + Headers( + { + "Date": DATE, + "Content-Length": "13", + "Content-Type": "text/plain; charset=utf-8", + "Connection": "close", + } + ), + ) + self.assertEqual(response.body, b"Sorry folks.\n") + + def test_no_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_eof() + self.assertEqual(client.events_received(), []) + + def test_partial_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data(b"HTTP/1.1 101 Switching Protocols\r\n") + client.receive_eof() + self.assertEqual(client.events_received(), []) + + def test_random_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data(b"220 smtp.invalid\r\n") + client.receive_data(b"250 Hello relay.invalid\r\n") + client.receive_data(b"250 Ok\r\n") + client.receive_data(b"250 Ok\r\n") + client.receive_eof() + self.assertEqual(client.events_received(), []) + + def make_accept_response(self, client): + request = client.connect() + return Response( + status_code=101, + reason_phrase="Switching Protocols", + headers=Headers( + { + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Accept": accept_key( + request.headers["Sec-WebSocket-Key"] + ), + } + ), + ) + + def test_basic(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + + def test_missing_connection(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Connection"] + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "missing Connection header") + + def test_invalid_connection(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Connection"] + response.headers["Connection"] = "close" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "invalid Connection header: close") + + def test_missing_upgrade(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Upgrade"] + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "missing Upgrade header") + + def test_invalid_upgrade(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Upgrade"] + response.headers["Upgrade"] = "h2c" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c") + + def test_missing_accept(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Sec-WebSocket-Accept"] + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Accept header") + + def test_multiple_accept(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Accept"] = ACCEPT + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Sec-WebSocket-Accept header: " + "more than one Sec-WebSocket-Accept header found", + ) + + def test_invalid_accept(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Sec-WebSocket-Accept"] + response.headers["Sec-WebSocket-Accept"] = ACCEPT + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), f"invalid Sec-WebSocket-Accept header: {ACCEPT}" + ) + + def test_no_extensions(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, []) + + def test_no_extension(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension()]) + + def test_extension(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [Rsv2Extension()]) + + def test_unexpected_extension(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "no extensions supported") + + def test_unsupported_extension(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), + "Unsupported extension: name = x-op, params = [('op', None)]", + ) + + def test_supported_extension_parameters(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory("this")], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op=this" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension("this")]) + + def test_unsupported_extension_parameters(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory("this")], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), + "Unsupported extension: name = x-op, params = [('op', 'that')]", + ) + + def test_multiple_supported_extension_parameters(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ + ClientOpExtensionFactory("this"), + ClientOpExtensionFactory("that"), + ], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension("that")]) + + def test_multiple_extensions(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension(), Rsv2Extension()]) + + def test_multiple_extensions_order(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [Rsv2Extension(), OpExtension()]) + + def test_no_subprotocols(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertIsNone(client.subprotocol) + + def test_no_subprotocol(self): + client = ClientProtocol(parse_uri("wss://example.com/"), subprotocols=["chat"]) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertIsNone(client.subprotocol) + + def test_subprotocol(self): + client = ClientProtocol(parse_uri("wss://example.com/"), subprotocols=["chat"]) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.subprotocol, "chat") + + def test_unexpected_subprotocol(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "no subprotocols supported") + + def test_multiple_subprotocols(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["superchat", "chat"], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "superchat" + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), "multiple subprotocols: superchat, chat" + ) + + def test_supported_subprotocol(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["superchat", "chat"], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.subprotocol, "chat") + + def test_unsupported_subprotocol(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["superchat", "chat"], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "otherchat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "unsupported subprotocol: otherchat") + + +class MiscTests(unittest.TestCase): + def test_bypass_handshake(self): + client = ClientProtocol(parse_uri("ws://example.com/test"), state=OPEN) + client.receive_data(b"\x81\x06Hello!") + [frame] = client.events_received() + self.assertEqual(frame, Frame(OP_TEXT, b"Hello!")) + + def test_custom_logger(self): + logger = logging.getLogger("test") + with self.assertLogs("test", logging.DEBUG) as logs: + ClientProtocol(parse_uri("wss://example.com/test"), logger=logger) + self.assertEqual(len(logs.records), 1) + + +class BackwardsCompatibilityTests(DeprecationTestCase): + def test_client_connection_class(self): + with self.assertDeprecationWarning( + "ClientConnection was renamed to ClientProtocol" + ): + from websockets.client import ClientConnection + + client = ClientConnection("ws://localhost/") + + self.assertIsInstance(client, ClientProtocol) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_connection.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_connection.py new file mode 100644 index 0000000000..6592d67d0d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_connection.py @@ -0,0 +1,14 @@ +from websockets.protocol import Protocol + +from .utils import DeprecationTestCase + + +class BackwardsCompatibilityTests(DeprecationTestCase): + def test_connection_class(self): + with self.assertDeprecationWarning( + "websockets.connection was renamed to websockets.protocol " + "and Connection was renamed to Protocol" + ): + from websockets.connection import Connection + + self.assertIs(Connection, Protocol) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_datastructures.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_datastructures.py new file mode 100644 index 0000000000..32b79817ae --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_datastructures.py @@ -0,0 +1,236 @@ +import unittest + +from websockets.datastructures import * + + +class MultipleValuesErrorTests(unittest.TestCase): + def test_multiple_values_error_str(self): + self.assertEqual(str(MultipleValuesError("Connection")), "'Connection'") + self.assertEqual(str(MultipleValuesError()), "") + + +class HeadersTests(unittest.TestCase): + def setUp(self): + self.headers = Headers([("Connection", "Upgrade"), ("Server", "websockets")]) + + def test_init(self): + self.assertEqual( + Headers(), + Headers(), + ) + + def test_init_from_kwargs(self): + self.assertEqual( + Headers(connection="Upgrade", server="websockets"), + self.headers, + ) + + def test_init_from_headers(self): + self.assertEqual( + Headers(self.headers), + self.headers, + ) + + def test_init_from_headers_and_kwargs(self): + self.assertEqual( + Headers(Headers(connection="Upgrade"), server="websockets"), + self.headers, + ) + + def test_init_from_mapping(self): + self.assertEqual( + Headers({"Connection": "Upgrade", "Server": "websockets"}), + self.headers, + ) + + def test_init_from_mapping_and_kwargs(self): + self.assertEqual( + Headers({"Connection": "Upgrade"}, server="websockets"), + self.headers, + ) + + def test_init_from_iterable(self): + self.assertEqual( + Headers([("Connection", "Upgrade"), ("Server", "websockets")]), + self.headers, + ) + + def test_init_from_iterable_and_kwargs(self): + self.assertEqual( + Headers([("Connection", "Upgrade")], server="websockets"), + self.headers, + ) + + def test_init_multiple_positional_arguments(self): + with self.assertRaises(TypeError): + Headers(Headers(connection="Upgrade"), Headers(server="websockets")) + + def test_str(self): + self.assertEqual( + str(self.headers), "Connection: Upgrade\r\nServer: websockets\r\n\r\n" + ) + + def test_repr(self): + self.assertEqual( + repr(self.headers), + "Headers([('Connection', 'Upgrade'), ('Server', 'websockets')])", + ) + + def test_copy(self): + self.assertEqual(repr(self.headers.copy()), repr(self.headers)) + + def test_serialize(self): + self.assertEqual( + self.headers.serialize(), + b"Connection: Upgrade\r\nServer: websockets\r\n\r\n", + ) + + def test_contains(self): + self.assertIn("Server", self.headers) + + def test_contains_case_insensitive(self): + self.assertIn("server", self.headers) + + def test_contains_not_found(self): + self.assertNotIn("Date", self.headers) + + def test_contains_non_string_key(self): + self.assertNotIn(42, self.headers) + + def test_iter(self): + self.assertEqual(set(iter(self.headers)), {"connection", "server"}) + + def test_len(self): + self.assertEqual(len(self.headers), 2) + + def test_getitem(self): + self.assertEqual(self.headers["Server"], "websockets") + + def test_getitem_case_insensitive(self): + self.assertEqual(self.headers["server"], "websockets") + + def test_getitem_key_error(self): + with self.assertRaises(KeyError): + self.headers["Upgrade"] + + def test_setitem(self): + self.headers["Upgrade"] = "websocket" + self.assertEqual(self.headers["Upgrade"], "websocket") + + def test_setitem_case_insensitive(self): + self.headers["upgrade"] = "websocket" + self.assertEqual(self.headers["Upgrade"], "websocket") + + def test_delitem(self): + del self.headers["Connection"] + with self.assertRaises(KeyError): + self.headers["Connection"] + + def test_delitem_case_insensitive(self): + del self.headers["connection"] + with self.assertRaises(KeyError): + self.headers["Connection"] + + def test_eq(self): + other_headers = Headers([("Connection", "Upgrade"), ("Server", "websockets")]) + self.assertEqual(self.headers, other_headers) + + def test_eq_case_insensitive(self): + other_headers = Headers(connection="Upgrade", server="websockets") + self.assertEqual(self.headers, other_headers) + + def test_eq_not_equal(self): + other_headers = Headers([("Connection", "close"), ("Server", "websockets")]) + self.assertNotEqual(self.headers, other_headers) + + def test_eq_other_type(self): + self.assertNotEqual( + self.headers, "Connection: Upgrade\r\nServer: websockets\r\n\r\n" + ) + + def test_clear(self): + self.headers.clear() + self.assertFalse(self.headers) + self.assertEqual(self.headers, Headers()) + + def test_get_all(self): + self.assertEqual(self.headers.get_all("Connection"), ["Upgrade"]) + + def test_get_all_case_insensitive(self): + self.assertEqual(self.headers.get_all("connection"), ["Upgrade"]) + + def test_get_all_no_values(self): + self.assertEqual(self.headers.get_all("Upgrade"), []) + + def test_raw_items(self): + self.assertEqual( + list(self.headers.raw_items()), + [("Connection", "Upgrade"), ("Server", "websockets")], + ) + + +class MultiValueHeadersTests(unittest.TestCase): + def setUp(self): + self.headers = Headers([("Server", "Python"), ("Server", "websockets")]) + + def test_init_from_headers(self): + self.assertEqual( + Headers(self.headers), + self.headers, + ) + + def test_init_from_headers_and_kwargs(self): + self.assertEqual( + Headers(Headers(server="Python"), server="websockets"), + self.headers, + ) + + def test_str(self): + self.assertEqual( + str(self.headers), "Server: Python\r\nServer: websockets\r\n\r\n" + ) + + def test_repr(self): + self.assertEqual( + repr(self.headers), + "Headers([('Server', 'Python'), ('Server', 'websockets')])", + ) + + def test_copy(self): + self.assertEqual(repr(self.headers.copy()), repr(self.headers)) + + def test_serialize(self): + self.assertEqual( + self.headers.serialize(), + b"Server: Python\r\nServer: websockets\r\n\r\n", + ) + + def test_iter(self): + self.assertEqual(set(iter(self.headers)), {"server"}) + + def test_len(self): + self.assertEqual(len(self.headers), 1) + + def test_getitem_multiple_values_error(self): + with self.assertRaises(MultipleValuesError): + self.headers["Server"] + + def test_setitem(self): + self.headers["Server"] = "redux" + self.assertEqual( + self.headers.get_all("Server"), ["Python", "websockets", "redux"] + ) + + def test_delitem(self): + del self.headers["Server"] + with self.assertRaises(KeyError): + self.headers["Server"] + + def test_get_all(self): + self.assertEqual(self.headers.get_all("Server"), ["Python", "websockets"]) + + def test_raw_items(self): + self.assertEqual( + list(self.headers.raw_items()), + [("Server", "Python"), ("Server", "websockets")], + ) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py new file mode 100644 index 0000000000..1e6f58fad5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py @@ -0,0 +1,196 @@ +import unittest + +from websockets.datastructures import Headers +from websockets.exceptions import * +from websockets.frames import Close, CloseCode +from websockets.http11 import Response + + +class ExceptionsTests(unittest.TestCase): + def test_str(self): + for exception, exception_str in [ + ( + WebSocketException("something went wrong"), + "something went wrong", + ), + ( + ConnectionClosed( + Close(CloseCode.NORMAL_CLOSURE, ""), + Close(CloseCode.NORMAL_CLOSURE, ""), + True, + ), + "received 1000 (OK); then sent 1000 (OK)", + ), + ( + ConnectionClosed( + Close(CloseCode.GOING_AWAY, "Bye!"), + Close(CloseCode.GOING_AWAY, "Bye!"), + False, + ), + "sent 1001 (going away) Bye!; then received 1001 (going away) Bye!", + ), + ( + ConnectionClosed( + Close(CloseCode.NORMAL_CLOSURE, "race"), + Close(CloseCode.NORMAL_CLOSURE, "cond"), + True, + ), + "received 1000 (OK) race; then sent 1000 (OK) cond", + ), + ( + ConnectionClosed( + Close(CloseCode.NORMAL_CLOSURE, "cond"), + Close(CloseCode.NORMAL_CLOSURE, "race"), + False, + ), + "sent 1000 (OK) race; then received 1000 (OK) cond", + ), + ( + ConnectionClosed( + None, + Close(CloseCode.MESSAGE_TOO_BIG, ""), + None, + ), + "sent 1009 (message too big); no close frame received", + ), + ( + ConnectionClosed( + Close(CloseCode.PROTOCOL_ERROR, ""), + None, + None, + ), + "received 1002 (protocol error); no close frame sent", + ), + ( + ConnectionClosedOK( + Close(CloseCode.NORMAL_CLOSURE, ""), + Close(CloseCode.NORMAL_CLOSURE, ""), + True, + ), + "received 1000 (OK); then sent 1000 (OK)", + ), + ( + ConnectionClosedError( + None, + None, + None, + ), + "no close frame received or sent", + ), + ( + InvalidHandshake("invalid request"), + "invalid request", + ), + ( + SecurityError("redirect from WSS to WS"), + "redirect from WSS to WS", + ), + ( + InvalidMessage("malformed HTTP message"), + "malformed HTTP message", + ), + ( + InvalidHeader("Name"), + "missing Name header", + ), + ( + InvalidHeader("Name", None), + "missing Name header", + ), + ( + InvalidHeader("Name", ""), + "empty Name header", + ), + ( + InvalidHeader("Name", "Value"), + "invalid Name header: Value", + ), + ( + InvalidHeaderFormat("Sec-WebSocket-Protocol", "exp. token", "a=|", 3), + "invalid Sec-WebSocket-Protocol header: exp. token at 3 in a=|", + ), + ( + InvalidHeaderValue("Sec-WebSocket-Version", "42"), + "invalid Sec-WebSocket-Version header: 42", + ), + ( + InvalidOrigin("http://bad.origin"), + "invalid Origin header: http://bad.origin", + ), + ( + InvalidUpgrade("Upgrade"), + "missing Upgrade header", + ), + ( + InvalidUpgrade("Connection", "websocket"), + "invalid Connection header: websocket", + ), + ( + InvalidStatus(Response(401, "Unauthorized", Headers())), + "server rejected WebSocket connection: HTTP 401", + ), + ( + InvalidStatusCode(403, Headers()), + "server rejected WebSocket connection: HTTP 403", + ), + ( + NegotiationError("unsupported subprotocol: spam"), + "unsupported subprotocol: spam", + ), + ( + DuplicateParameter("a"), + "duplicate parameter: a", + ), + ( + InvalidParameterName("|"), + "invalid parameter name: |", + ), + ( + InvalidParameterValue("a", None), + "missing value for parameter a", + ), + ( + InvalidParameterValue("a", ""), + "empty value for parameter a", + ), + ( + InvalidParameterValue("a", "|"), + "invalid value for parameter a: |", + ), + ( + AbortHandshake(200, Headers(), b"OK\n"), + "HTTP 200, 0 headers, 3 bytes", + ), + ( + RedirectHandshake("wss://example.com"), + "redirect to wss://example.com", + ), + ( + InvalidState("WebSocket connection isn't established yet"), + "WebSocket connection isn't established yet", + ), + ( + InvalidURI("|", "not at all!"), + "| isn't a valid URI: not at all!", + ), + ( + PayloadTooBig("payload length exceeds limit: 2 > 1 bytes"), + "payload length exceeds limit: 2 > 1 bytes", + ), + ( + ProtocolError("invalid opcode: 7"), + "invalid opcode: 7", + ), + ]: + with self.subTest(exception=exception): + self.assertEqual(str(exception), exception_str) + + def test_connection_closed_attributes_backwards_compatibility(self): + exception = ConnectionClosed(Close(CloseCode.NORMAL_CLOSURE, "OK"), None, None) + self.assertEqual(exception.code, CloseCode.NORMAL_CLOSURE) + self.assertEqual(exception.reason, "OK") + + def test_connection_closed_attributes_backwards_compatibility_defaults(self): + exception = ConnectionClosed(None, None, None) + self.assertEqual(exception.code, CloseCode.ABNORMAL_CLOSURE) + self.assertEqual(exception.reason, "") diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py new file mode 100644 index 0000000000..67a1a6f994 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py @@ -0,0 +1,30 @@ +import unittest + +import websockets +import websockets.auth +import websockets.client +import websockets.datastructures +import websockets.exceptions +import websockets.legacy.protocol +import websockets.server +import websockets.typing +import websockets.uri + + +combined_exports = ( + websockets.auth.__all__ + + websockets.client.__all__ + + websockets.datastructures.__all__ + + websockets.exceptions.__all__ + + websockets.legacy.protocol.__all__ + + websockets.server.__all__ + + websockets.typing.__all__ +) + + +class ExportsTests(unittest.TestCase): + def test_top_level_module_reexports_all_submodule_exports(self): + self.assertEqual(set(combined_exports), set(websockets.__all__)) + + def test_submodule_exports_are_globally_unique(self): + self.assertEqual(len(set(combined_exports)), len(combined_exports)) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_frames.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_frames.py new file mode 100644 index 0000000000..e323b3b57c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_frames.py @@ -0,0 +1,495 @@ +import codecs +import dataclasses +import unittest +import unittest.mock + +from websockets.exceptions import PayloadTooBig, ProtocolError +from websockets.frames import * +from websockets.frames import CloseCode +from websockets.streams import StreamReader + +from .utils import GeneratorTestCase + + +class FramesTestCase(GeneratorTestCase): + def enforce_mask(self, mask): + return unittest.mock.patch("secrets.token_bytes", return_value=mask) + + def parse(self, data, mask, max_size=None, extensions=None): + """ + Parse a frame from a bytestring. + + """ + reader = StreamReader() + reader.feed_data(data) + reader.feed_eof() + parser = Frame.parse( + reader.read_exact, mask=mask, max_size=max_size, extensions=extensions + ) + return self.assertGeneratorReturns(parser) + + def assertFrameData(self, frame, data, mask, extensions=None): + """ + Serializing frame yields data. Parsing data yields frame. + + """ + # Compare frames first, because test failures are easier to read, + # especially when mask = True. + parsed = self.parse(data, mask=mask, extensions=extensions) + self.assertEqual(parsed, frame) + + # Make masking deterministic by reusing the same "random" mask. + # This has an effect only when mask is True. + mask_bytes = data[2:6] if mask else b"" + with self.enforce_mask(mask_bytes): + serialized = frame.serialize(mask=mask, extensions=extensions) + self.assertEqual(serialized, data) + + +class FrameTests(FramesTestCase): + def test_text_unmasked(self): + self.assertFrameData( + Frame(OP_TEXT, b"Spam"), + b"\x81\x04Spam", + mask=False, + ) + + def test_text_masked(self): + self.assertFrameData( + Frame(OP_TEXT, b"Spam"), + b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5", + mask=True, + ) + + def test_binary_unmasked(self): + self.assertFrameData( + Frame(OP_BINARY, b"Eggs"), + b"\x82\x04Eggs", + mask=False, + ) + + def test_binary_masked(self): + self.assertFrameData( + Frame(OP_BINARY, b"Eggs"), + b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa", + mask=True, + ) + + def test_non_ascii_text_unmasked(self): + self.assertFrameData( + Frame(OP_TEXT, "café".encode("utf-8")), + b"\x81\x05caf\xc3\xa9", + mask=False, + ) + + def test_non_ascii_text_masked(self): + self.assertFrameData( + Frame(OP_TEXT, "café".encode("utf-8")), + b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd", + mask=True, + ) + + def test_close(self): + self.assertFrameData( + Frame(OP_CLOSE, b""), + b"\x88\x00", + mask=False, + ) + + def test_ping(self): + self.assertFrameData( + Frame(OP_PING, b"ping"), + b"\x89\x04ping", + mask=False, + ) + + def test_pong(self): + self.assertFrameData( + Frame(OP_PONG, b"pong"), + b"\x8a\x04pong", + mask=False, + ) + + def test_long(self): + self.assertFrameData( + Frame(OP_BINARY, 126 * b"a"), + b"\x82\x7e\x00\x7e" + 126 * b"a", + mask=False, + ) + + def test_very_long(self): + self.assertFrameData( + Frame(OP_BINARY, 65536 * b"a"), + b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a", + mask=False, + ) + + def test_payload_too_big(self): + with self.assertRaises(PayloadTooBig): + self.parse(b"\x82\x7e\x04\x01" + 1025 * b"a", mask=False, max_size=1024) + + def test_bad_reserved_bits(self): + for data in [b"\xc0\x00", b"\xa0\x00", b"\x90\x00"]: + with self.subTest(data=data): + with self.assertRaises(ProtocolError): + self.parse(data, mask=False) + + def test_good_opcode(self): + for opcode in list(range(0x00, 0x03)) + list(range(0x08, 0x0B)): + data = bytes([0x80 | opcode, 0]) + with self.subTest(data=data): + self.parse(data, mask=False) # does not raise an exception + + def test_bad_opcode(self): + for opcode in list(range(0x03, 0x08)) + list(range(0x0B, 0x10)): + data = bytes([0x80 | opcode, 0]) + with self.subTest(data=data): + with self.assertRaises(ProtocolError): + self.parse(data, mask=False) + + def test_mask_flag(self): + # Mask flag correctly set. + self.parse(b"\x80\x80\x00\x00\x00\x00", mask=True) + # Mask flag incorrectly unset. + with self.assertRaises(ProtocolError): + self.parse(b"\x80\x80\x00\x00\x00\x00", mask=False) + # Mask flag correctly unset. + self.parse(b"\x80\x00", mask=False) + # Mask flag incorrectly set. + with self.assertRaises(ProtocolError): + self.parse(b"\x80\x00", mask=True) + + def test_control_frame_max_length(self): + # At maximum allowed length. + self.parse(b"\x88\x7e\x00\x7d" + 125 * b"a", mask=False) + # Above maximum allowed length. + with self.assertRaises(ProtocolError): + self.parse(b"\x88\x7e\x00\x7e" + 126 * b"a", mask=False) + + def test_fragmented_control_frame(self): + # Fin bit correctly set. + self.parse(b"\x88\x00", mask=False) + # Fin bit incorrectly unset. + with self.assertRaises(ProtocolError): + self.parse(b"\x08\x00", mask=False) + + def test_extensions(self): + class Rot13: + @staticmethod + def encode(frame): + assert frame.opcode == OP_TEXT + text = frame.data.decode() + data = codecs.encode(text, "rot13").encode() + return dataclasses.replace(frame, data=data) + + # This extensions is symmetrical. + @staticmethod + def decode(frame, *, max_size=None): + return Rot13.encode(frame) + + self.assertFrameData( + Frame(OP_TEXT, b"hello"), + b"\x81\x05uryyb", + mask=False, + extensions=[Rot13()], + ) + + +class StrTests(unittest.TestCase): + def test_cont_text(self): + self.assertEqual( + str(Frame(OP_CONT, b" cr\xc3\xa8me", fin=False)), + "CONT ' crème' [text, 7 bytes, continued]", + ) + + def test_cont_binary(self): + self.assertEqual( + str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff", fin=False)), + "CONT fc fd fe ff [binary, 4 bytes, continued]", + ) + + def test_cont_binary_from_memoryview(self): + self.assertEqual( + str(Frame(OP_CONT, memoryview(b"\xfc\xfd\xfe\xff"), fin=False)), + "CONT fc fd fe ff [binary, 4 bytes, continued]", + ) + + def test_cont_final_text(self): + self.assertEqual( + str(Frame(OP_CONT, b" cr\xc3\xa8me")), + "CONT ' crème' [text, 7 bytes]", + ) + + def test_cont_final_binary(self): + self.assertEqual( + str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff")), + "CONT fc fd fe ff [binary, 4 bytes]", + ) + + def test_cont_final_binary_from_memoryview(self): + self.assertEqual( + str(Frame(OP_CONT, memoryview(b"\xfc\xfd\xfe\xff"))), + "CONT fc fd fe ff [binary, 4 bytes]", + ) + + def test_cont_text_truncated(self): + self.assertEqual( + str(Frame(OP_CONT, b"caf\xc3\xa9 " * 16, fin=False)), + "CONT 'café café café café café café café café café ca..." + "fé café café café café ' [text, 96 bytes, continued]", + ) + + def test_cont_binary_truncated(self): + self.assertEqual( + str(Frame(OP_CONT, bytes(range(256)), fin=False)), + "CONT 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [binary, 256 bytes, continued]", + ) + + def test_cont_binary_truncated_from_memoryview(self): + self.assertEqual( + str(Frame(OP_CONT, memoryview(bytes(range(256))), fin=False)), + "CONT 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [binary, 256 bytes, continued]", + ) + + def test_text(self): + self.assertEqual( + str(Frame(OP_TEXT, b"caf\xc3\xa9")), + "TEXT 'café' [5 bytes]", + ) + + def test_text_non_final(self): + self.assertEqual( + str(Frame(OP_TEXT, b"caf\xc3\xa9", fin=False)), + "TEXT 'café' [5 bytes, continued]", + ) + + def test_text_truncated(self): + self.assertEqual( + str(Frame(OP_TEXT, b"caf\xc3\xa9 " * 16)), + "TEXT 'café café café café café café café café café ca..." + "fé café café café café ' [96 bytes]", + ) + + def test_text_with_newline(self): + self.assertEqual( + str(Frame(OP_TEXT, b"Hello\nworld!")), + "TEXT 'Hello\\nworld!' [12 bytes]", + ) + + def test_binary(self): + self.assertEqual( + str(Frame(OP_BINARY, b"\x00\x01\x02\x03")), + "BINARY 00 01 02 03 [4 bytes]", + ) + + def test_binary_from_memoryview(self): + self.assertEqual( + str(Frame(OP_BINARY, memoryview(b"\x00\x01\x02\x03"))), + "BINARY 00 01 02 03 [4 bytes]", + ) + + def test_binary_non_final(self): + self.assertEqual( + str(Frame(OP_BINARY, b"\x00\x01\x02\x03", fin=False)), + "BINARY 00 01 02 03 [4 bytes, continued]", + ) + + def test_binary_non_final_from_memoryview(self): + self.assertEqual( + str(Frame(OP_BINARY, memoryview(b"\x00\x01\x02\x03"), fin=False)), + "BINARY 00 01 02 03 [4 bytes, continued]", + ) + + def test_binary_truncated(self): + self.assertEqual( + str(Frame(OP_BINARY, bytes(range(256)))), + "BINARY 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [256 bytes]", + ) + + def test_binary_truncated_from_memoryview(self): + self.assertEqual( + str(Frame(OP_BINARY, memoryview(bytes(range(256))))), + "BINARY 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [256 bytes]", + ) + + def test_close(self): + self.assertEqual( + str(Frame(OP_CLOSE, b"\x03\xe8")), + "CLOSE 1000 (OK) [2 bytes]", + ) + + def test_close_reason(self): + self.assertEqual( + str(Frame(OP_CLOSE, b"\x03\xe9Bye!")), + "CLOSE 1001 (going away) Bye! [6 bytes]", + ) + + def test_ping(self): + self.assertEqual( + str(Frame(OP_PING, b"")), + "PING '' [0 bytes]", + ) + + def test_ping_text(self): + self.assertEqual( + str(Frame(OP_PING, b"ping")), + "PING 'ping' [text, 4 bytes]", + ) + + def test_ping_text_with_newline(self): + self.assertEqual( + str(Frame(OP_PING, b"ping\n")), + "PING 'ping\\n' [text, 5 bytes]", + ) + + def test_ping_binary(self): + self.assertEqual( + str(Frame(OP_PING, b"\xff\x00\xff\x00")), + "PING ff 00 ff 00 [binary, 4 bytes]", + ) + + def test_pong(self): + self.assertEqual( + str(Frame(OP_PONG, b"")), + "PONG '' [0 bytes]", + ) + + def test_pong_text(self): + self.assertEqual( + str(Frame(OP_PONG, b"pong")), + "PONG 'pong' [text, 4 bytes]", + ) + + def test_pong_text_with_newline(self): + self.assertEqual( + str(Frame(OP_PONG, b"pong\n")), + "PONG 'pong\\n' [text, 5 bytes]", + ) + + def test_pong_binary(self): + self.assertEqual( + str(Frame(OP_PONG, b"\xff\x00\xff\x00")), + "PONG ff 00 ff 00 [binary, 4 bytes]", + ) + + +class PrepareDataTests(unittest.TestCase): + def test_prepare_data_str(self): + self.assertEqual( + prepare_data("café"), + (OP_TEXT, b"caf\xc3\xa9"), + ) + + def test_prepare_data_bytes(self): + self.assertEqual( + prepare_data(b"tea"), + (OP_BINARY, b"tea"), + ) + + def test_prepare_data_bytearray(self): + self.assertEqual( + prepare_data(bytearray(b"tea")), + (OP_BINARY, bytearray(b"tea")), + ) + + def test_prepare_data_memoryview(self): + self.assertEqual( + prepare_data(memoryview(b"tea")), + (OP_BINARY, memoryview(b"tea")), + ) + + def test_prepare_data_list(self): + with self.assertRaises(TypeError): + prepare_data([]) + + def test_prepare_data_none(self): + with self.assertRaises(TypeError): + prepare_data(None) + + +class PrepareCtrlTests(unittest.TestCase): + def test_prepare_ctrl_str(self): + self.assertEqual(prepare_ctrl("café"), b"caf\xc3\xa9") + + def test_prepare_ctrl_bytes(self): + self.assertEqual(prepare_ctrl(b"tea"), b"tea") + + def test_prepare_ctrl_bytearray(self): + self.assertEqual(prepare_ctrl(bytearray(b"tea")), b"tea") + + def test_prepare_ctrl_memoryview(self): + self.assertEqual(prepare_ctrl(memoryview(b"tea")), b"tea") + + def test_prepare_ctrl_list(self): + with self.assertRaises(TypeError): + prepare_ctrl([]) + + def test_prepare_ctrl_none(self): + with self.assertRaises(TypeError): + prepare_ctrl(None) + + +class CloseTests(unittest.TestCase): + def assertCloseData(self, close, data): + """ + Serializing close yields data. Parsing data yields close. + + """ + serialized = close.serialize() + self.assertEqual(serialized, data) + parsed = Close.parse(data) + self.assertEqual(parsed, close) + + def test_str(self): + self.assertEqual( + str(Close(CloseCode.NORMAL_CLOSURE, "")), + "1000 (OK)", + ) + self.assertEqual( + str(Close(CloseCode.GOING_AWAY, "Bye!")), + "1001 (going away) Bye!", + ) + self.assertEqual( + str(Close(3000, "")), + "3000 (registered)", + ) + self.assertEqual( + str(Close(4000, "")), + "4000 (private use)", + ) + self.assertEqual( + str(Close(5000, "")), + "5000 (unknown)", + ) + + def test_parse_and_serialize(self): + self.assertCloseData( + Close(CloseCode.NORMAL_CLOSURE, "OK"), + b"\x03\xe8OK", + ) + self.assertCloseData( + Close(CloseCode.GOING_AWAY, ""), + b"\x03\xe9", + ) + + def test_parse_empty(self): + self.assertEqual( + Close.parse(b""), + Close(CloseCode.NO_STATUS_RCVD, ""), + ) + + def test_parse_errors(self): + with self.assertRaises(ProtocolError): + Close.parse(b"\x03") + with self.assertRaises(ProtocolError): + Close.parse(b"\x03\xe7") + with self.assertRaises(UnicodeDecodeError): + Close.parse(b"\x03\xe8\xff\xff") + + def test_serialize_errors(self): + with self.assertRaises(ProtocolError): + Close(999, "").serialize() diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py new file mode 100644 index 0000000000..4ebd8b90cf --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py @@ -0,0 +1,222 @@ +import unittest + +from websockets.exceptions import InvalidHeaderFormat, InvalidHeaderValue +from websockets.headers import * + + +class HeadersTests(unittest.TestCase): + def test_build_host(self): + for (host, port, secure), result in [ + (("localhost", 80, False), "localhost"), + (("localhost", 8000, False), "localhost:8000"), + (("localhost", 443, True), "localhost"), + (("localhost", 8443, True), "localhost:8443"), + (("example.com", 80, False), "example.com"), + (("example.com", 8000, False), "example.com:8000"), + (("example.com", 443, True), "example.com"), + (("example.com", 8443, True), "example.com:8443"), + (("127.0.0.1", 80, False), "127.0.0.1"), + (("127.0.0.1", 8000, False), "127.0.0.1:8000"), + (("127.0.0.1", 443, True), "127.0.0.1"), + (("127.0.0.1", 8443, True), "127.0.0.1:8443"), + (("::1", 80, False), "[::1]"), + (("::1", 8000, False), "[::1]:8000"), + (("::1", 443, True), "[::1]"), + (("::1", 8443, True), "[::1]:8443"), + ]: + with self.subTest(host=host, port=port, secure=secure): + self.assertEqual(build_host(host, port, secure), result) + + def test_parse_connection(self): + for header, parsed in [ + # Realistic use cases + ("Upgrade", ["Upgrade"]), # Safari, Chrome + ("keep-alive, Upgrade", ["keep-alive", "Upgrade"]), # Firefox + # Pathological example + (",,\t, , ,Upgrade ,,", ["Upgrade"]), + ]: + with self.subTest(header=header): + self.assertEqual(parse_connection(header), parsed) + + def test_parse_connection_invalid_header_format(self): + for header in ["???", "keep-alive; Upgrade"]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_connection(header) + + def test_parse_upgrade(self): + for header, parsed in [ + # Realistic use case + ("websocket", ["websocket"]), + # Synthetic example + ("http/3.0, websocket", ["http/3.0", "websocket"]), + # Pathological example + (",, WebSocket, \t,,", ["WebSocket"]), + ]: + with self.subTest(header=header): + self.assertEqual(parse_upgrade(header), parsed) + + def test_parse_upgrade_invalid_header_format(self): + for header in ["???", "websocket 2", "http/3.0; websocket"]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_upgrade(header) + + def test_parse_extension(self): + for header, parsed in [ + # Synthetic examples + ("foo", [("foo", [])]), + ("foo, bar", [("foo", []), ("bar", [])]), + ( + 'foo; name; token=token; quoted-string="quoted-string", ' + "bar; quux; quuux", + [ + ( + "foo", + [ + ("name", None), + ("token", "token"), + ("quoted-string", "quoted-string"), + ], + ), + ("bar", [("quux", None), ("quuux", None)]), + ], + ), + # Pathological example + ( + ",\t, , ,foo ;bar = 42,, baz,,", + [("foo", [("bar", "42")]), ("baz", [])], + ), + # Realistic use cases for permessage-deflate + ("permessage-deflate", [("permessage-deflate", [])]), + ( + "permessage-deflate; client_max_window_bits", + [("permessage-deflate", [("client_max_window_bits", None)])], + ), + ( + "permessage-deflate; server_max_window_bits=10", + [("permessage-deflate", [("server_max_window_bits", "10")])], + ), + ]: + with self.subTest(header=header): + self.assertEqual(parse_extension(header), parsed) + # Also ensure that build_extension round-trips cleanly. + unparsed = build_extension(parsed) + self.assertEqual(parse_extension(unparsed), parsed) + + def test_parse_extension_invalid_header_format(self): + for header in [ + # Truncated examples + "", + ",\t,", + "foo;", + "foo; bar;", + "foo; bar=", + 'foo; bar="baz', + # Wrong delimiter + "foo, bar, baz=quux; quuux", + # Value in quoted string parameter that isn't a token + 'foo; bar=" "', + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_extension(header) + + def test_parse_subprotocol(self): + for header, parsed in [ + # Synthetic examples + ("foo", ["foo"]), + ("foo, bar", ["foo", "bar"]), + # Pathological example + (",\t, , ,foo ,, bar,baz,,", ["foo", "bar", "baz"]), + ]: + with self.subTest(header=header): + self.assertEqual(parse_subprotocol(header), parsed) + # Also ensure that build_subprotocol round-trips cleanly. + unparsed = build_subprotocol(parsed) + self.assertEqual(parse_subprotocol(unparsed), parsed) + + def test_parse_subprotocol_invalid_header(self): + for header in [ + # Truncated examples + "", + ",\t,", + # Wrong delimiter + "foo; bar", + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_subprotocol(header) + + def test_validate_subprotocols(self): + for subprotocols in [[], ["sip"], ["v1.usp"], ["sip", "v1.usp"]]: + with self.subTest(subprotocols=subprotocols): + validate_subprotocols(subprotocols) + + def test_validate_subprotocols_invalid(self): + for subprotocols, exception in [ + ({"sip": None}, TypeError), + ("sip", TypeError), + ([""], ValueError), + ]: + with self.subTest(subprotocols=subprotocols): + with self.assertRaises(exception): + validate_subprotocols(subprotocols) + + def test_build_www_authenticate_basic(self): + # Test vector from RFC 7617 + self.assertEqual( + build_www_authenticate_basic("foo"), 'Basic realm="foo", charset="UTF-8"' + ) + + def test_build_www_authenticate_basic_invalid_realm(self): + # Realm contains a control character forbidden in quoted-string encoding + with self.assertRaises(ValueError): + build_www_authenticate_basic("\u0007") + + def test_build_authorization_basic(self): + # Test vector from RFC 7617 + self.assertEqual( + build_authorization_basic("Aladdin", "open sesame"), + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + ) + + def test_build_authorization_basic_utf8(self): + # Test vector from RFC 7617 + self.assertEqual( + build_authorization_basic("test", "123£"), "Basic dGVzdDoxMjPCow==" + ) + + def test_parse_authorization_basic(self): + for header, parsed in [ + ("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")), + # Password contains non-ASCII character + ("Basic dGVzdDoxMjPCow==", ("test", "123£")), + # Password contains a colon + ("Basic YWxhZGRpbjpvcGVuOnNlc2FtZQ==", ("aladdin", "open:sesame")), + # Scheme name must be case insensitive + ("basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")), + ]: + with self.subTest(header=header): + self.assertEqual(parse_authorization_basic(header), parsed) + + def test_parse_authorization_basic_invalid_header_format(self): + for header in [ + "// Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "Basic\tQWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "Basic ****************************", + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== //", + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_authorization_basic(header) + + def test_parse_authorization_basic_invalid_header_value(self): + for header in [ + "Digest ...", + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ", + "Basic QWxhZGNlc2FtZQ==", + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderValue): + parse_authorization_basic(header) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py new file mode 100644 index 0000000000..036bc14102 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py @@ -0,0 +1 @@ +from websockets.http import * diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_http11.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http11.py new file mode 100644 index 0000000000..d2e5e04627 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http11.py @@ -0,0 +1,344 @@ +from websockets.datastructures import Headers +from websockets.exceptions import SecurityError +from websockets.http11 import * +from websockets.http11 import parse_headers +from websockets.streams import StreamReader + +from .utils import GeneratorTestCase + + +class RequestTests(GeneratorTestCase): + def setUp(self): + super().setUp() + self.reader = StreamReader() + + def parse(self): + return Request.parse(self.reader.read_line) + + def test_parse(self): + # Example from the protocol overview in RFC 6455 + self.reader.feed_data( + b"GET /chat HTTP/1.1\r\n" + b"Host: server.example.com\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + b"Origin: http://example.com\r\n" + b"Sec-WebSocket-Protocol: chat, superchat\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n" + ) + request = self.assertGeneratorReturns(self.parse()) + self.assertEqual(request.path, "/chat") + self.assertEqual(request.headers["Upgrade"], "websocket") + + def test_parse_empty(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "connection closed while reading HTTP request line", + ) + + def test_parse_invalid_request_line(self): + self.reader.feed_data(b"GET /\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP request line: GET /", + ) + + def test_parse_unsupported_method(self): + self.reader.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP method: OPTIONS", + ) + + def test_parse_unsupported_version(self): + self.reader.feed_data(b"GET /chat HTTP/1.0\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP version: HTTP/1.0", + ) + + def test_parse_invalid_header(self): + self.reader.feed_data(b"GET /chat HTTP/1.1\r\nOops\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP header line: Oops", + ) + + def test_parse_body(self): + self.reader.feed_data(b"GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\nYo\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported request body", + ) + + def test_parse_body_with_transfer_encoding(self): + self.reader.feed_data(b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n") + with self.assertRaises(NotImplementedError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "transfer codings aren't supported", + ) + + def test_serialize(self): + # Example from the protocol overview in RFC 6455 + request = Request( + "/chat", + Headers( + [ + ("Host", "server.example.com"), + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ=="), + ("Origin", "http://example.com"), + ("Sec-WebSocket-Protocol", "chat, superchat"), + ("Sec-WebSocket-Version", "13"), + ] + ), + ) + self.assertEqual( + request.serialize(), + b"GET /chat HTTP/1.1\r\n" + b"Host: server.example.com\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + b"Origin: http://example.com\r\n" + b"Sec-WebSocket-Protocol: chat, superchat\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + + +class ResponseTests(GeneratorTestCase): + def setUp(self): + super().setUp() + self.reader = StreamReader() + + def parse(self): + return Response.parse( + self.reader.read_line, + self.reader.read_exact, + self.reader.read_to_eof, + ) + + def test_parse(self): + # Example from the protocol overview in RFC 6455 + self.reader.feed_data( + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + b"Sec-WebSocket-Protocol: chat\r\n" + b"\r\n" + ) + response = self.assertGeneratorReturns(self.parse()) + self.assertEqual(response.status_code, 101) + self.assertEqual(response.reason_phrase, "Switching Protocols") + self.assertEqual(response.headers["Upgrade"], "websocket") + self.assertIsNone(response.body) + + def test_parse_empty(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "connection closed while reading HTTP status line", + ) + + def test_parse_invalid_status_line(self): + self.reader.feed_data(b"Hello!\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP status line: Hello!", + ) + + def test_parse_unsupported_version(self): + self.reader.feed_data(b"HTTP/1.0 400 Bad Request\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP version: HTTP/1.0", + ) + + def test_parse_invalid_status(self): + self.reader.feed_data(b"HTTP/1.1 OMG WTF\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP status code: OMG", + ) + + def test_parse_unsupported_status(self): + self.reader.feed_data(b"HTTP/1.1 007 My name is Bond\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP status code: 007", + ) + + def test_parse_invalid_reason(self): + self.reader.feed_data(b"HTTP/1.1 200 \x7f\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP reason phrase: \x7f", + ) + + def test_parse_invalid_header(self): + self.reader.feed_data(b"HTTP/1.1 500 Internal Server Error\r\nOops\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP header line: Oops", + ) + + def test_parse_body_with_content_length(self): + self.reader.feed_data( + b"HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello world!\n" + ) + response = self.assertGeneratorReturns(self.parse()) + self.assertEqual(response.body, b"Hello world!\n") + + def test_parse_body_without_content_length(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\n\r\nHello world!\n") + gen = self.parse() + self.assertGeneratorRunning(gen) + self.reader.feed_eof() + response = self.assertGeneratorReturns(gen) + self.assertEqual(response.body, b"Hello world!\n") + + def test_parse_body_with_content_length_too_long(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 1048577\r\n\r\n") + with self.assertRaises(SecurityError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "body too large: 1048577 bytes", + ) + + def test_parse_body_without_content_length_too_long(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\n\r\n" + b"a" * 1048577) + with self.assertRaises(SecurityError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "body too large: over 1048576 bytes", + ) + + def test_parse_body_with_transfer_encoding(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n") + with self.assertRaises(NotImplementedError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "transfer codings aren't supported", + ) + + def test_parse_body_no_content(self): + self.reader.feed_data(b"HTTP/1.1 204 No Content\r\n\r\n") + response = self.assertGeneratorReturns(self.parse()) + self.assertIsNone(response.body) + + def test_parse_body_not_modified(self): + self.reader.feed_data(b"HTTP/1.1 304 Not Modified\r\n\r\n") + response = self.assertGeneratorReturns(self.parse()) + self.assertIsNone(response.body) + + def test_serialize(self): + # Example from the protocol overview in RFC 6455 + response = Response( + 101, + "Switching Protocols", + Headers( + [ + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="), + ("Sec-WebSocket-Protocol", "chat"), + ] + ), + ) + self.assertEqual( + response.serialize(), + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + b"Sec-WebSocket-Protocol: chat\r\n" + b"\r\n", + ) + + def test_serialize_with_body(self): + response = Response( + 200, + "OK", + Headers([("Content-Length", "13"), ("Content-Type", "text/plain")]), + b"Hello world!\n", + ) + self.assertEqual( + response.serialize(), + b"HTTP/1.1 200 OK\r\n" + b"Content-Length: 13\r\n" + b"Content-Type: text/plain\r\n" + b"\r\n" + b"Hello world!\n", + ) + + +class HeadersTests(GeneratorTestCase): + def setUp(self): + super().setUp() + self.reader = StreamReader() + + def parse_headers(self): + return parse_headers(self.reader.read_line) + + def test_parse_invalid_name(self): + self.reader.feed_data(b"foo bar: baz qux\r\n\r\n") + with self.assertRaises(ValueError): + next(self.parse_headers()) + + def test_parse_invalid_value(self): + self.reader.feed_data(b"foo: \x00\x00\x0f\r\n\r\n") + with self.assertRaises(ValueError): + next(self.parse_headers()) + + def test_parse_too_long_value(self): + self.reader.feed_data(b"foo: bar\r\n" * 129 + b"\r\n") + with self.assertRaises(SecurityError): + next(self.parse_headers()) + + def test_parse_too_long_line(self): + # Header line contains 5 + 8186 + 2 = 8193 bytes. + self.reader.feed_data(b"foo: " + b"a" * 8186 + b"\r\n\r\n") + with self.assertRaises(SecurityError): + next(self.parse_headers()) + + def test_parse_invalid_line_ending(self): + self.reader.feed_data(b"foo: bar\n\n") + with self.assertRaises(EOFError): + next(self.parse_headers()) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_imports.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_imports.py new file mode 100644 index 0000000000..b69ed93162 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_imports.py @@ -0,0 +1,64 @@ +import types +import unittest +import warnings + +from websockets.imports import * + + +foo = object() + +bar = object() + + +class ImportsTests(unittest.TestCase): + def setUp(self): + self.mod = types.ModuleType("tests.test_imports.test_alias") + self.mod.__package__ = self.mod.__name__ + + def test_get_alias(self): + lazy_import( + vars(self.mod), + aliases={"foo": "...test_imports"}, + ) + + self.assertEqual(self.mod.foo, foo) + + def test_get_deprecated_alias(self): + lazy_import( + vars(self.mod), + deprecated_aliases={"bar": "...test_imports"}, + ) + + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.assertEqual(self.mod.bar, bar) + + self.assertEqual(len(recorded_warnings), 1) + warning = recorded_warnings[0].message + self.assertEqual( + str(warning), "tests.test_imports.test_alias.bar is deprecated" + ) + self.assertEqual(type(warning), DeprecationWarning) + + def test_dir(self): + lazy_import( + vars(self.mod), + aliases={"foo": "...test_imports"}, + deprecated_aliases={"bar": "...test_imports"}, + ) + + self.assertEqual( + [item for item in dir(self.mod) if not item[:2] == item[-2:] == "__"], + ["bar", "foo"], + ) + + def test_attribute_error(self): + lazy_import(vars(self.mod)) + + with self.assertRaises(AttributeError) as raised: + self.mod.foo + + self.assertEqual( + str(raised.exception), + "module 'tests.test_imports.test_alias' has no attribute 'foo'", + ) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf new file mode 100644 index 0000000000..4069e39670 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf @@ -0,0 +1,27 @@ +[ req ] + +default_md = sha256 +encrypt_key = no + +prompt = no + +distinguished_name = dn +x509_extensions = ext + +[ dn ] + +C = "FR" +L = "Paris" +O = "Aymeric Augustin" +CN = "localhost" + +[ ext ] + +subjectAltName = @san + +[ san ] + +DNS.1 = localhost +DNS.2 = overridden +IP.3 = 127.0.0.1 +IP.4 = ::1 diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem new file mode 100644 index 0000000000..8df63ec8f4 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem @@ -0,0 +1,48 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYOOQyq8yYtn5x +K3yRborFxTFse16JIVb4x/ZhZgGm49eARCi09fmczQxJdQpHz81Ij6z0xi7AUYH7 +9wS8T0Lh3uGFDDS1GzITUVPIqSUi0xim2T6XPzXFVQYI1D/OjUxlHm+3/up+WwbL +sBgBO/lDmzoa3ZN7kt9HQoGc/14oQz1Qsv1QTDQs69r+o7mmBJr/hf/g7S0Csyy3 +iC6aaq+yCUyzDbjXceTI7WJqbTGNnK0/DjdFD/SJS/uSDNEg0AH53eqcCSjm+Ei/ +UF8qR5Pu4sSsNwToOW2MVgjtHFazc+kG3rzD6+3Dp+t6x6uI/npyuudOMCmOtd6z +kX0UPQaNAgMBAAECggEAS4eMBztGC+5rusKTEAZKSY15l0h9HG/d/qdzJFDKsO6T +/8VPZu8pk6F48kwFHFK1hexSYWq9OAcA3fBK4jDZzybZJm2+F6l5U5AsMUMMqt6M +lPP8Tj8RXG433muuIkvvbL82DVLpvNu1Qv+vUvcNOpWFtY7DDv6eKjlMJ3h4/pzh +89MNt26VMCYOlq1NSjuZBzFohL2u9nsFehlOpcVsqNfNfcYCq9+5yoH8fWJP90Op +hqhvqUoGLN7DRKV1f+AWHSA4nmGgvVviV5PQgMhtk5exlN7kG+rDc3LbzhefS1Sp +Tat1qIgm8fK2n+Q/obQPjHOGOGuvE5cIF7E275ZKgQKBgQDt87BqALKWnbkbQnb7 +GS1h6LRcKyZhFbxnO2qbviBWSo15LEF8jPGV33Dj+T56hqufa/rUkbZiUbIR9yOX +dnOwpAVTo+ObAwZfGfHvrnufiIbHFqJBumaYLqjRZ7AC0QtS3G+kjS9dbllrr7ok +fO4JdfKRXzBJKrkQdCn8hR22rQKBgQDon0b49Dxs1EfdSDbDode2TSwE83fI3vmR +SKUkNY8ma6CRbomVRWijhBM458wJeuhpjPZOvjNMsnDzGwrtdAp2VfFlMIDnA8ZC +fEWIAAH2QYKXKGmkoXOcWB2QbvbI154zCm6zFGtzvRKOCGmTXuhFajO8VPwOyJVt +aSJA3bLrYQKBgQDJM2/tAfAAKRdW9GlUwqI8Ep9G+/l0yANJqtTnIemH7XwYhJJO +9YJlPszfB2aMBgliQNSUHy1/jyKpzDYdITyLlPUoFwEilnkxuud2yiuf5rpH51yF +hU6wyWtXvXv3tbkEdH42PmdZcjBMPQeBSN2hxEi6ISncBDL9tau26PwJ9QKBgQCs +cNYl2reoXTzgtpWSNDk6NL769JjJWTFcF6QD0YhKjOI8rNpkw00sWc3+EybXqDr9 +c7dq6+gPZQAB1vwkxi6zRkZqIqiLl+qygnjwtkC+EhYCg7y8g8q2DUPtO7TJcb0e +TQ9+xRZad8B3dZj93A8G1hF//OfU9bB/qL3xo+bsQQKBgC/9YJvgLIWA/UziLcB2 +29Ai0nbPkN5df7z4PifUHHSlbQJHKak8UKbMP+8S064Ul0F7g8UCjZMk2LzSbaNY +XU5+2j0sIOnGUFoSlvcpdowzYrD2LN5PkKBot7AOq/v7HlcOoR8J8RGWAMpCrHsI +a/u/dlZs+/K16RcavQwx8rag +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDWTCCAkGgAwIBAgIJAOL9UKiOOxupMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV +BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp +bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIyMTAxNTE5Mjg0MVoYDzIwNjQxMDE0 +MTkyODQxWjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM +EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANg45DKrzJi2fnErfJFuisXFMWx7XokhVvjH +9mFmAabj14BEKLT1+ZzNDEl1CkfPzUiPrPTGLsBRgfv3BLxPQuHe4YUMNLUbMhNR +U8ipJSLTGKbZPpc/NcVVBgjUP86NTGUeb7f+6n5bBsuwGAE7+UObOhrdk3uS30dC +gZz/XihDPVCy/VBMNCzr2v6juaYEmv+F/+DtLQKzLLeILppqr7IJTLMNuNdx5Mjt +YmptMY2crT8ON0UP9IlL+5IM0SDQAfnd6pwJKOb4SL9QXypHk+7ixKw3BOg5bYxW +CO0cVrNz6QbevMPr7cOn63rHq4j+enK6504wKY613rORfRQ9Bo0CAwEAAaM8MDow +OAYDVR0RBDEwL4IJbG9jYWxob3N0ggpvdmVycmlkZGVuhwR/AAABhxAAAAAAAAAA +AAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBPNDGDdl4wsCRlDuyCHBC8o+vW +Vb14thUw9Z6UrlsQRXLONxHOXbNAj1sYQACNwIWuNz36HXu5m8Xw/ID/bOhnIg+b +Y6l/JU/kZQYB7SV1aR3ZdbCK0gjfkE0POBHuKOjUFIOPBCtJ4tIBUX94zlgJrR9v +2rqJC3TIYrR7pVQumHZsI5GZEMpM5NxfreWwxcgltgxmGdm7elcizHfz7k5+szwh +4eZ/rxK9bw1q8BIvVBWelRvUR55mIrCjzfZp5ZObSYQTZlW7PzXBe5Jk+1w31YHM +RSBA2EpPhYlGNqPidi7bg7rnQcsc6+hE0OqzTL/hWxPm9Vbp9dj3HFTik1wa +-----END CERTIFICATE----- diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py new file mode 100644 index 0000000000..a64172b539 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py @@ -0,0 +1,1790 @@ +import logging +import unittest.mock + +from websockets.exceptions import ( + ConnectionClosedError, + ConnectionClosedOK, + InvalidState, + PayloadTooBig, + ProtocolError, +) +from websockets.frames import ( + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Frame, +) +from websockets.protocol import * +from websockets.protocol import CLIENT, CLOSED, CLOSING, SERVER + +from .extensions.utils import Rsv2Extension +from .test_frames import FramesTestCase + + +class ProtocolTestCase(FramesTestCase): + def assertFrameSent(self, connection, frame, eof=False): + """ + Outgoing data for ``connection`` contains the given frame. + + ``frame`` may be ``None`` if no frame is expected. + + When ``eof`` is ``True``, the end of the stream is also expected. + + """ + frames_sent = [ + None + if write is SEND_EOF + else self.parse( + write, + mask=connection.side is CLIENT, + extensions=connection.extensions, + ) + for write in connection.data_to_send() + ] + frames_expected = [] if frame is None else [frame] + if eof: + frames_expected += [None] + self.assertEqual(frames_sent, frames_expected) + + def assertFrameReceived(self, connection, frame): + """ + Incoming data for ``connection`` contains the given frame. + + ``frame`` may be ``None`` if no frame is expected. + + """ + frames_received = connection.events_received() + frames_expected = [] if frame is None else [frame] + self.assertEqual(frames_received, frames_expected) + + def assertConnectionClosing(self, connection, code=None, reason=""): + """ + Incoming data caused the "Start the WebSocket Closing Handshake" process. + + """ + close_frame = Frame( + OP_CLOSE, + b"" if code is None else Close(code, reason).serialize(), + ) + # A close frame was received. + self.assertFrameReceived(connection, close_frame) + # A close frame and possibly the end of stream were sent. + self.assertFrameSent(connection, close_frame, eof=connection.side is SERVER) + + def assertConnectionFailing(self, connection, code=None, reason=""): + """ + Incoming data caused the "Fail the WebSocket Connection" process. + + """ + close_frame = Frame( + OP_CLOSE, + b"" if code is None else Close(code, reason).serialize(), + ) + # No frame was received. + self.assertFrameReceived(connection, None) + # A close frame and possibly the end of stream were sent. + self.assertFrameSent(connection, close_frame, eof=connection.side is SERVER) + + +class MaskingTests(ProtocolTestCase): + """ + Test frame masking. + + 5.1. Overview + + """ + + unmasked_text_frame_date = b"\x81\x04Spam" + masked_text_frame_data = b"\x81\x84\x00\xff\x00\xff\x53\x8f\x61\x92" + + def test_client_sends_masked_frame(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\xff\x00\xff"): + client.send_text(b"Spam", True) + self.assertEqual(client.data_to_send(), [self.masked_text_frame_data]) + + def test_server_sends_unmasked_frame(self): + server = Protocol(SERVER) + server.send_text(b"Spam", True) + self.assertEqual(server.data_to_send(), [self.unmasked_text_frame_date]) + + def test_client_receives_unmasked_frame(self): + client = Protocol(CLIENT) + client.receive_data(self.unmasked_text_frame_date) + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"Spam"), + ) + + def test_server_receives_masked_frame(self): + server = Protocol(SERVER) + server.receive_data(self.masked_text_frame_data) + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"Spam"), + ) + + def test_client_receives_masked_frame(self): + client = Protocol(CLIENT) + client.receive_data(self.masked_text_frame_data) + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "incorrect masking") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "incorrect masking" + ) + + def test_server_receives_unmasked_frame(self): + server = Protocol(SERVER) + server.receive_data(self.unmasked_text_frame_date) + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "incorrect masking") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "incorrect masking" + ) + + +class ContinuationTests(ProtocolTestCase): + """ + Test continuation frames without text or binary frames. + + """ + + def test_client_sends_unexpected_continuation(self): + client = Protocol(CLIENT) + with self.assertRaises(ProtocolError) as raised: + client.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_server_sends_unexpected_continuation(self): + server = Protocol(SERVER) + with self.assertRaises(ProtocolError) as raised: + server.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_client_receives_unexpected_continuation(self): + client = Protocol(CLIENT) + client.receive_data(b"\x00\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "unexpected continuation frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "unexpected continuation frame" + ) + + def test_server_receives_unexpected_continuation(self): + server = Protocol(SERVER) + server.receive_data(b"\x00\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "unexpected continuation frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "unexpected continuation frame" + ) + + def test_client_sends_continuation_after_sending_close(self): + client = Protocol(CLIENT) + # Since it isn't possible to send a close frame in a fragmented + # message (see test_client_send_close_in_fragmented_message), in fact, + # this is the same test as test_client_sends_unexpected_continuation. + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + with self.assertRaises(ProtocolError) as raised: + client.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_server_sends_continuation_after_sending_close(self): + # Since it isn't possible to send a close frame in a fragmented + # message (see test_server_send_close_in_fragmented_message), in fact, + # this is the same test as test_server_sends_unexpected_continuation. + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + with self.assertRaises(ProtocolError) as raised: + server.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_client_receives_continuation_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x00\x00") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_continuation_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x00\x80\x00\xff\x00\xff") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class TextTests(ProtocolTestCase): + """ + Test text frames and continuation frames. + + """ + + def test_client_sends_text(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_text("😀".encode()) + self.assertEqual( + client.data_to_send(), [b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80"] + ) + + def test_server_sends_text(self): + server = Protocol(SERVER) + server.send_text("😀".encode()) + self.assertEqual(server.data_to_send(), [b"\x81\x04\xf0\x9f\x98\x80"]) + + def test_client_receives_text(self): + client = Protocol(CLIENT) + client.receive_data(b"\x81\x04\xf0\x9f\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_server_receives_text(self): + server = Protocol(SERVER) + server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_client_receives_text_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x81\x04\xf0\x9f\x98\x80") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_server_receives_text_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_client_receives_text_without_size_limit(self): + client = Protocol(CLIENT, max_size=None) + client.receive_data(b"\x81\x04\xf0\x9f\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_server_receives_text_without_size_limit(self): + server = Protocol(SERVER, max_size=None) + server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_client_sends_fragmented_text(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_text("😀".encode()[:2], fin=False) + self.assertEqual(client.data_to_send(), [b"\x01\x82\x00\x00\x00\x00\xf0\x9f"]) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation("😀😀".encode()[2:6], fin=False) + self.assertEqual( + client.data_to_send(), [b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f"] + ) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation("😀".encode()[2:], fin=True) + self.assertEqual(client.data_to_send(), [b"\x80\x82\x00\x00\x00\x00\x98\x80"]) + + def test_server_sends_fragmented_text(self): + server = Protocol(SERVER) + server.send_text("😀".encode()[:2], fin=False) + self.assertEqual(server.data_to_send(), [b"\x01\x02\xf0\x9f"]) + server.send_continuation("😀😀".encode()[2:6], fin=False) + self.assertEqual(server.data_to_send(), [b"\x00\x04\x98\x80\xf0\x9f"]) + server.send_continuation("😀".encode()[2:], fin=True) + self.assertEqual(server.data_to_send(), [b"\x80\x02\x98\x80"]) + + def test_client_receives_fragmented_text(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x02\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + client.receive_data(b"\x00\x04\x98\x80\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + client.receive_data(b"\x80\x02\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_server_receives_fragmented_text(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_client_receives_fragmented_text_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x01\x02\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + client.receive_data(b"\x80\x02\x98\x80") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_server_receives_fragmented_text_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_client_receives_fragmented_text_without_size_limit(self): + client = Protocol(CLIENT, max_size=None) + client.receive_data(b"\x01\x02\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + client.receive_data(b"\x00\x04\x98\x80\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + client.receive_data(b"\x80\x02\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_server_receives_fragmented_text_without_size_limit(self): + server = Protocol(SERVER, max_size=None) + server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_client_sends_unexpected_text(self): + client = Protocol(CLIENT) + client.send_text(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + client.send_text(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_server_sends_unexpected_text(self): + server = Protocol(SERVER) + server.send_text(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + server.send_text(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_client_receives_unexpected_text(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x00") + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"", fin=False), + ) + client.receive_data(b"\x01\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_server_receives_unexpected_text(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x80\x00\x00\x00\x00") + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"", fin=False), + ) + server.receive_data(b"\x01\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_client_sends_text_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + with self.assertRaises(InvalidState): + client.send_text(b"") + + def test_server_sends_text_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + with self.assertRaises(InvalidState): + server.send_text(b"") + + def test_client_receives_text_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x81\x00") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_text_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x81\x80\x00\xff\x00\xff") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class BinaryTests(ProtocolTestCase): + """ + Test binary frames and continuation frames. + + """ + + def test_client_sends_binary(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_binary(b"\x01\x02\xfe\xff") + self.assertEqual( + client.data_to_send(), [b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff"] + ) + + def test_server_sends_binary(self): + server = Protocol(SERVER) + server.send_binary(b"\x01\x02\xfe\xff") + self.assertEqual(server.data_to_send(), [b"\x82\x04\x01\x02\xfe\xff"]) + + def test_client_receives_binary(self): + client = Protocol(CLIENT) + client.receive_data(b"\x82\x04\x01\x02\xfe\xff") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"\x01\x02\xfe\xff"), + ) + + def test_server_receives_binary(self): + server = Protocol(SERVER) + server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"\x01\x02\xfe\xff"), + ) + + def test_client_receives_binary_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x82\x04\x01\x02\xfe\xff") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_server_receives_binary_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_client_sends_fragmented_binary(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_binary(b"\x01\x02", fin=False) + self.assertEqual(client.data_to_send(), [b"\x02\x82\x00\x00\x00\x00\x01\x02"]) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation(b"\xee\xff\x01\x02", fin=False) + self.assertEqual( + client.data_to_send(), [b"\x00\x84\x00\x00\x00\x00\xee\xff\x01\x02"] + ) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation(b"\xee\xff", fin=True) + self.assertEqual(client.data_to_send(), [b"\x80\x82\x00\x00\x00\x00\xee\xff"]) + + def test_server_sends_fragmented_binary(self): + server = Protocol(SERVER) + server.send_binary(b"\x01\x02", fin=False) + self.assertEqual(server.data_to_send(), [b"\x02\x02\x01\x02"]) + server.send_continuation(b"\xee\xff\x01\x02", fin=False) + self.assertEqual(server.data_to_send(), [b"\x00\x04\xee\xff\x01\x02"]) + server.send_continuation(b"\xee\xff", fin=True) + self.assertEqual(server.data_to_send(), [b"\x80\x02\xee\xff"]) + + def test_client_receives_fragmented_binary(self): + client = Protocol(CLIENT) + client.receive_data(b"\x02\x02\x01\x02") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + client.receive_data(b"\x00\x04\xfe\xff\x01\x02") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"\xfe\xff\x01\x02", fin=False), + ) + client.receive_data(b"\x80\x02\xfe\xff") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"\xfe\xff"), + ) + + def test_server_receives_fragmented_binary(self): + server = Protocol(SERVER) + server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + server.receive_data(b"\x00\x84\x00\x00\x00\x00\xee\xff\x01\x02") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"\xee\xff\x01\x02", fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"\xfe\xff"), + ) + + def test_client_receives_fragmented_binary_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x02\x02\x01\x02") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + client.receive_data(b"\x80\x02\xfe\xff") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_server_receives_fragmented_binary_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_client_sends_unexpected_binary(self): + client = Protocol(CLIENT) + client.send_binary(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + client.send_binary(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_server_sends_unexpected_binary(self): + server = Protocol(SERVER) + server.send_binary(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + server.send_binary(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_client_receives_unexpected_binary(self): + client = Protocol(CLIENT) + client.receive_data(b"\x02\x00") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"", fin=False), + ) + client.receive_data(b"\x02\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_server_receives_unexpected_binary(self): + server = Protocol(SERVER) + server.receive_data(b"\x02\x80\x00\x00\x00\x00") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"", fin=False), + ) + server.receive_data(b"\x02\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_client_sends_binary_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + with self.assertRaises(InvalidState): + client.send_binary(b"") + + def test_server_sends_binary_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + with self.assertRaises(InvalidState): + server.send_binary(b"") + + def test_client_receives_binary_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x82\x00") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_binary_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x82\x80\x00\xff\x00\xff") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class CloseTests(ProtocolTestCase): + """ + Test close frames. + + See RFC 6544: + + 5.5.1. Close + 7.1.6. The WebSocket Connection Close Reason + 7.1.7. Fail the WebSocket Connection + + """ + + def test_close_code(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x04\x03\xe8OK") + client.receive_eof() + self.assertEqual(client.close_code, CloseCode.NORMAL_CLOSURE) + + def test_close_reason(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x84\x00\x00\x00\x00\x03\xe8OK") + server.receive_eof() + self.assertEqual(server.close_reason, "OK") + + def test_close_code_not_provided(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x00\x00\x00\x00") + server.receive_eof() + self.assertEqual(server.close_code, CloseCode.NO_STATUS_RCVD) + + def test_close_reason_not_provided(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + client.receive_eof() + self.assertEqual(client.close_reason, "") + + def test_close_code_not_available(self): + client = Protocol(CLIENT) + client.receive_eof() + self.assertEqual(client.close_code, CloseCode.ABNORMAL_CLOSURE) + + def test_close_reason_not_available(self): + server = Protocol(SERVER) + server.receive_eof() + self.assertEqual(server.close_reason, "") + + def test_close_code_not_available_yet(self): + server = Protocol(SERVER) + self.assertIsNone(server.close_code) + + def test_close_reason_not_available_yet(self): + client = Protocol(CLIENT) + self.assertIsNone(client.close_reason) + + def test_client_sends_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x3c\x3c\x3c\x3c"): + client.send_close() + self.assertEqual(client.data_to_send(), [b"\x88\x80\x3c\x3c\x3c\x3c"]) + self.assertIs(client.state, CLOSING) + + def test_server_sends_close(self): + server = Protocol(SERVER) + server.send_close() + self.assertEqual(server.data_to_send(), [b"\x88\x00"]) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x3c\x3c\x3c\x3c"): + client.receive_data(b"\x88\x00") + self.assertEqual(client.events_received(), [Frame(OP_CLOSE, b"")]) + self.assertEqual(client.data_to_send(), [b"\x88\x80\x3c\x3c\x3c\x3c"]) + self.assertIs(client.state, CLOSING) + + def test_server_receives_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertEqual(server.events_received(), [Frame(OP_CLOSE, b"")]) + self.assertEqual(server.data_to_send(), [b"\x88\x00", b""]) + self.assertIs(server.state, CLOSING) + + def test_client_sends_close_then_receives_close(self): + # Client-initiated close handshake on the client side. + client = Protocol(CLIENT) + + client.send_close() + self.assertFrameReceived(client, None) + self.assertFrameSent(client, Frame(OP_CLOSE, b"")) + + client.receive_data(b"\x88\x00") + self.assertFrameReceived(client, Frame(OP_CLOSE, b"")) + self.assertFrameSent(client, None) + + client.receive_eof() + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None, eof=True) + + def test_server_sends_close_then_receives_close(self): + # Server-initiated close handshake on the server side. + server = Protocol(SERVER) + + server.send_close() + self.assertFrameReceived(server, None) + self.assertFrameSent(server, Frame(OP_CLOSE, b"")) + + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertFrameReceived(server, Frame(OP_CLOSE, b"")) + self.assertFrameSent(server, None, eof=True) + + server.receive_eof() + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + def test_client_receives_close_then_sends_close(self): + # Server-initiated close handshake on the client side. + client = Protocol(CLIENT) + + client.receive_data(b"\x88\x00") + self.assertFrameReceived(client, Frame(OP_CLOSE, b"")) + self.assertFrameSent(client, Frame(OP_CLOSE, b"")) + + client.receive_eof() + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None, eof=True) + + def test_server_receives_close_then_sends_close(self): + # Client-initiated close handshake on the server side. + server = Protocol(SERVER) + + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertFrameReceived(server, Frame(OP_CLOSE, b"")) + self.assertFrameSent(server, Frame(OP_CLOSE, b""), eof=True) + + server.receive_eof() + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + def test_client_sends_close_with_code(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + self.assertIs(client.state, CLOSING) + + def test_server_sends_close_with_code(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close_with_code(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE, "") + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_code(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY, "") + self.assertIs(server.state, CLOSING) + + def test_client_sends_close_with_code_and_reason(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY, "going away") + self.assertEqual( + client.data_to_send(), [b"\x88\x8c\x00\x00\x00\x00\x03\xe9going away"] + ) + self.assertIs(client.state, CLOSING) + + def test_server_sends_close_with_code_and_reason(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE, "OK") + self.assertEqual(server.data_to_send(), [b"\x88\x04\x03\xe8OK"]) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close_with_code_and_reason(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x04\x03\xe8OK") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE, "OK") + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_code_and_reason(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x8c\x00\x00\x00\x00\x03\xe9going away") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY, "going away") + self.assertIs(server.state, CLOSING) + + def test_client_sends_close_with_reason_only(self): + client = Protocol(CLIENT) + with self.assertRaises(ProtocolError) as raised: + client.send_close(reason="going away") + self.assertEqual(str(raised.exception), "cannot send a reason without a code") + + def test_server_sends_close_with_reason_only(self): + server = Protocol(SERVER) + with self.assertRaises(ProtocolError) as raised: + server.send_close(reason="OK") + self.assertEqual(str(raised.exception), "cannot send a reason without a code") + + def test_client_receives_close_with_truncated_code(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x01\x03") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "close frame too short") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "close frame too short" + ) + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_truncated_code(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x81\x00\x00\x00\x00\x03") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "close frame too short") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "close frame too short" + ) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close_with_non_utf8_reason(self): + client = Protocol(CLIENT) + + client.receive_data(b"\x88\x04\x03\xe8\xff\xff") + self.assertIsInstance(client.parser_exc, UnicodeDecodeError) + self.assertEqual( + str(client.parser_exc), + "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte", + ) + self.assertConnectionFailing( + client, CloseCode.INVALID_DATA, "invalid start byte at position 0" + ) + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_non_utf8_reason(self): + server = Protocol(SERVER) + + server.receive_data(b"\x88\x84\x00\x00\x00\x00\x03\xe9\xff\xff") + self.assertIsInstance(server.parser_exc, UnicodeDecodeError) + self.assertEqual( + str(server.parser_exc), + "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte", + ) + self.assertConnectionFailing( + server, CloseCode.INVALID_DATA, "invalid start byte at position 0" + ) + self.assertIs(server.state, CLOSING) + + +class PingTests(ProtocolTestCase): + """ + Test ping. See 5.5.2. Ping in RFC 6544. + + """ + + def test_client_sends_ping(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_ping(b"") + self.assertEqual(client.data_to_send(), [b"\x89\x80\x00\x44\x88\xcc"]) + + def test_server_sends_ping(self): + server = Protocol(SERVER) + server.send_ping(b"") + self.assertEqual(server.data_to_send(), [b"\x89\x00"]) + + def test_client_receives_ping(self): + client = Protocol(CLIENT) + client.receive_data(b"\x89\x00") + self.assertFrameReceived( + client, + Frame(OP_PING, b""), + ) + self.assertFrameSent( + client, + Frame(OP_PONG, b""), + ) + + def test_server_receives_ping(self): + server = Protocol(SERVER) + server.receive_data(b"\x89\x80\x00\x44\x88\xcc") + self.assertFrameReceived( + server, + Frame(OP_PING, b""), + ) + self.assertFrameSent( + server, + Frame(OP_PONG, b""), + ) + + def test_client_sends_ping_with_data(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_ping(b"\x22\x66\xaa\xee") + self.assertEqual( + client.data_to_send(), [b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22"] + ) + + def test_server_sends_ping_with_data(self): + server = Protocol(SERVER) + server.send_ping(b"\x22\x66\xaa\xee") + self.assertEqual(server.data_to_send(), [b"\x89\x04\x22\x66\xaa\xee"]) + + def test_client_receives_ping_with_data(self): + client = Protocol(CLIENT) + client.receive_data(b"\x89\x04\x22\x66\xaa\xee") + self.assertFrameReceived( + client, + Frame(OP_PING, b"\x22\x66\xaa\xee"), + ) + self.assertFrameSent( + client, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_server_receives_ping_with_data(self): + server = Protocol(SERVER) + server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived( + server, + Frame(OP_PING, b"\x22\x66\xaa\xee"), + ) + self.assertFrameSent( + server, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_client_sends_fragmented_ping_frame(self): + client = Protocol(CLIENT) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + client.send_frame(Frame(OP_PING, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_server_sends_fragmented_ping_frame(self): + server = Protocol(SERVER) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + server.send_frame(Frame(OP_PING, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_client_receives_fragmented_ping_frame(self): + client = Protocol(CLIENT) + client.receive_data(b"\x09\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_server_receives_fragmented_ping_frame(self): + server = Protocol(SERVER) + server.receive_data(b"\x09\x80\x3c\x3c\x3c\x3c") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_client_sends_ping_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + # The spec says: "An endpoint MAY send a Ping frame any time (...) + # before the connection is closed" but websockets doesn't support + # sending a Ping frame after a Close frame. + with self.assertRaises(InvalidState) as raised: + client.send_ping(b"") + self.assertEqual( + str(raised.exception), + "cannot write to a WebSocket in the CLOSING state", + ) + + def test_server_sends_ping_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + # The spec says: "An endpoint MAY send a Ping frame any time (...) + # before the connection is closed" but websockets doesn't support + # sending a Ping frame after a Close frame. + with self.assertRaises(InvalidState) as raised: + server.send_ping(b"") + self.assertEqual( + str(raised.exception), + "cannot write to a WebSocket in the CLOSING state", + ) + + def test_client_receives_ping_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x89\x04\x22\x66\xaa\xee") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_ping_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class PongTests(ProtocolTestCase): + """ + Test pong frames. See 5.5.3. Pong in RFC 6544. + + """ + + def test_client_sends_pong(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_pong(b"") + self.assertEqual(client.data_to_send(), [b"\x8a\x80\x00\x44\x88\xcc"]) + + def test_server_sends_pong(self): + server = Protocol(SERVER) + server.send_pong(b"") + self.assertEqual(server.data_to_send(), [b"\x8a\x00"]) + + def test_client_receives_pong(self): + client = Protocol(CLIENT) + client.receive_data(b"\x8a\x00") + self.assertFrameReceived( + client, + Frame(OP_PONG, b""), + ) + + def test_server_receives_pong(self): + server = Protocol(SERVER) + server.receive_data(b"\x8a\x80\x00\x44\x88\xcc") + self.assertFrameReceived( + server, + Frame(OP_PONG, b""), + ) + + def test_client_sends_pong_with_data(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_pong(b"\x22\x66\xaa\xee") + self.assertEqual( + client.data_to_send(), [b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22"] + ) + + def test_server_sends_pong_with_data(self): + server = Protocol(SERVER) + server.send_pong(b"\x22\x66\xaa\xee") + self.assertEqual(server.data_to_send(), [b"\x8a\x04\x22\x66\xaa\xee"]) + + def test_client_receives_pong_with_data(self): + client = Protocol(CLIENT) + client.receive_data(b"\x8a\x04\x22\x66\xaa\xee") + self.assertFrameReceived( + client, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_server_receives_pong_with_data(self): + server = Protocol(SERVER) + server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived( + server, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_client_sends_fragmented_pong_frame(self): + client = Protocol(CLIENT) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + client.send_frame(Frame(OP_PONG, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_server_sends_fragmented_pong_frame(self): + server = Protocol(SERVER) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + server.send_frame(Frame(OP_PONG, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_client_receives_fragmented_pong_frame(self): + client = Protocol(CLIENT) + client.receive_data(b"\x0a\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_server_receives_fragmented_pong_frame(self): + server = Protocol(SERVER) + server.receive_data(b"\x0a\x80\x3c\x3c\x3c\x3c") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_client_sends_pong_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + # websockets doesn't support sending a Pong frame after a Close frame. + with self.assertRaises(InvalidState): + client.send_pong(b"") + + def test_server_sends_pong_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + # websockets doesn't support sending a Pong frame after a Close frame. + with self.assertRaises(InvalidState): + server.send_pong(b"") + + def test_client_receives_pong_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x8a\x04\x22\x66\xaa\xee") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_pong_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class FailTests(ProtocolTestCase): + """ + Test failing the connection. + + See 7.1.7. Fail the WebSocket Connection in RFC 6544. + + """ + + def test_client_stops_processing_frames_after_fail(self): + client = Protocol(CLIENT) + client.fail(CloseCode.PROTOCOL_ERROR) + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR) + client.receive_data(b"\x88\x02\x03\xea") + self.assertFrameReceived(client, None) + + def test_server_stops_processing_frames_after_fail(self): + server = Protocol(SERVER) + server.fail(CloseCode.PROTOCOL_ERROR) + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xea") + self.assertFrameReceived(server, None) + + +class FragmentationTests(ProtocolTestCase): + """ + Test message fragmentation. + + See 5.4. Fragmentation in RFC 6544. + + """ + + def test_client_send_ping_pong_in_fragmented_message(self): + client = Protocol(CLIENT) + client.send_text(b"Spam", fin=False) + self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False)) + client.send_ping(b"Ping") + self.assertFrameSent(client, Frame(OP_PING, b"Ping")) + client.send_continuation(b"Ham", fin=False) + self.assertFrameSent(client, Frame(OP_CONT, b"Ham", fin=False)) + client.send_pong(b"Pong") + self.assertFrameSent(client, Frame(OP_PONG, b"Pong")) + client.send_continuation(b"Eggs", fin=True) + self.assertFrameSent(client, Frame(OP_CONT, b"Eggs")) + + def test_server_send_ping_pong_in_fragmented_message(self): + server = Protocol(SERVER) + server.send_text(b"Spam", fin=False) + self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False)) + server.send_ping(b"Ping") + self.assertFrameSent(server, Frame(OP_PING, b"Ping")) + server.send_continuation(b"Ham", fin=False) + self.assertFrameSent(server, Frame(OP_CONT, b"Ham", fin=False)) + server.send_pong(b"Pong") + self.assertFrameSent(server, Frame(OP_PONG, b"Pong")) + server.send_continuation(b"Eggs", fin=True) + self.assertFrameSent(server, Frame(OP_CONT, b"Eggs")) + + def test_client_receive_ping_pong_in_fragmented_message(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x04Spam") + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"Spam", fin=False), + ) + client.receive_data(b"\x89\x04Ping") + self.assertFrameReceived( + client, + Frame(OP_PING, b"Ping"), + ) + self.assertFrameSent( + client, + Frame(OP_PONG, b"Ping"), + ) + client.receive_data(b"\x00\x03Ham") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"Ham", fin=False), + ) + client.receive_data(b"\x8a\x04Pong") + self.assertFrameReceived( + client, + Frame(OP_PONG, b"Pong"), + ) + client.receive_data(b"\x80\x04Eggs") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"Eggs"), + ) + + def test_server_receive_ping_pong_in_fragmented_message(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam") + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"Spam", fin=False), + ) + server.receive_data(b"\x89\x84\x00\x00\x00\x00Ping") + self.assertFrameReceived( + server, + Frame(OP_PING, b"Ping"), + ) + self.assertFrameSent( + server, + Frame(OP_PONG, b"Ping"), + ) + server.receive_data(b"\x00\x83\x00\x00\x00\x00Ham") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"Ham", fin=False), + ) + server.receive_data(b"\x8a\x84\x00\x00\x00\x00Pong") + self.assertFrameReceived( + server, + Frame(OP_PONG, b"Pong"), + ) + server.receive_data(b"\x80\x84\x00\x00\x00\x00Eggs") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"Eggs"), + ) + + def test_client_send_close_in_fragmented_message(self): + client = Protocol(CLIENT) + client.send_text(b"Spam", fin=False) + self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False)) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + with self.assertRaises(ProtocolError) as raised: + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(str(raised.exception), "expected a continuation frame") + client.send_continuation(b"Eggs", fin=True) + + def test_server_send_close_in_fragmented_message(self): + server = Protocol(CLIENT) + server.send_text(b"Spam", fin=False) + self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False)) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + with self.assertRaises(ProtocolError) as raised: + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_client_receive_close_in_fragmented_message(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x04Spam") + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"Spam", fin=False), + ) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + client.receive_data(b"\x88\x02\x03\xe8") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "incomplete fragmented message") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "incomplete fragmented message" + ) + + def test_server_receive_close_in_fragmented_message(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam") + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"Spam", fin=False), + ) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "incomplete fragmented message") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "incomplete fragmented message" + ) + + +class EOFTests(ProtocolTestCase): + """ + Test half-closes on connection termination. + + """ + + def test_client_receives_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertConnectionClosing(client) + client.receive_eof() + self.assertIs(client.state, CLOSED) + + def test_server_receives_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertConnectionClosing(server) + server.receive_eof() + self.assertIs(server.state, CLOSED) + + def test_client_receives_eof_between_frames(self): + client = Protocol(CLIENT) + client.receive_eof() + self.assertIsInstance(client.parser_exc, EOFError) + self.assertEqual(str(client.parser_exc), "unexpected end of stream") + self.assertIs(client.state, CLOSED) + + def test_server_receives_eof_between_frames(self): + server = Protocol(SERVER) + server.receive_eof() + self.assertIsInstance(server.parser_exc, EOFError) + self.assertEqual(str(server.parser_exc), "unexpected end of stream") + self.assertIs(server.state, CLOSED) + + def test_client_receives_eof_inside_frame(self): + client = Protocol(CLIENT) + client.receive_data(b"\x81") + client.receive_eof() + self.assertIsInstance(client.parser_exc, EOFError) + self.assertEqual( + str(client.parser_exc), + "stream ends after 1 bytes, expected 2 bytes", + ) + self.assertIs(client.state, CLOSED) + + def test_server_receives_eof_inside_frame(self): + server = Protocol(SERVER) + server.receive_data(b"\x81") + server.receive_eof() + self.assertIsInstance(server.parser_exc, EOFError) + self.assertEqual( + str(server.parser_exc), + "stream ends after 1 bytes, expected 2 bytes", + ) + self.assertIs(server.state, CLOSED) + + def test_client_receives_data_after_exception(self): + client = Protocol(CLIENT) + client.receive_data(b"\xff\xff") + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode") + client.receive_data(b"\x00\x00") + self.assertFrameSent(client, None) + + def test_server_receives_data_after_exception(self): + server = Protocol(SERVER) + server.receive_data(b"\xff\xff") + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode") + server.receive_data(b"\x00\x00") + self.assertFrameSent(server, None) + + def test_client_receives_eof_after_exception(self): + client = Protocol(CLIENT) + client.receive_data(b"\xff\xff") + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode") + client.receive_eof() + self.assertFrameSent(client, None, eof=True) + + def test_server_receives_eof_after_exception(self): + server = Protocol(SERVER) + server.receive_data(b"\xff\xff") + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode") + server.receive_eof() + self.assertFrameSent(server, None) + + def test_client_receives_data_and_eof_after_exception(self): + client = Protocol(CLIENT) + client.receive_data(b"\xff\xff") + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode") + client.receive_data(b"\x00\x00") + client.receive_eof() + self.assertFrameSent(client, None, eof=True) + + def test_server_receives_data_and_eof_after_exception(self): + server = Protocol(SERVER) + server.receive_data(b"\xff\xff") + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode") + server.receive_data(b"\x00\x00") + server.receive_eof() + self.assertFrameSent(server, None) + + def test_client_receives_data_after_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertConnectionClosing(client) + client.receive_eof() + with self.assertRaises(EOFError) as raised: + client.receive_data(b"\x88\x00") + self.assertEqual(str(raised.exception), "stream ended") + + def test_server_receives_data_after_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertConnectionClosing(server) + server.receive_eof() + with self.assertRaises(EOFError) as raised: + server.receive_data(b"\x88\x80\x00\x00\x00\x00") + self.assertEqual(str(raised.exception), "stream ended") + + def test_client_receives_eof_after_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertConnectionClosing(client) + client.receive_eof() + with self.assertRaises(EOFError) as raised: + client.receive_eof() + self.assertEqual(str(raised.exception), "stream ended") + + def test_server_receives_eof_after_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertConnectionClosing(server) + server.receive_eof() + with self.assertRaises(EOFError) as raised: + server.receive_eof() + self.assertEqual(str(raised.exception), "stream ended") + + +class TCPCloseTests(ProtocolTestCase): + """ + Test expectation of TCP close on connection termination. + + """ + + def test_client_default(self): + client = Protocol(CLIENT) + self.assertFalse(client.close_expected()) + + def test_server_default(self): + server = Protocol(SERVER) + self.assertFalse(server.close_expected()) + + def test_client_sends_close(self): + client = Protocol(CLIENT) + client.send_close() + self.assertTrue(client.close_expected()) + + def test_server_sends_close(self): + server = Protocol(SERVER) + server.send_close() + self.assertTrue(server.close_expected()) + + def test_client_receives_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertTrue(client.close_expected()) + + def test_client_receives_close_then_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + client.receive_eof() + self.assertFalse(client.close_expected()) + + def test_server_receives_close_then_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + server.receive_eof() + self.assertFalse(server.close_expected()) + + def test_server_receives_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertTrue(server.close_expected()) + + def test_client_fails_connection(self): + client = Protocol(CLIENT) + client.fail(CloseCode.PROTOCOL_ERROR) + self.assertTrue(client.close_expected()) + + def test_server_fails_connection(self): + server = Protocol(SERVER) + server.fail(CloseCode.PROTOCOL_ERROR) + self.assertTrue(server.close_expected()) + + +class ConnectionClosedTests(ProtocolTestCase): + """ + Test connection closed exception. + + """ + + def test_client_sends_close_then_receives_close(self): + # Client-initiated close handshake on the client side complete. + client = Protocol(CLIENT) + client.send_close(CloseCode.NORMAL_CLOSURE, "") + client.receive_data(b"\x88\x02\x03\xe8") + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertFalse(exc.rcvd_then_sent) + + def test_server_sends_close_then_receives_close(self): + # Server-initiated close handshake on the server side complete. + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE, "") + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe8") + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertFalse(exc.rcvd_then_sent) + + def test_client_receives_close_then_sends_close(self): + # Server-initiated close handshake on the client side complete. + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertTrue(exc.rcvd_then_sent) + + def test_server_receives_close_then_sends_close(self): + # Client-initiated close handshake on the server side complete. + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe8") + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertTrue(exc.rcvd_then_sent) + + def test_client_sends_close_then_receives_eof(self): + # Client-initiated close handshake on the client side times out. + client = Protocol(CLIENT) + client.send_close(CloseCode.NORMAL_CLOSURE, "") + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertIsNone(exc.rcvd_then_sent) + + def test_server_sends_close_then_receives_eof(self): + # Server-initiated close handshake on the server side times out. + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE, "") + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertIsNone(exc.rcvd_then_sent) + + def test_client_receives_eof(self): + # Server-initiated close handshake on the client side times out. + client = Protocol(CLIENT) + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertIsNone(exc.sent) + self.assertIsNone(exc.rcvd_then_sent) + + def test_server_receives_eof(self): + # Client-initiated close handshake on the server side times out. + server = Protocol(SERVER) + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertIsNone(exc.sent) + self.assertIsNone(exc.rcvd_then_sent) + + +class ErrorTests(ProtocolTestCase): + """ + Test other error cases. + + """ + + def test_client_hits_internal_error_reading_frame(self): + client = Protocol(CLIENT) + # This isn't supposed to happen, so we're simulating it. + with unittest.mock.patch("struct.unpack", side_effect=RuntimeError("BOOM")): + client.receive_data(b"\x81\x00") + self.assertIsInstance(client.parser_exc, RuntimeError) + self.assertEqual(str(client.parser_exc), "BOOM") + self.assertConnectionFailing(client, CloseCode.INTERNAL_ERROR, "") + + def test_server_hits_internal_error_reading_frame(self): + server = Protocol(SERVER) + # This isn't supposed to happen, so we're simulating it. + with unittest.mock.patch("struct.unpack", side_effect=RuntimeError("BOOM")): + server.receive_data(b"\x81\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, RuntimeError) + self.assertEqual(str(server.parser_exc), "BOOM") + self.assertConnectionFailing(server, CloseCode.INTERNAL_ERROR, "") + + +class ExtensionsTests(ProtocolTestCase): + """ + Test how extensions affect frames. + + """ + + def test_client_extension_encodes_frame(self): + client = Protocol(CLIENT) + client.extensions = [Rsv2Extension()] + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_ping(b"") + self.assertEqual(client.data_to_send(), [b"\xa9\x80\x00\x44\x88\xcc"]) + + def test_server_extension_encodes_frame(self): + server = Protocol(SERVER) + server.extensions = [Rsv2Extension()] + server.send_ping(b"") + self.assertEqual(server.data_to_send(), [b"\xa9\x00"]) + + def test_client_extension_decodes_frame(self): + client = Protocol(CLIENT) + client.extensions = [Rsv2Extension()] + client.receive_data(b"\xaa\x00") + self.assertEqual(client.events_received(), [Frame(OP_PONG, b"")]) + + def test_server_extension_decodes_frame(self): + server = Protocol(SERVER) + server.extensions = [Rsv2Extension()] + server.receive_data(b"\xaa\x80\x00\x44\x88\xcc") + self.assertEqual(server.events_received(), [Frame(OP_PONG, b"")]) + + +class MiscTests(unittest.TestCase): + def test_client_default_logger(self): + client = Protocol(CLIENT) + logger = logging.getLogger("websockets.client") + self.assertIs(client.logger, logger) + + def test_server_default_logger(self): + server = Protocol(SERVER) + logger = logging.getLogger("websockets.server") + self.assertIs(server.logger, logger) + + def test_client_custom_logger(self): + logger = logging.getLogger("test") + client = Protocol(CLIENT, logger=logger) + self.assertIs(client.logger, logger) + + def test_server_custom_logger(self): + logger = logging.getLogger("test") + server = Protocol(SERVER, logger=logger) + self.assertIs(server.logger, logger) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_server.py new file mode 100644 index 0000000000..b6f5e35681 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_server.py @@ -0,0 +1,686 @@ +import http +import logging +import unittest +import unittest.mock + +from websockets.datastructures import Headers +from websockets.exceptions import ( + InvalidHeader, + InvalidOrigin, + InvalidUpgrade, + NegotiationError, +) +from websockets.frames import OP_TEXT, Frame +from websockets.http11 import Request, Response +from websockets.protocol import CONNECTING, OPEN +from websockets.server import * + +from .extensions.utils import ( + OpExtension, + Rsv2Extension, + ServerOpExtensionFactory, + ServerRsv2ExtensionFactory, +) +from .test_utils import ACCEPT, KEY +from .utils import DATE, DeprecationTestCase + + +class ConnectTests(unittest.TestCase): + def test_receive_connect(self): + server = ServerProtocol() + server.receive_data( + ( + f"GET /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {KEY}\r\n" + f"Sec-WebSocket-Version: 13\r\n" + f"\r\n" + ).encode(), + ) + [request] = server.events_received() + self.assertIsInstance(request, Request) + self.assertEqual(server.data_to_send(), []) + self.assertFalse(server.close_expected()) + + def test_connect_request(self): + server = ServerProtocol() + server.receive_data( + ( + f"GET /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {KEY}\r\n" + f"Sec-WebSocket-Version: 13\r\n" + f"\r\n" + ).encode(), + ) + [request] = server.events_received() + self.assertEqual(request.path, "/test") + self.assertEqual( + request.headers, + Headers( + { + "Host": "example.com", + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Key": KEY, + "Sec-WebSocket-Version": "13", + } + ), + ) + + def test_no_request(self): + server = ServerProtocol() + server.receive_eof() + self.assertEqual(server.events_received(), []) + + def test_partial_request(self): + server = ServerProtocol() + server.receive_data(b"GET /test HTTP/1.1\r\n") + server.receive_eof() + self.assertEqual(server.events_received(), []) + + def test_random_request(self): + server = ServerProtocol() + server.receive_data(b"HELO relay.invalid\r\n") + server.receive_data(b"MAIL FROM: <alice@invalid>\r\n") + server.receive_data(b"RCPT TO: <bob@invalid>\r\n") + self.assertEqual(server.events_received(), []) + + +class AcceptRejectTests(unittest.TestCase): + def make_request(self): + return Request( + path="/test", + headers=Headers( + { + "Host": "example.com", + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Key": KEY, + "Sec-WebSocket-Version": "13", + } + ), + ) + + def test_send_accept(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.accept(self.make_request()) + self.assertIsInstance(response, Response) + server.send_response(response) + self.assertEqual( + server.data_to_send(), + [ + f"HTTP/1.1 101 Switching Protocols\r\n" + f"Date: {DATE}\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {ACCEPT}\r\n" + f"\r\n".encode() + ], + ) + self.assertFalse(server.close_expected()) + self.assertEqual(server.state, OPEN) + + def test_send_reject(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.reject(http.HTTPStatus.NOT_FOUND, "Sorry folks.\n") + self.assertIsInstance(response, Response) + server.send_response(response) + self.assertEqual( + server.data_to_send(), + [ + f"HTTP/1.1 404 Not Found\r\n" + f"Date: {DATE}\r\n" + f"Connection: close\r\n" + f"Content-Length: 13\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"\r\n" + f"Sorry folks.\n".encode(), + b"", + ], + ) + self.assertTrue(server.close_expected()) + self.assertEqual(server.state, CONNECTING) + + def test_accept_response(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.accept(self.make_request()) + self.assertIsInstance(response, Response) + self.assertEqual(response.status_code, 101) + self.assertEqual(response.reason_phrase, "Switching Protocols") + self.assertEqual( + response.headers, + Headers( + { + "Date": DATE, + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Accept": ACCEPT, + } + ), + ) + self.assertIsNone(response.body) + + def test_reject_response(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.reject(http.HTTPStatus.NOT_FOUND, "Sorry folks.\n") + self.assertIsInstance(response, Response) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.reason_phrase, "Not Found") + self.assertEqual( + response.headers, + Headers( + { + "Date": DATE, + "Connection": "close", + "Content-Length": "13", + "Content-Type": "text/plain; charset=utf-8", + } + ), + ) + self.assertEqual(response.body, b"Sorry folks.\n") + + def test_reject_response_supports_int_status(self): + server = ServerProtocol() + response = server.reject(404, "Sorry folks.\n") + self.assertEqual(response.status_code, 404) + self.assertEqual(response.reason_phrase, "Not Found") + + def test_basic(self): + server = ServerProtocol() + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + + def test_unexpected_exception(self): + server = ServerProtocol() + request = self.make_request() + with unittest.mock.patch( + "websockets.server.ServerProtocol.process_request", + side_effect=Exception("BOOM"), + ): + response = server.accept(request) + + self.assertEqual(response.status_code, 500) + with self.assertRaises(Exception) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "BOOM") + + def test_missing_connection(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Connection"] + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Connection header") + + def test_invalid_connection(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Connection"] + request.headers["Connection"] = "close" + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "invalid Connection header: close") + + def test_missing_upgrade(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Upgrade"] + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Upgrade header") + + def test_invalid_upgrade(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Upgrade"] + request.headers["Upgrade"] = "h2c" + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c") + + def test_missing_key(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Key"] + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Key header") + + def test_multiple_key(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Key"] = KEY + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Sec-WebSocket-Key header: " + "more than one Sec-WebSocket-Key header found", + ) + + def test_invalid_key(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Key"] + request.headers["Sec-WebSocket-Key"] = "not Base64 data!" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Sec-WebSocket-Key header: not Base64 data!" + ) + + def test_truncated_key(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Key"] + request.headers["Sec-WebSocket-Key"] = KEY[ + :16 + ] # 12 bytes instead of 16, Base64-encoded + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), f"invalid Sec-WebSocket-Key header: {KEY[:16]}" + ) + + def test_missing_version(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Version"] + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Version header") + + def test_multiple_version(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Version"] = "11" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Sec-WebSocket-Version header: " + "more than one Sec-WebSocket-Version header found", + ) + + def test_invalid_version(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Version"] + request.headers["Sec-WebSocket-Version"] = "11" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Sec-WebSocket-Version header: 11" + ) + + def test_no_origin(self): + server = ServerProtocol(origins=["https://example.com"]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 403) + with self.assertRaises(InvalidOrigin) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Origin header") + + def test_origin(self): + server = ServerProtocol(origins=["https://example.com"]) + request = self.make_request() + request.headers["Origin"] = "https://example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(server.origin, "https://example.com") + + def test_unexpected_origin(self): + server = ServerProtocol(origins=["https://example.com"]) + request = self.make_request() + request.headers["Origin"] = "https://other.example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 403) + with self.assertRaises(InvalidOrigin) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Origin header: https://other.example.com" + ) + + def test_multiple_origin(self): + server = ServerProtocol( + origins=["https://example.com", "https://other.example.com"] + ) + request = self.make_request() + request.headers["Origin"] = "https://example.com" + request.headers["Origin"] = "https://other.example.com" + response = server.accept(request) + + # This is prohibited by the HTTP specification, so the return code is + # 400 Bad Request rather than 403 Forbidden. + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Origin header: more than one Origin header found", + ) + + def test_supported_origin(self): + server = ServerProtocol( + origins=["https://example.com", "https://other.example.com"] + ) + request = self.make_request() + request.headers["Origin"] = "https://other.example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(server.origin, "https://other.example.com") + + def test_unsupported_origin(self): + server = ServerProtocol( + origins=["https://example.com", "https://other.example.com"] + ) + request = self.make_request() + request.headers["Origin"] = "https://original.example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 403) + with self.assertRaises(InvalidOrigin) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Origin header: https://original.example.com" + ) + + def test_no_origin_accepted(self): + server = ServerProtocol(origins=[None]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertIsNone(server.origin) + + def test_no_extensions(self): + server = ServerProtocol() + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_no_extension(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory()]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_extension(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory()]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op") + self.assertEqual(server.extensions, [OpExtension()]) + + def test_unexpected_extension(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_unsupported_extension(self): + server = ServerProtocol(extensions=[ServerRsv2ExtensionFactory()]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_supported_extension_parameters(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory("this")]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op=this" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op=this") + self.assertEqual(server.extensions, [OpExtension("this")]) + + def test_unsupported_extension_parameters(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory("this")]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_multiple_supported_extension_parameters(self): + server = ServerProtocol( + extensions=[ + ServerOpExtensionFactory("this"), + ServerOpExtensionFactory("that"), + ] + ) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op=that") + self.assertEqual(server.extensions, [OpExtension("that")]) + + def test_multiple_extensions(self): + server = ServerProtocol( + extensions=[ServerOpExtensionFactory(), ServerRsv2ExtensionFactory()] + ) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + request.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual( + response.headers["Sec-WebSocket-Extensions"], "x-op; op, x-rsv2" + ) + self.assertEqual(server.extensions, [OpExtension(), Rsv2Extension()]) + + def test_multiple_extensions_order(self): + server = ServerProtocol( + extensions=[ServerOpExtensionFactory(), ServerRsv2ExtensionFactory()] + ) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual( + response.headers["Sec-WebSocket-Extensions"], "x-rsv2, x-op; op" + ) + self.assertEqual(server.extensions, [Rsv2Extension(), OpExtension()]) + + def test_no_subprotocols(self): + server = ServerProtocol() + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Protocol", response.headers) + self.assertIsNone(server.subprotocol) + + def test_no_subprotocol(self): + server = ServerProtocol(subprotocols=["chat"]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaisesRegex( + NegotiationError, + r"missing subprotocol", + ): + raise server.handshake_exc + + def test_subprotocol(self): + server = ServerProtocol(subprotocols=["chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat") + self.assertEqual(server.subprotocol, "chat") + + def test_unexpected_subprotocol(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Protocol", response.headers) + self.assertIsNone(server.subprotocol) + + def test_multiple_subprotocols(self): + server = ServerProtocol(subprotocols=["superchat", "chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + request.headers["Sec-WebSocket-Protocol"] = "superchat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "superchat") + self.assertEqual(server.subprotocol, "superchat") + + def test_supported_subprotocol(self): + server = ServerProtocol(subprotocols=["superchat", "chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat") + self.assertEqual(server.subprotocol, "chat") + + def test_unsupported_subprotocol(self): + server = ServerProtocol(subprotocols=["superchat", "chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "otherchat" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaisesRegex( + NegotiationError, + r"invalid subprotocol; expected one of superchat, chat", + ): + raise server.handshake_exc + + @staticmethod + def optional_chat(protocol, subprotocols): + if "chat" in subprotocols: + return "chat" + + def test_select_subprotocol(self): + server = ServerProtocol(select_subprotocol=self.optional_chat) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat") + self.assertEqual(server.subprotocol, "chat") + + def test_select_no_subprotocol(self): + server = ServerProtocol(select_subprotocol=self.optional_chat) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "otherchat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Protocol", response.headers) + self.assertIsNone(server.subprotocol) + + +class MiscTests(unittest.TestCase): + def test_bypass_handshake(self): + server = ServerProtocol(state=OPEN) + server.receive_data(b"\x81\x86\x00\x00\x00\x00Hello!") + [frame] = server.events_received() + self.assertEqual(frame, Frame(OP_TEXT, b"Hello!")) + + def test_custom_logger(self): + logger = logging.getLogger("test") + with self.assertLogs("test", logging.DEBUG) as logs: + ServerProtocol(logger=logger) + self.assertEqual(len(logs.records), 1) + + +class BackwardsCompatibilityTests(DeprecationTestCase): + def test_server_connection_class(self): + with self.assertDeprecationWarning( + "ServerConnection was renamed to ServerProtocol" + ): + from websockets.server import ServerConnection + + server = ServerConnection() + + self.assertIsInstance(server, ServerProtocol) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_streams.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_streams.py new file mode 100644 index 0000000000..fd7c66a0bd --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_streams.py @@ -0,0 +1,198 @@ +from websockets.streams import StreamReader + +from .utils import GeneratorTestCase + + +class StreamReaderTests(GeneratorTestCase): + def setUp(self): + self.reader = StreamReader() + + def test_read_line(self): + self.reader.feed_data(b"spam\neggs\n") + + gen = self.reader.read_line(32) + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"spam\n") + + gen = self.reader.read_line(32) + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"eggs\n") + + def test_read_line_need_more_data(self): + self.reader.feed_data(b"spa") + + gen = self.reader.read_line(32) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"m\neg") + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"spam\n") + + gen = self.reader.read_line(32) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"gs\n") + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"eggs\n") + + def test_read_line_not_enough_data(self): + self.reader.feed_data(b"spa") + self.reader.feed_eof() + + gen = self.reader.read_line(32) + with self.assertRaises(EOFError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "stream ends after 3 bytes, before end of line", + ) + + def test_read_line_too_long(self): + self.reader.feed_data(b"spam\neggs\n") + + gen = self.reader.read_line(2) + with self.assertRaises(RuntimeError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "read 5 bytes, expected no more than 2 bytes", + ) + + def test_read_line_too_long_need_more_data(self): + self.reader.feed_data(b"spa") + + gen = self.reader.read_line(2) + with self.assertRaises(RuntimeError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "read 3 bytes, expected no more than 2 bytes", + ) + + def test_read_exact(self): + self.reader.feed_data(b"spameggs") + + gen = self.reader.read_exact(4) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + + gen = self.reader.read_exact(4) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"eggs") + + def test_read_exact_need_more_data(self): + self.reader.feed_data(b"spa") + + gen = self.reader.read_exact(4) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"meg") + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + + gen = self.reader.read_exact(4) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"gs") + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"eggs") + + def test_read_exact_not_enough_data(self): + self.reader.feed_data(b"spa") + self.reader.feed_eof() + + gen = self.reader.read_exact(4) + with self.assertRaises(EOFError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "stream ends after 3 bytes, expected 4 bytes", + ) + + def test_read_to_eof(self): + gen = self.reader.read_to_eof(32) + + self.reader.feed_data(b"spam") + self.assertGeneratorRunning(gen) + + self.reader.feed_eof() + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + + def test_read_to_eof_at_eof(self): + self.reader.feed_eof() + + gen = self.reader.read_to_eof(32) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"") + + def test_read_to_eof_too_long(self): + gen = self.reader.read_to_eof(2) + + self.reader.feed_data(b"spam") + with self.assertRaises(RuntimeError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "read 4 bytes, expected no more than 2 bytes", + ) + + def test_at_eof_after_feed_data(self): + gen = self.reader.at_eof() + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"spam") + eof = self.assertGeneratorReturns(gen) + self.assertFalse(eof) + + def test_at_eof_after_feed_eof(self): + gen = self.reader.at_eof() + self.assertGeneratorRunning(gen) + self.reader.feed_eof() + eof = self.assertGeneratorReturns(gen) + self.assertTrue(eof) + + def test_feed_data_after_feed_data(self): + self.reader.feed_data(b"spam") + self.reader.feed_data(b"eggs") + + gen = self.reader.read_exact(8) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spameggs") + gen = self.reader.at_eof() + self.assertGeneratorRunning(gen) + + def test_feed_eof_after_feed_data(self): + self.reader.feed_data(b"spam") + self.reader.feed_eof() + + gen = self.reader.read_exact(4) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + gen = self.reader.at_eof() + eof = self.assertGeneratorReturns(gen) + self.assertTrue(eof) + + def test_feed_data_after_feed_eof(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + self.reader.feed_data(b"spam") + self.assertEqual( + str(raised.exception), + "stream ended", + ) + + def test_feed_eof_after_feed_eof(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + self.reader.feed_eof() + self.assertEqual( + str(raised.exception), + "stream ended", + ) + + def test_discard(self): + gen = self.reader.read_to_eof(32) + + self.reader.feed_data(b"spam") + self.reader.discard() + self.assertGeneratorRunning(gen) + + self.reader.feed_eof() + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"") diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_typing.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_typing.py new file mode 100644 index 0000000000..202de840f3 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_typing.py @@ -0,0 +1 @@ +from websockets.typing import * diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py new file mode 100644 index 0000000000..8acc01c187 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py @@ -0,0 +1,96 @@ +import unittest + +from websockets.exceptions import InvalidURI +from websockets.uri import * + + +VALID_URIS = [ + ( + "ws://localhost/", + WebSocketURI(False, "localhost", 80, "/", "", None, None), + ), + ( + "wss://localhost/", + WebSocketURI(True, "localhost", 443, "/", "", None, None), + ), + ( + "ws://localhost", + WebSocketURI(False, "localhost", 80, "", "", None, None), + ), + ( + "ws://localhost/path?query", + WebSocketURI(False, "localhost", 80, "/path", "query", None, None), + ), + ( + "ws://localhost/path;params", + WebSocketURI(False, "localhost", 80, "/path;params", "", None, None), + ), + ( + "WS://LOCALHOST/PATH?QUERY", + WebSocketURI(False, "localhost", 80, "/PATH", "QUERY", None, None), + ), + ( + "ws://user:pass@localhost/", + WebSocketURI(False, "localhost", 80, "/", "", "user", "pass"), + ), + ( + "ws://høst/", + WebSocketURI(False, "xn--hst-0na", 80, "/", "", None, None), + ), + ( + "ws://üser:påss@høst/πass?qùéry", + WebSocketURI( + False, + "xn--hst-0na", + 80, + "/%CF%80ass", + "q%C3%B9%C3%A9ry", + "%C3%BCser", + "p%C3%A5ss", + ), + ), +] + +INVALID_URIS = [ + "http://localhost/", + "https://localhost/", + "ws://localhost/path#fragment", + "ws://user@localhost/", + "ws:///path", +] + +RESOURCE_NAMES = [ + ("ws://localhost/", "/"), + ("ws://localhost", "/"), + ("ws://localhost/path?query", "/path?query"), + ("ws://høst/πass?qùéry", "/%CF%80ass?q%C3%B9%C3%A9ry"), +] + +USER_INFOS = [ + ("ws://localhost/", None), + ("ws://user:pass@localhost/", ("user", "pass")), + ("ws://üser:påss@høst/", ("%C3%BCser", "p%C3%A5ss")), +] + + +class URITests(unittest.TestCase): + def test_success(self): + for uri, parsed in VALID_URIS: + with self.subTest(uri=uri): + self.assertEqual(parse_uri(uri), parsed) + + def test_error(self): + for uri in INVALID_URIS: + with self.subTest(uri=uri): + with self.assertRaises(InvalidURI): + parse_uri(uri) + + def test_resource_name(self): + for uri, resource_name in RESOURCE_NAMES: + with self.subTest(uri=uri): + self.assertEqual(parse_uri(uri).resource_name, resource_name) + + def test_user_info(self): + for uri, user_info in USER_INFOS: + with self.subTest(uri=uri): + self.assertEqual(parse_uri(uri).user_info, user_info) diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py new file mode 100644 index 0000000000..678fcfe798 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py @@ -0,0 +1,103 @@ +import base64 +import itertools +import platform +import unittest + +from websockets.utils import accept_key, apply_mask as py_apply_mask, generate_key + + +# Test vector from RFC 6455 +KEY = "dGhlIHNhbXBsZSBub25jZQ==" +ACCEPT = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" + + +class UtilsTests(unittest.TestCase): + def test_generate_key(self): + key = generate_key() + self.assertEqual(len(base64.b64decode(key.encode())), 16) + + def test_accept_key(self): + self.assertEqual(accept_key(KEY), ACCEPT) + + +class ApplyMaskTests(unittest.TestCase): + @staticmethod + def apply_mask(*args, **kwargs): + return py_apply_mask(*args, **kwargs) + + apply_mask_type_combos = list(itertools.product([bytes, bytearray], repeat=2)) + + apply_mask_test_values = [ + (b"", b"1234", b""), + (b"aBcDe", b"\x00\x00\x00\x00", b"aBcDe"), + (b"abcdABCD", b"1234", b"PPPPpppp"), + (b"abcdABCD" * 10, b"1234", b"PPPPpppp" * 10), + ] + + def test_apply_mask(self): + for data_type, mask_type in self.apply_mask_type_combos: + for data_in, mask, data_out in self.apply_mask_test_values: + data_in, mask = data_type(data_in), mask_type(mask) + + with self.subTest(data_in=data_in, mask=mask): + result = self.apply_mask(data_in, mask) + self.assertEqual(result, data_out) + + def test_apply_mask_memoryview(self): + for mask_type in [bytes, bytearray]: + for data_in, mask, data_out in self.apply_mask_test_values: + data_in, mask = memoryview(data_in), mask_type(mask) + + with self.subTest(data_in=data_in, mask=mask): + result = self.apply_mask(data_in, mask) + self.assertEqual(result, data_out) + + def test_apply_mask_non_contiguous_memoryview(self): + for mask_type in [bytes, bytearray]: + for data_in, mask, data_out in self.apply_mask_test_values: + data_in, mask = memoryview(data_in)[::-1], mask_type(mask)[::-1] + data_out = data_out[::-1] + + with self.subTest(data_in=data_in, mask=mask): + result = self.apply_mask(data_in, mask) + self.assertEqual(result, data_out) + + def test_apply_mask_check_input_types(self): + for data_in, mask in [(None, None), (b"abcd", None), (None, b"abcd")]: + with self.subTest(data_in=data_in, mask=mask): + with self.assertRaises(TypeError): + self.apply_mask(data_in, mask) + + def test_apply_mask_check_mask_length(self): + for data_in, mask in [ + (b"", b""), + (b"abcd", b"123"), + (b"", b"aBcDe"), + (b"12345678", b"12345678"), + ]: + with self.subTest(data_in=data_in, mask=mask): + with self.assertRaises(ValueError): + self.apply_mask(data_in, mask) + + +try: + from websockets.speedups import apply_mask as c_apply_mask +except ImportError: + pass +else: + + class SpeedupsTests(ApplyMaskTests): + @staticmethod + def apply_mask(*args, **kwargs): + try: + return c_apply_mask(*args, **kwargs) + except NotImplementedError as exc: # pragma: no cover + # PyPy doesn't implement creating contiguous readonly buffer + # from non-contiguous. We don't care about this edge case. + if ( + platform.python_implementation() == "PyPy" + and "not implemented yet" in str(exc) + ): + raise unittest.SkipTest(str(exc)) + else: + raise diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py new file mode 100644 index 0000000000..2937a2f15e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py @@ -0,0 +1,88 @@ +import contextlib +import email.utils +import os +import pathlib +import platform +import tempfile +import time +import unittest +import warnings + + +# Generate TLS certificate with: +# $ openssl req -x509 -config test_localhost.cnf -days 15340 -newkey rsa:2048 \ +# -out test_localhost.crt -keyout test_localhost.key +# $ cat test_localhost.key test_localhost.crt > test_localhost.pem +# $ rm test_localhost.key test_localhost.crt + +CERTIFICATE = bytes(pathlib.Path(__file__).with_name("test_localhost.pem")) + + +DATE = email.utils.formatdate(usegmt=True) + + +# Unit for timeouts. May be increased on slow machines by setting the +# WEBSOCKETS_TESTS_TIMEOUT_FACTOR environment variable. +MS = 0.001 * float(os.environ.get("WEBSOCKETS_TESTS_TIMEOUT_FACTOR", "1")) + +# PyPy has a performance penalty for this test suite. +if platform.python_implementation() == "PyPy": # pragma: no cover + MS *= 5 + +# asyncio's debug mode has a 10x performance penalty for this test suite. +if os.environ.get("PYTHONASYNCIODEBUG"): # pragma: no cover + MS *= 10 + +# Ensure that timeouts are larger than the clock's resolution (for Windows). +MS = max(MS, 2.5 * time.get_clock_info("monotonic").resolution) + + +class GeneratorTestCase(unittest.TestCase): + """ + Base class for testing generator-based coroutines. + + """ + + def assertGeneratorRunning(self, gen): + """ + Check that a generator-based coroutine hasn't completed yet. + + """ + next(gen) + + def assertGeneratorReturns(self, gen): + """ + Check that a generator-based coroutine completes and return its value. + + """ + with self.assertRaises(StopIteration) as raised: + next(gen) + return raised.exception.value + + +class DeprecationTestCase(unittest.TestCase): + """ + Base class for testing deprecations. + + """ + + @contextlib.contextmanager + def assertDeprecationWarning(self, message): + """ + Check that a deprecation warning was raised with the given message. + + """ + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + yield + + self.assertEqual(len(recorded_warnings), 1) + warning = recorded_warnings[0] + self.assertEqual(warning.category, DeprecationWarning) + self.assertEqual(str(warning.message), message) + + +@contextlib.contextmanager +def temp_unix_socket_path(): + with tempfile.TemporaryDirectory() as temp_dir: + yield str(pathlib.Path(temp_dir) / "websockets") diff --git a/testing/web-platform/tests/tools/third_party/websockets/tox.ini b/testing/web-platform/tests/tools/third_party/websockets/tox.ini new file mode 100644 index 0000000000..939d8c0cd8 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist = + py37 + py38 + py39 + py310 + py311 + coverage + black + ruff + mypy + +[testenv] +commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning -m unittest {posargs} + +[testenv:coverage] +commands = + python -m coverage erase + python -m coverage run --source {envsitepackagesdir}/websockets,tests -m unittest {posargs} + python -m coverage report --show-missing --fail-under=100 +deps = coverage + +[testenv:maxi_cov] +commands = + python tests/maxi_cov.py {envsitepackagesdir} + python -m coverage report --show-missing --fail-under=100 +deps = coverage + +[testenv:black] +commands = black --check src tests +deps = black + +[testenv:ruff] +commands = ruff src tests +deps = ruff + +[testenv:mypy] +commands = mypy --strict src +deps = mypy diff --git a/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py b/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py index 040de1ae53..87fa4a638a 100644 --- a/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py +++ b/testing/web-platform/tests/tools/third_party_modified/mozlog/mozlog/formatters/html/html.py @@ -6,7 +6,7 @@ import base64 import json import os from collections import defaultdict -from datetime import datetime +from datetime import datetime, timezone from .. import base @@ -242,7 +242,7 @@ class HTMLFormatter(base.BaseFormatter): ) def generate_html(self): - generated = datetime.utcnow() + generated = datetime.now(timezone.utc) with open(os.path.join(base_path, "main.js")) as main_f: doc = html.html( self.head, diff --git a/testing/web-platform/tests/tools/tox.ini b/testing/web-platform/tests/tools/tox.ini index a4b747ac4f..61442932f3 100644 --- a/testing/web-platform/tests/tools/tox.ini +++ b/testing/web-platform/tests/tools/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,{py37,py38,py39,py310,py311}-{flake8,mypy} +envlist = py38,py39,py310,py311,py312,{py38,py39,py310,py311,py312}-{flake8,mypy} skipsdist=True skip_missing_interpreters=False diff --git a/testing/web-platform/tests/tools/wave/requirements.txt b/testing/web-platform/tests/tools/wave/requirements.txt index b2c151b8fe..3bb476fd96 100644 --- a/testing/web-platform/tests/tools/wave/requirements.txt +++ b/testing/web-platform/tests/tools/wave/requirements.txt @@ -1,2 +1,2 @@ ua-parser==0.18.0 -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 diff --git a/testing/web-platform/tests/tools/wave/tox.ini b/testing/web-platform/tests/tools/wave/tox.ini index 06bdfcd674..88c76096f4 100644 --- a/testing/web-platform/tests/tools/wave/tox.ini +++ b/testing/web-platform/tests/tools/wave/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311 +envlist = py38,py39,py310,py311,py312 skipsdist=True skip_missing_interpreters = False diff --git a/testing/web-platform/tests/tools/webtransport/h3/capsule.py b/testing/web-platform/tests/tools/webtransport/h3/capsule.py index 74ca71ade9..fc8183a65f 100644 --- a/testing/web-platform/tests/tools/webtransport/h3/capsule.py +++ b/testing/web-platform/tests/tools/webtransport/h3/capsule.py @@ -108,7 +108,7 @@ class H3CapsuleDecoder: if self._final: raise e if not self._buffer: - return 0 + return size = self._buffer.capacity - self._buffer.tell() if size >= UINT_VAR_MAX_SIZE: raise e diff --git a/testing/web-platform/tests/tools/wpt/browser.py b/testing/web-platform/tests/tools/wpt/browser.py index ea71499ec4..4c42ffa4e8 100644 --- a/testing/web-platform/tests/tools/wpt/browser.py +++ b/testing/web-platform/tests/tools/wpt/browser.py @@ -1452,7 +1452,7 @@ class ChromeAndroidBase(Browser): if browser_binary is None: browser_binary = self.find_binary(channel) chrome = Chrome(self.logger) - return chrome.install_webdriver_by_version(self.version(browser_binary), dest) + return chrome.install_webdriver_by_version(self.version(browser_binary), dest, channel) def version(self, binary=None, webdriver_binary=None): if not binary: @@ -1489,20 +1489,6 @@ class ChromeAndroid(ChromeAndroidBase): return "com.android.chrome" -# TODO(aluo): This is largely copied from the AndroidWebView implementation. -# Tests are not running for weblayer yet (crbug/1019521), this initial -# implementation will help to reproduce and debug any issues. -class AndroidWeblayer(ChromeAndroidBase): - """Weblayer-specific interface for Android.""" - - product = "android_weblayer" - # TODO(aluo): replace this with weblayer version after tests are working. - requirements = "requirements_chromium.txt" - - def find_binary(self, venv_path=None, channel=None): - return "org.chromium.weblayer.shell" - - class AndroidWebview(ChromeAndroidBase): """Webview-specific interface for Android. @@ -1656,10 +1642,10 @@ class Opera(Browser): return m.group(0) -class EdgeChromium(Browser): +class Edge(Browser): """Microsoft Edge Chromium Browser class.""" - product = "edgechromium" + product = "edge" requirements = "requirements_chromium.txt" platform = { "Linux": "linux", diff --git a/testing/web-platform/tests/tools/wpt/install.py b/testing/web-platform/tests/tools/wpt/install.py index 382c1e2eb8..1e6408b0be 100644 --- a/testing/web-platform/tests/tools/wpt/install.py +++ b/testing/web-platform/tests/tools/wpt/install.py @@ -4,14 +4,13 @@ import argparse from . import browser latest_channels = { - 'android_weblayer': 'dev', 'android_webview': 'dev', 'firefox': 'nightly', 'firefox_android': 'nightly', 'chrome': 'canary', 'chrome_android': 'dev', 'chromium': 'nightly', - 'edgechromium': 'dev', + 'edge': 'dev', 'safari': 'preview', 'servo': 'nightly', 'webkitgtk_minibrowser': 'nightly', diff --git a/testing/web-platform/tests/tools/wpt/requirements_android.txt b/testing/web-platform/tests/tools/wpt/requirements_android.txt index 17672383cb..e8205caa70 100644 --- a/testing/web-platform/tests/tools/wpt/requirements_android.txt +++ b/testing/web-platform/tests/tools/wpt/requirements_android.txt @@ -1 +1 @@ -mozrunner==8.3.0 +mozrunner==8.3.1 diff --git a/testing/web-platform/tests/tools/wpt/requirements_install.txt b/testing/web-platform/tests/tools/wpt/requirements_install.txt index 55bed99f8c..f7df06548c 100644 --- a/testing/web-platform/tests/tools/wpt/requirements_install.txt +++ b/testing/web-platform/tests/tools/wpt/requirements_install.txt @@ -1,2 +1,2 @@ mozinstall==2.1.0 -packaging==23.1 +packaging==24.0 diff --git a/testing/web-platform/tests/tools/wpt/run.py b/testing/web-platform/tests/tools/wpt/run.py index b9db082517..c84cdb442a 100644 --- a/testing/web-platform/tests/tools/wpt/run.py +++ b/testing/web-platform/tests/tools/wpt/run.py @@ -111,10 +111,9 @@ otherwise install OpenSSL and ensure that it's on your $PATH.""") def check_environ(product): - if product not in ("android_weblayer", "android_webview", "chrome", - "chrome_android", "chrome_ios", "content_shell", - "edgechromium", "firefox", "firefox_android", "ladybird", "servo", - "wktr"): + if product not in ("android_webview", "chrome", "chrome_android", "chrome_ios", + "content_shell", "edge", "firefox", "firefox_android", + "ladybird", "servo", "wktr"): config_builder = serve.build_config(os.path.join(wpt_root, "config.json")) # Override the ports to avoid looking for free ports config_builder.ssl = {"type": "none"} @@ -568,6 +567,8 @@ class ChromeAndroidBase(BrowserSetup): if kwargs["package_name"] is None: kwargs["package_name"] = self.browser.find_binary( channel=browser_channel) + if not kwargs["device_serial"]: + kwargs["device_serial"] = ["emulator-5554"] if kwargs["webdriver_binary"] is None: webdriver_binary = None if not kwargs["install_webdriver"]: @@ -615,17 +616,6 @@ class ChromeiOS(BrowserSetup): raise WptrunError("Unable to locate or install chromedriver binary") -class AndroidWeblayer(ChromeAndroidBase): - name = "android_weblayer" - browser_cls = browser.AndroidWeblayer - - def setup_kwargs(self, kwargs): - super().setup_kwargs(kwargs) - if kwargs["browser_channel"] in self.experimental_channels and kwargs["enable_experimental"] is None: - logger.info("Automatically turning on experimental features for WebLayer Dev/Canary") - kwargs["enable_experimental"] = True - - class AndroidWebview(ChromeAndroidBase): name = "android_webview" browser_cls = browser.AndroidWebview @@ -663,9 +653,9 @@ class Opera(BrowserSetup): raise WptrunError("Unable to locate or install operadriver binary") -class EdgeChromium(BrowserSetup): +class Edge(BrowserSetup): name = "MicrosoftEdge" - browser_cls = browser.EdgeChromium + browser_cls = browser.Edge experimental_channels: ClassVar[Tuple[str, ...]] = ("dev", "canary") def setup_kwargs(self, kwargs): @@ -872,7 +862,6 @@ class Epiphany(BrowserSetup): product_setup = { - "android_weblayer": AndroidWeblayer, "android_webview": AndroidWebview, "firefox": Firefox, "firefox_android": FirefoxAndroid, @@ -881,7 +870,7 @@ product_setup = { "chrome_ios": ChromeiOS, "chromium": Chromium, "content_shell": ContentShell, - "edgechromium": EdgeChromium, + "edge": Edge, "safari": Safari, "servo": Servo, "servodriver": ServoWebDriver, @@ -924,6 +913,9 @@ def setup_wptrunner(venv, **kwargs): args_general(kwargs) if kwargs["product"] not in product_setup: + if kwargs["product"] == "edgechromium": + raise WptrunError("edgechromium has been renamed to edge.") + raise WptrunError("Unsupported product %s" % kwargs["product"]) setup_cls = product_setup[kwargs["product"]](venv, kwargs["prompt"]) diff --git a/testing/web-platform/tests/tools/wpt/tests/test_browser.py b/testing/web-platform/tests/tools/wpt/tests/test_browser.py index 95094e376d..3a45dab16e 100644 --- a/testing/web-platform/tests/tools/wpt/tests/test_browser.py +++ b/testing/web-platform/tests/tools/wpt/tests/test_browser.py @@ -30,33 +30,33 @@ def test_all_browser_abc(): assert not inspect.isabstract(cls), "%s is abstract" % name -def test_edgechromium_webdriver_supports_browser(): +def test_edge_webdriver_supports_browser(): # MSEdgeDriver binary cannot be called. - edge = browser.EdgeChromium(logger) + edge = browser.Edge(logger) edge.webdriver_version = mock.MagicMock(return_value=None) assert not edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable') # Browser binary cannot be called. - edge = browser.EdgeChromium(logger) + edge = browser.Edge(logger) edge.webdriver_version = mock.MagicMock(return_value='70.0.1') edge.version = mock.MagicMock(return_value=None) assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable') # Browser version matches. - edge = browser.EdgeChromium(logger) + edge = browser.Edge(logger) # Versions should be an exact match to be compatible. edge.webdriver_version = mock.MagicMock(return_value='70.1.5') edge.version = mock.MagicMock(return_value='70.1.5') assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable') # Browser version doesn't match. - edge = browser.EdgeChromium(logger) + edge = browser.Edge(logger) edge.webdriver_version = mock.MagicMock(return_value='70.0.1') edge.version = mock.MagicMock(return_value='69.0.1') assert not edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'stable') # MSEdgeDriver version should match for MAJOR.MINOR.BUILD version. - edge = browser.EdgeChromium(logger) + edge = browser.Edge(logger) edge.webdriver_version = mock.MagicMock(return_value='70.0.1.0') edge.version = mock.MagicMock(return_value='70.0.1.1 dev') assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge', 'dev') @@ -68,8 +68,8 @@ def test_edgechromium_webdriver_supports_browser(): # logic to test there. @pytest.mark.skipif(sys.platform.startswith('win'), reason='just uses _get_fileversion on Windows') @mock.patch('tools.wpt.browser.call') -def test_edgechromium_webdriver_version(mocked_call): - edge = browser.EdgeChromium(logger) +def test_edge_webdriver_version(mocked_call): + edge = browser.Edge(logger) webdriver_binary = '/usr/bin/edgedriver' # Working cases. diff --git a/testing/web-platform/tests/tools/wpt/tox.ini b/testing/web-platform/tests/tools/wpt/tox.ini index 4068f70898..b6e3dab231 100644 --- a/testing/web-platform/tests/tools/wpt/tox.ini +++ b/testing/web-platform/tests/tools/wpt/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311 +envlist = py38,py39,py310,py311,py312 skipsdist=True skip_missing_interpreters = False diff --git a/testing/web-platform/tests/tools/wpt/utils.py b/testing/web-platform/tests/tools/wpt/utils.py index 5899dc3f3a..51bb3f55dc 100644 --- a/testing/web-platform/tests/tools/wpt/utils.py +++ b/testing/web-platform/tests/tools/wpt/utils.py @@ -46,8 +46,11 @@ def untar(fileobj, dest="."): """Extract tar archive.""" logger.debug("untar") fileobj = seekable(fileobj) + kwargs = {} + if sys.version_info.major >= 3 and sys.version_info.minor >= 12: + kwargs["filter"] = "tar" with tarfile.open(fileobj=fileobj) as tar_data: - tar_data.extractall(path=dest) + tar_data.extractall(path=dest, **kwargs) def unzip(fileobj, dest=None, limit=None): diff --git a/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst b/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst index fea676565b..76f088dd8f 100644 --- a/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst +++ b/testing/web-platform/tests/tools/wptrunner/docs/expectation.rst @@ -153,7 +153,7 @@ When used for expectation data, metadata files have the following format: :implementation-status: One of the values ``implementing``, - ``not-implementing`` or ``default``. This is used in conjunction + ``not-implementing`` or ``backlog``. This is used in conjunction with the ``--skip-implementation-status`` command line argument to ``wptrunner`` to ignore certain features where running the test is low value. diff --git a/testing/web-platform/tests/tools/wptrunner/requirements.txt b/testing/web-platform/tests/tools/wptrunner/requirements.txt index a7face3bd0..bb9b4ac77c 100644 --- a/testing/web-platform/tests/tools/wptrunner/requirements.txt +++ b/testing/web-platform/tests/tools/wptrunner/requirements.txt @@ -1,11 +1,11 @@ html5lib==1.1 -mozdebug==0.3.0 +mozdebug==0.3.1 mozinfo==1.2.3 # https://bugzilla.mozilla.org/show_bug.cgi?id=1621226 mozlog==8.0.0 mozprocess==1.3.1 -packaging==23.1 -pillow==9.5.0 +packaging==24.0 +pillow==10.3.0 requests==2.31.0 six==1.16.0 -urllib3==2.0.7 -aioquic==0.9.19 +urllib3==2.2.1 +aioquic==0.9.21 diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt b/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt index 3ba4731494..ed377b9c95 100644 --- a/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt +++ b/testing/web-platform/tests/tools/wptrunner/requirements_firefox.txt @@ -1,10 +1,10 @@ marionette_driver==3.4.0 mozcrash==2.2.0 -mozdevice==4.1.1 +mozdevice==4.1.2 mozinstall==2.1.0 mozleak==0.2 -mozprofile==2.6.1 -mozrunner==8.3.0 +mozprofile==3.0.0 +mozrunner==8.3.1 mozversion==2.4.0 -psutil==5.9.5 +psutil==5.9.8 redo==2.0.4 diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt b/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt index db0c5dd992..6c2425f337 100644 --- a/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt +++ b/testing/web-platform/tests/tools/wptrunner/requirements_opera.txt @@ -1,2 +1,2 @@ mozprocess==1.3.1 -selenium==4.18.1 +selenium==4.20.0 diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt b/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt index bcce11aed8..0704b2dbf6 100644 --- a/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt +++ b/testing/web-platform/tests/tools/wptrunner/requirements_safari.txt @@ -1 +1 @@ -psutil==5.9.5 +psutil==5.9.8 diff --git a/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt b/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt index c9e42346ce..806352e87e 100644 --- a/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt +++ b/testing/web-platform/tests/tools/wptrunner/requirements_sauce.txt @@ -1,2 +1,2 @@ -selenium==4.18.1 +selenium==4.20.0 requests==2.31.0 diff --git a/testing/web-platform/tests/tools/wptrunner/tox.ini b/testing/web-platform/tests/tools/wptrunner/tox.ini index 82d3ac6f55..c380be1252 100644 --- a/testing/web-platform/tests/tools/wptrunner/tox.ini +++ b/testing/web-platform/tests/tools/wptrunner/tox.ini @@ -2,7 +2,7 @@ xfail_strict=true [tox] -envlist = py311-{base,chrome,firefox,opera,safari,sauce,servo,webkit,webkitgtk_minibrowser,epiphany},{py37,py38,py39,py310}-base +envlist = py312-{base,chrome,firefox,opera,safari,sauce,servo,webkit,webkitgtk_minibrowser,epiphany},{py38,py39,py310,py311}-base skip_missing_interpreters = False [testenv] diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py index 81dc549d73..d54a9be943 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/__init__.py @@ -22,14 +22,13 @@ All classes and functions named in the above dict must be imported into the module global scope. """ -product_list = ["android_weblayer", - "android_webview", +product_list = ["android_webview", "chrome", "chrome_android", "chrome_ios", "chromium", "content_shell", - "edgechromium", + "edge", "firefox", "firefox_android", "safari", diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/android_weblayer.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/android_weblayer.py deleted file mode 100644 index db23b64793..0000000000 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/android_weblayer.py +++ /dev/null @@ -1,105 +0,0 @@ -# mypy: allow-untyped-defs - -from .base import NullBrowser # noqa: F401 -from .base import require_arg -from .base import get_timeout_multiplier # noqa: F401 -from .chrome import executor_kwargs as chrome_executor_kwargs -from .chrome_android import ChromeAndroidBrowserBase -from ..executors.base import WdspecExecutor # noqa: F401 -from ..executors.executorchrome import ChromeDriverPrintRefTestExecutor # noqa: F401 -from ..executors.executorwebdriver import (WebDriverCrashtestExecutor, # noqa: F401 - WebDriverTestharnessExecutor, # noqa: F401 - WebDriverRefTestExecutor) # noqa: F401 - - -__wptrunner__ = {"product": "android_weblayer", - "check_args": "check_args", - "browser": {None: "WeblayerShell", - "wdspec": "NullBrowser"}, - "executor": {"testharness": "WebDriverTestharnessExecutor", - "reftest": "WebDriverRefTestExecutor", - "print-reftest": "ChromeDriverPrintRefTestExecutor", - "wdspec": "WdspecExecutor", - "crashtest": "WebDriverCrashtestExecutor"}, - "browser_kwargs": "browser_kwargs", - "executor_kwargs": "executor_kwargs", - "env_extras": "env_extras", - "env_options": "env_options", - "timeout_multiplier": "get_timeout_multiplier"} - -_wptserve_ports = set() - - -def check_args(**kwargs): - require_arg(kwargs, "webdriver_binary") - - -def browser_kwargs(logger, test_type, run_info_data, config, **kwargs): - return {"binary": kwargs["binary"], - "adb_binary": kwargs["adb_binary"], - "device_serial": kwargs["device_serial"], - "webdriver_binary": kwargs["webdriver_binary"], - "webdriver_args": kwargs.get("webdriver_args"), - "stackwalk_binary": kwargs.get("stackwalk_binary"), - "symbols_path": kwargs.get("symbols_path")} - - -def executor_kwargs(logger, test_type, test_environment, run_info_data, - **kwargs): - # Use update() to modify the global list in place. - _wptserve_ports.update(set( - test_environment.config['ports']['http'] + test_environment.config['ports']['https'] + - test_environment.config['ports']['ws'] + test_environment.config['ports']['wss'] - )) - - executor_kwargs = chrome_executor_kwargs(logger, test_type, test_environment, run_info_data, - **kwargs) - del executor_kwargs["capabilities"]["goog:chromeOptions"]["prefs"] - capabilities = executor_kwargs["capabilities"] - # Note that for WebLayer, we launch a test shell and have the test shell use - # WebLayer. - # https://cs.chromium.org/chromium/src/weblayer/shell/android/shell_apk/ - capabilities["goog:chromeOptions"]["androidPackage"] = \ - "org.chromium.weblayer.shell" - capabilities["goog:chromeOptions"]["androidActivity"] = ".WebLayerShellActivity" - capabilities["goog:chromeOptions"]["androidKeepAppDataDir"] = \ - kwargs.get("keep_app_data_directory") - - # Workaround: driver.quit() cannot quit WeblayerShell. - executor_kwargs["pause_after_test"] = False - # Workaround: driver.close() is not supported. - executor_kwargs["restart_after_test"] = True - executor_kwargs["close_after_done"] = False - return executor_kwargs - - -def env_extras(**kwargs): - return [] - - -def env_options(): - # allow the use of host-resolver-rules in lieu of modifying /etc/hosts file - return {"server_host": "127.0.0.1"} - - -class WeblayerShell(ChromeAndroidBrowserBase): - """Chrome is backed by chromedriver, which is supplied through - ``wptrunner.webdriver.ChromeDriverServer``. - """ - - def __init__(self, logger, binary, - webdriver_binary="chromedriver", - adb_binary=None, - remote_queue=None, - device_serial=None, - webdriver_args=None, - stackwalk_binary=None, - symbols_path=None): - """Creates a new representation of Chrome. The `binary` argument gives - the browser binary to use for testing.""" - super().__init__(logger, - webdriver_binary, adb_binary, remote_queue, - device_serial, webdriver_args, stackwalk_binary, - symbols_path) - self.binary = binary - self.wptserver_ports = _wptserve_ports diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py index 8198bfe11d..c0a176743d 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py @@ -53,13 +53,13 @@ def browser_kwargs(logger, test_type, run_info_data, config, **kwargs): "webdriver_args": kwargs.get("webdriver_args")} -def executor_kwargs(logger, test_type, test_environment, run_info_data, +def executor_kwargs(logger, test_type, test_environment, run_info_data, subsuite, **kwargs): sanitizer_enabled = kwargs.get("sanitizer_enabled") if sanitizer_enabled: test_type = "crashtest" executor_kwargs = base_executor_kwargs(test_type, test_environment, run_info_data, - **kwargs) + subsuite, **kwargs) executor_kwargs["close_after_done"] = True executor_kwargs["sanitizer_enabled"] = sanitizer_enabled executor_kwargs["reuse_window"] = kwargs.get("reuse_window", False) @@ -115,6 +115,10 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data, # The GenericSensorExtraClasses flag enables the browser-side # implementation of sensors such as Ambient Light Sensor. chrome_options["args"].append("--enable-features=GenericSensorExtraClasses") + # Do not show Chrome for Testing infobar. For other Chromium build this + # flag is no-op. Required to avoid flakiness in tests, as the infobar + # changes the viewport, which can happen during the test run. + chrome_options["args"].append("--disable-infobars") # Classify `http-private`, `http-public` and https variants in the # appropriate IP address spaces. @@ -134,8 +138,14 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data, chrome_options["args"].append( "--ip-address-space-overrides=" + address_space_overrides_arg) + # Always disable antialiasing on the Ahem font. + blink_features = ['DisableAhemAntialias'] + if kwargs["enable_mojojs"]: - chrome_options["args"].append("--enable-blink-features=MojoJS,MojoJSTest") + blink_features.append('MojoJS') + blink_features.append('MojoJSTest') + + chrome_options["args"].append("--enable-blink-features=" + ','.join(blink_features)) if kwargs["enable_swiftshader"]: # https://chromium.googlesource.com/chromium/src/+/HEAD/docs/gpu/swiftshader.md @@ -149,6 +159,10 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data, if arg not in chrome_options["args"]: chrome_options["args"].append(arg) + for arg in subsuite.config.get("binary_args", []): + if arg not in chrome_options["args"]: + chrome_options["args"].append(arg) + # Pass the --headless=new flag to Chrome if WPT's own --headless flag was # set. '--headless' should always mean the new headless mode, as the old # headless mode is not used anyway. diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edgechromium.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py index 4f5bffa06c..82597c9312 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edgechromium.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py @@ -6,18 +6,18 @@ from .chrome import executor_kwargs as chrome_executor_kwargs from ..executors.executorwebdriver import WebDriverCrashtestExecutor # noqa: F401 from ..executors.base import WdspecExecutor # noqa: F401 from ..executors.executoredge import ( # noqa: F401 - EdgeChromiumDriverPrintRefTestExecutor, - EdgeChromiumDriverRefTestExecutor, - EdgeChromiumDriverTestharnessExecutor, + EdgeDriverPrintRefTestExecutor, + EdgeDriverRefTestExecutor, + EdgeDriverTestharnessExecutor, ) -__wptrunner__ = {"product": "edgechromium", +__wptrunner__ = {"product": "edge", "check_args": "check_args", - "browser": "EdgeChromiumBrowser", - "executor": {"testharness": "EdgeChromiumDriverTestharnessExecutor", - "reftest": "EdgeChromiumDriverRefTestExecutor", - "print-reftest": "EdgeChromiumDriverPrintRefTestExecutor", + "browser": "EdgeBrowser", + "executor": {"testharness": "EdgeDriverTestharnessExecutor", + "reftest": "EdgeDriverRefTestExecutor", + "print-reftest": "EdgeDriverPrintRefTestExecutor", "wdspec": "WdspecExecutor", "crashtest": "WebDriverCrashtestExecutor"}, "browser_kwargs": "browser_kwargs", @@ -58,9 +58,9 @@ def update_properties(): return (["debug", "os", "processor"], {"os": ["version"], "processor": ["bits"]}) -class EdgeChromiumBrowser(WebDriverBrowser): +class EdgeBrowser(WebDriverBrowser): """MicrosoftEdge is backed by MSEdgeDriver, which is supplied through - ``wptrunner.webdriver.EdgeChromiumDriverServer``. + ``wptrunner.webdriver.EdgeDriverServer``. """ def make_command(self): diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py index 814b8b8d75..d977930a28 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py @@ -132,6 +132,7 @@ def browser_kwargs(logger, test_type, run_info_data, config, subsuite, **kwargs) "headless": kwargs["headless"], "preload_browser": kwargs["preload_browser"] and not kwargs["pause_after_test"] and not kwargs["num_test_groups"] == 1, "specialpowers_path": kwargs["specialpowers_path"], + "allow_list_paths": kwargs["allow_list_paths"], "debug_test": kwargs["debug_test"]} if test_type == "wdspec" and kwargs["binary"]: browser_kwargs["webdriver_args"].extend(["--binary", kwargs["binary"]]) @@ -644,7 +645,8 @@ class GeckodriverOutputHandler(FirefoxOutputHandler): class ProfileCreator: def __init__(self, logger, prefs_root, config, test_type, extra_prefs, disable_fission, debug_test, browser_channel, binary, - package_name, certutil_binary, ca_certificate_path): + package_name, certutil_binary, ca_certificate_path, + allow_list_paths): self.logger = logger self.prefs_root = prefs_root self.config = config @@ -658,6 +660,7 @@ class ProfileCreator: self.package_name = package_name self.certutil_binary = certutil_binary self.ca_certificate_path = ca_certificate_path + self.allow_list_paths = allow_list_paths def create(self, **kwargs): """Create a Firefox profile and return the mozprofile Profile object pointing at that @@ -669,6 +672,7 @@ class ProfileCreator: profile = FirefoxProfile(preferences=preferences, restore=False, + allowlistpaths=self.allow_list_paths, **kwargs) self._set_required_prefs(profile) if self.ca_certificate_path is not None: @@ -795,7 +799,7 @@ class FirefoxBrowser(Browser): stackfix_dir=None, binary_args=None, timeout_multiplier=None, leak_check=False, asan=False, chaos_mode_flags=None, config=None, browser_channel="nightly", headless=None, preload_browser=False, - specialpowers_path=None, debug_test=False, **kwargs): + specialpowers_path=None, debug_test=False, allow_list_paths=None, **kwargs): Browser.__init__(self, logger) self.logger = logger @@ -826,7 +830,8 @@ class FirefoxBrowser(Browser): binary, package_name, certutil_binary, - ca_certificate_path) + ca_certificate_path, + allow_list_paths) if preload_browser: instance_manager_cls = PreloadInstanceManager @@ -899,7 +904,7 @@ class FirefoxWdSpecBrowser(WebDriverBrowser): disable_fission=False, stackfix_dir=None, leak_check=False, asan=False, chaos_mode_flags=None, config=None, browser_channel="nightly", headless=None, debug_test=False, profile_creator_cls=ProfileCreator, - **kwargs): + allow_list_paths=None, **kwargs): super().__init__(logger, binary, webdriver_binary, webdriver_args) self.binary = binary @@ -927,7 +932,8 @@ class FirefoxWdSpecBrowser(WebDriverBrowser): binary, package_name, certutil_binary, - ca_certificate_path) + ca_certificate_path, + allow_list_paths) self.profile = profile_creator.create() self.marionette_port = None diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py index 7c158902e1..526f83d595 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py @@ -148,11 +148,13 @@ def get_environ(chaos_mode_flags, env_extras=None): class ProfileCreator(FirefoxProfileCreator): def __init__(self, logger, prefs_root, config, test_type, extra_prefs, disable_fission, debug_test, browser_channel, binary, - package_name, certutil_binary, ca_certificate_path): + package_name, certutil_binary, ca_certificate_path, + allow_list_paths=None): super().__init__(logger, prefs_root, config, test_type, extra_prefs, disable_fission, debug_test, browser_channel, None, - package_name, certutil_binary, ca_certificate_path) + package_name, certutil_binary, ca_certificate_path, + allow_list_paths) def _set_required_prefs(self, profile): profile.set_preferences({ diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py index cb9c1a1508..6e0c081b48 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/actions.py @@ -443,6 +443,26 @@ class GetVirtualSensorInformationAction: self.logger.debug("Requesting information from %s sensor" % sensor_type) return self.protocol.virtual_sensor.get_virtual_sensor_information(sensor_type) +class SetDevicePostureAction: + name = "set_device_posture" + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + def __call__(self, payload): + posture = payload["posture"] + return self.protocol.device_posture.set_device_posture(posture) + +class ClearDevicePostureAction: + name = "clear_device_posture" + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + def __call__(self, payload): + return self.protocol.device_posture.clear_device_posture() actions = [ClickAction, DeleteAllCookiesAction, @@ -477,4 +497,6 @@ actions = [ClickAction, CreateVirtualSensorAction, UpdateVirtualSensorAction, RemoveVirtualSensorAction, - GetVirtualSensorInformationAction] + GetVirtualSensorInformationAction, + SetDevicePostureAction, + ClearDevicePostureAction] diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py index 763b6fcb19..92a782e835 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py @@ -313,7 +313,7 @@ class TestExecutor: result = self.do_test(test) except Exception as e: exception_string = traceback.format_exc() - message = f"Exception in TextExecutor.run:\n{exception_string}" + message = f"Exception in TestExecutor.run:\n{exception_string}" self.logger.warning(message) result = self.result_from_exception(test, e, exception_string) diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py index cbe5eadf9a..3b62cb7477 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executoredge.py @@ -20,42 +20,42 @@ here = os.path.dirname(__file__) _SanitizerMixin = make_sanitizer_mixin(WebDriverCrashtestExecutor)
-class EdgeChromiumDriverTestharnessProtocolPart(ChromeDriverTestharnessProtocolPart):
+class EdgeDriverTestharnessProtocolPart(ChromeDriverTestharnessProtocolPart):
def setup(self):
super().setup()
self.cdp_company_prefix = "ms"
-class EdgeChromiumDriverPrintProtocolPart(ChromeDriverPrintProtocolPart):
+class EdgeDriverPrintProtocolPart(ChromeDriverPrintProtocolPart):
def setup(self):
super().setup()
self.cdp_company_prefix = "ms"
-class EdgeChromiumDriverProtocol(WebDriverProtocol):
+class EdgeDriverProtocol(WebDriverProtocol):
implements = [
- EdgeChromiumDriverPrintProtocolPart,
- EdgeChromiumDriverTestharnessProtocolPart,
+ EdgeDriverPrintProtocolPart,
+ EdgeDriverTestharnessProtocolPart,
*(part for part in WebDriverProtocol.implements
- if part.name != EdgeChromiumDriverTestharnessProtocolPart.name)
+ if part.name != EdgeDriverTestharnessProtocolPart.name)
]
reuse_window = False
-class EdgeChromiumDriverRefTestExecutor(WebDriverRefTestExecutor, _SanitizerMixin): # type: ignore
- protocol_cls = EdgeChromiumDriverProtocol
+class EdgeDriverRefTestExecutor(WebDriverRefTestExecutor, _SanitizerMixin): # type: ignore
+ protocol_cls = EdgeDriverProtocol
-class EdgeChromiumDriverTestharnessExecutor(WebDriverTestharnessExecutor, _SanitizerMixin): # type: ignore
- protocol_cls = EdgeChromiumDriverProtocol
+class EdgeDriverTestharnessExecutor(WebDriverTestharnessExecutor, _SanitizerMixin): # type: ignore
+ protocol_cls = EdgeDriverProtocol
def __init__(self, *args, reuse_window=False, **kwargs):
super().__init__(*args, **kwargs)
self.protocol.reuse_window = reuse_window
-class EdgeChromiumDriverPrintRefTestExecutor(EdgeChromiumDriverRefTestExecutor):
- protocol_cls = EdgeChromiumDriverProtocol
+class EdgeDriverPrintRefTestExecutor(EdgeDriverRefTestExecutor):
+ protocol_cls = EdgeDriverProtocol
def setup(self, runner):
super().setup(runner)
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py index 0f640d7741..05a9fc1ae4 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py @@ -45,6 +45,7 @@ from .protocol import (AccessibilityProtocolPart, PrintProtocolPart, DebugProtocolPart, VirtualSensorProtocolPart, + DevicePostureProtocolPart, merge_dicts) @@ -749,6 +750,17 @@ class MarionetteVirtualSensorProtocolPart(VirtualSensorProtocolPart): raise NotImplementedError("get_virtual_sensor_information not yet implemented") +class MarionetteDevicePostureProtocolPart(DevicePostureProtocolPart): + def setup(self): + self.marionette = self.parent.marionette + + def set_device_posture(self, posture): + raise NotImplementedError("set_device_posture not yet implemented") + + def clear_device_posture(self): + raise NotImplementedError("clear_device_posture not yet implemented") + + class MarionetteProtocol(Protocol): implements = [MarionetteBaseProtocolPart, MarionetteTestharnessProtocolPart, @@ -769,7 +781,8 @@ class MarionetteProtocol(Protocol): MarionettePrintProtocolPart, MarionetteDebugProtocolPart, MarionetteAccessibilityProtocolPart, - MarionetteVirtualSensorProtocolPart] + MarionetteVirtualSensorProtocolPart, + MarionetteDevicePostureProtocolPart] def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False): do_delayed_imports() diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py index 6df2d96461..69013e5e79 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -35,6 +35,7 @@ from .protocol import (BaseProtocolPart, RPHRegistrationsProtocolPart, FedCMProtocolPart, VirtualSensorProtocolPart, + DevicePostureProtocolPart, merge_dicts) from webdriver.client import Session @@ -431,6 +432,16 @@ class WebDriverVirtualSensorPart(VirtualSensorProtocolPart): def get_virtual_sensor_information(self, sensor_type): return self.webdriver.send_session_command("GET", "sensor/%s" % sensor_type) +class WebDriverDevicePostureProtocolPart(DevicePostureProtocolPart): + def setup(self): + self.webdriver = self.parent.webdriver + + def set_device_posture(self, posture): + body = {"posture": posture} + return self.webdriver.send_session_command("POST", "deviceposture", body) + + def clear_device_posture(self): + return self.webdriver.send_session_command("DELETE", "deviceposture") class WebDriverProtocol(Protocol): implements = [WebDriverBaseProtocolPart, @@ -450,7 +461,8 @@ class WebDriverProtocol(Protocol): WebDriverRPHRegistrationsProtocolPart, WebDriverFedCMProtocolPart, WebDriverDebugProtocolPart, - WebDriverVirtualSensorPart] + WebDriverVirtualSensorPart, + WebDriverDevicePostureProtocolPart] def __init__(self, executor, browser, capabilities, **kwargs): super().__init__(executor, browser) @@ -527,7 +539,9 @@ class WebDriverRun(TimedRunner): self.result = True, self.func(self.protocol, self.url, self.timeout) except (error.TimeoutException, error.ScriptTimeoutException): self.result = False, ("EXTERNAL-TIMEOUT", None) - except (socket.timeout, error.UnknownErrorException): + except socket.timeout: + # Checking if the browser is alive below is likely to hang, so mark + # this case as a CRASH unconditionally. self.result = False, ("CRASH", None) except Exception as e: if (isinstance(e, error.WebDriverException) and @@ -536,11 +550,12 @@ class WebDriverRun(TimedRunner): # workaround for https://bugs.chromium.org/p/chromedriver/issues/detail?id=2001 self.result = False, ("EXTERNAL-TIMEOUT", None) else: + status = "INTERNAL-ERROR" if self.protocol.is_alive() else "CRASH" message = str(getattr(e, "message", "")) if message: message += "\n" message += traceback.format_exc() - self.result = False, ("INTERNAL-ERROR", message) + self.result = False, (status, message) finally: self.result_flag.set() diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py index e44d1a7666..3d588738b6 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/protocol.py @@ -802,3 +802,17 @@ class VirtualSensorProtocolPart(ProtocolPart): @abstractmethod def get_virtual_sensor_information(self, sensor_type): pass + +class DevicePostureProtocolPart(ProtocolPart): + """Protocol part for Device Posture""" + __metaclass__ = ABCMeta + + name = "device_posture" + + @abstractmethod + def set_device_posture(self, posture): + pass + + @abstractmethod + def clear_device_posture(self): + pass diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js b/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js index af25bf4111..87d3826bfc 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js @@ -327,4 +327,12 @@ window.test_driver_internal.get_virtual_sensor_information = function(sensor_type, context=null) { return create_action("get_virtual_sensor_information", {sensor_type, context}); }; + + window.test_driver_internal.set_device_posture = function(posture, context=null) { + return create_action("set_device_posture", {posture, context}); + }; + + window.test_driver_internal.clear_device_posture = function(context=null) { + return create_action("clear_device_posture", {context}); + }; })(); diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py index 70da22f5b7..93e19fa47b 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/testrunner.py @@ -1,7 +1,6 @@ # mypy: allow-untyped-defs import threading -import time import traceback from queue import Empty from collections import namedtuple, defaultdict @@ -31,6 +30,30 @@ TestImplementation = namedtuple('TestImplementation', 'browser_cls', 'browser_kwargs']) +class StopFlag: + """Synchronization for coordinating a graceful exit.""" + + def __init__(self, size: int): + # Flag that is polled by threads so that they can gracefully exit in the + # face of SIGINT. + self._should_stop = threading.Event() + # A barrier that each `TestRunnerManager` thread waits on when exiting + # its run loop. This provides a reliable way for the `ManagerGroup` to + # tell when all threads have cleaned up their resources. + # + # The barrier's extra waiter is the main thread (`ManagerGroup`). + self._all_managers_done = threading.Barrier(1 + size) + + def stop(self) -> None: + self._should_stop.set() + + def should_stop(self) -> bool: + return self._should_stop.is_set() + + def wait_for_all_managers_done(self, timeout: Optional[float] = None) -> None: + self._all_managers_done.wait(timeout) + + class LogMessageHandler: def __init__(self, send_message): self.send_message = send_message @@ -443,7 +466,8 @@ class TestRunnerManager(threading.Thread): if self.browser is not None: assert self.browser.browser is not None self.browser.browser.cleanup() - self.logger.debug("TestRunnerManager main loop terminated") + self.logger.debug("TestRunnerManager main loop terminated") + self.parent_stop_flag.wait_for_all_managers_done() def wait_event(self): dispatch = { @@ -517,7 +541,7 @@ class TestRunnerManager(threading.Thread): return f(*data) def should_stop(self): - return self.child_stop_flag.is_set() or self.parent_stop_flag.is_set() + return self.child_stop_flag.is_set() or self.parent_stop_flag.should_stop() def start_init(self): subsuite, test_type, test, test_group, group_metadata = self.get_next_test() @@ -977,9 +1001,7 @@ class ManagerGroup: self.max_restarts = max_restarts self.pool = set() - # Event that is polled by threads so that they can gracefully exit in the face - # of sigint - self.stop_flag = threading.Event() + self.stop_flag = None self.logger = structuredlog.StructuredLogger(suite_name) def __enter__(self): @@ -992,6 +1014,7 @@ class ManagerGroup: """Start all managers in the group""" test_queue, size = self.test_queue_builder.make_queue(tests) self.logger.info("Using %i child processes" % size) + self.stop_flag = StopFlag(size) for idx in range(size): manager = TestRunnerManager(self.suite_name, @@ -1020,18 +1043,31 @@ class ManagerGroup: timeout: Overall timeout (in seconds) for all threads to join. The default value indicates an indefinite timeout. """ - deadline = None if timeout is None else time.time() + timeout - for manager in self.pool: - manager_timeout = None - if deadline is not None: - manager_timeout = max(0, deadline - time.time()) - manager.join(manager_timeout) + # Here, the main thread cannot simply `join()` the threads in + # `self.pool` sequentially because a keyboard interrupt raised during a + # `Thread.join()` may incorrectly mark that thread as "stopped" when it + # is not [0, 1]. Subsequent `join()`s for the affected thread won't + # block anymore, so a subsequent `ManagerGroup.wait()` may return with + # that thread still alive. + # + # To the extent the timeout allows, it's important that + # `ManagerGroup.wait()` returns with all `TestRunnerManager` threads + # actually stopped. Otherwise, a live thread may log after `mozlog` + # shutdown (not allowed) or worse, leak browser processes that the + # thread should have stopped when exiting its run loop [2]. + # + # [0]: https://github.com/python/cpython/issues/90882 + # [1]: https://github.com/python/cpython/blob/558b517b/Lib/threading.py#L1146-L1178 + # [2]: https://crbug.com/330236796 + assert self.stop_flag, "ManagerGroup hasn't been started yet" + self.stop_flag.wait_for_all_managers_done(timeout) def stop(self): """Set the stop flag so that all managers in the group stop as soon as possible""" - self.stop_flag.set() - self.logger.debug("Stop flag set in ManagerGroup") + if self.stop_flag: + self.stop_flag.stop() + self.logger.debug("Stop flag set in ManagerGroup") def test_count(self): return sum(manager.test_count for manager in self.pool) diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py index d65369b380..d9d85de6a4 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py @@ -396,11 +396,16 @@ def run_tests(config, product, test_paths, **kwargs): product.check_args(**kwargs) + kwargs["allow_list_paths"] = [] if kwargs["install_fonts"]: + # Add test font to allow list for sandbox to ensure that the content + # processes will have read access. + ahem_path = os.path.join(test_paths["/"].tests_path, "fonts/Ahem.ttf") + kwargs["allow_list_paths"].append(ahem_path) env_extras.append(FontInstaller( logger, font_dir=kwargs["font_dir"], - ahem=os.path.join(test_paths["/"].tests_path, "fonts/Ahem.ttf") + ahem=ahem_path )) recording.set(["startup", "load_tests"]) diff --git a/testing/web-platform/tests/tools/wptserve/setup.py b/testing/web-platform/tests/tools/wptserve/setup.py index 1496fa7e17..36aa98f1d8 100644 --- a/testing/web-platform/tests/tools/wptserve/setup.py +++ b/testing/web-platform/tests/tools/wptserve/setup.py @@ -1,7 +1,10 @@ from setuptools import setup -PACKAGE_VERSION = '4.0' -deps = ["h2>=4.1.0"] +PACKAGE_VERSION = '4.0.2' +deps = [ + "h2>=4.1.0", + "pywebsocket3>=4.0.2", +] setup(name='wptserve', version=PACKAGE_VERSION, diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/response.py b/testing/web-platform/tests/tools/wptserve/wptserve/response.py index a6ece62dab..8d0e01bbdd 100644 --- a/testing/web-platform/tests/tools/wptserve/wptserve/response.py +++ b/testing/web-platform/tests/tools/wptserve/wptserve/response.py @@ -4,7 +4,7 @@ import json import uuid import traceback from collections import OrderedDict -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from io import BytesIO from hpack.struct import HeaderTuple @@ -135,7 +135,7 @@ class Response: "oct", "nov", "dec"])} if isinstance(expires, timedelta): - expires = datetime.utcnow() + expires + expires = datetime.now(timezone.utc) + expires if expires is not None: expires_str = expires.strftime("%d %%s %Y %H:%M:%S GMT") diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/server.py b/testing/web-platform/tests/tools/wptserve/wptserve/server.py index 8ce36201ee..1863861542 100644 --- a/testing/web-platform/tests/tools/wptserve/wptserve/server.py +++ b/testing/web-platform/tests/tools/wptserve/wptserve/server.py @@ -23,8 +23,8 @@ from h2.utilities import extract_method_header from urllib.parse import urlsplit, urlunsplit -from mod_pywebsocket import dispatch -from mod_pywebsocket.handshake import HandshakeException, AbortedByUserException +from pywebsocket3 import dispatch +from pywebsocket3.handshake import HandshakeException, AbortedByUserException from . import routes as default_routes from .config import ConfigBuilder diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py b/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py index 5a16097e37..25f86e019e 100644 --- a/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py +++ b/testing/web-platform/tests/tools/wptserve/wptserve/sslutils/openssl.py @@ -6,7 +6,7 @@ import random import shutil import subprocess import tempfile -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone # Amount of time beyond the present to consider certificates "expired." This # allows certificates to be proactively re-generated in the "buffer" period @@ -316,7 +316,7 @@ class OpenSSLEnvironment: # Because `strptime` does not account for time zone offsets, it is # always in terms of UTC, so the current time should be calculated # accordingly. - if end_date < datetime.utcnow() + time_buffer: + if end_date < datetime.now(timezone.utc) + time_buffer: return False #TODO: check the key actually signed the cert. diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py b/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py index af668dd558..ab1ab958a0 100644 --- a/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py +++ b/testing/web-platform/tests/tools/wptserve/wptserve/ws_h2_handshake.py @@ -7,12 +7,12 @@ Specification: https://tools.ietf.org/html/rfc8441 """ -from mod_pywebsocket import common +from pywebsocket3 import common -from mod_pywebsocket.handshake.base import get_mandatory_header -from mod_pywebsocket.handshake.base import HandshakeException -from mod_pywebsocket.handshake.base import validate_mandatory_header -from mod_pywebsocket.handshake.base import HandshakerBase +from pywebsocket3.handshake.base import get_mandatory_header +from pywebsocket3.handshake.base import HandshakeException +from pywebsocket3.handshake.base import validate_mandatory_header +from pywebsocket3.handshake.base import HandshakerBase def check_connect_method(request): |