├── .codecov.yml ├── .github └── workflows │ ├── ci.yml │ └── install_h2spec.sh ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── .gitkeep │ ├── h2.connection.H2ConnectionStateMachine.dot.png │ └── h2.stream.H2StreamStateMachine.dot.png │ ├── _templates │ └── .gitkeep │ ├── advanced-usage.rst │ ├── api.rst │ ├── asyncio-example.rst │ ├── basic-usage.rst │ ├── conf.py │ ├── curio-example.rst │ ├── eventlet-example.rst │ ├── examples.rst │ ├── gevent-example.rst │ ├── index.rst │ ├── installation.rst │ ├── low-level.rst │ ├── negotiating-http2.rst │ ├── plain-sockets-example.rst │ ├── release-notes.rst │ ├── release-process.rst │ ├── testimonials.rst │ ├── tornado-example.rst │ ├── twisted-example.rst │ ├── twisted-head-example.rst │ ├── twisted-post-example.rst │ └── wsgi-example.rst ├── examples ├── asyncio │ ├── asyncio-server.py │ ├── cert.crt │ ├── cert.key │ └── wsgi-server.py ├── curio │ ├── curio-server.py │ ├── localhost.crt.pem │ └── localhost.key ├── eventlet │ ├── eventlet-server.py │ ├── server.crt │ └── server.key ├── fragments │ ├── client_https_setup_fragment.py │ ├── client_upgrade_fragment.py │ ├── server_https_setup_fragment.py │ └── server_upgrade_fragment.py ├── gevent │ ├── gevent-server.py │ ├── localhost.crt │ └── localhost.key ├── plain_sockets │ └── plain_sockets_client.py ├── tornado │ ├── server.crt │ ├── server.key │ └── tornado-server.py └── twisted │ ├── head_request.py │ ├── post_request.py │ ├── server.crt │ ├── server.csr │ ├── server.key │ └── twisted-server.py ├── pyproject.toml ├── src └── h2 │ ├── __init__.py │ ├── config.py │ ├── connection.py │ ├── errors.py │ ├── events.py │ ├── exceptions.py │ ├── frame_buffer.py │ ├── py.typed │ ├── settings.py │ ├── stream.py │ ├── utilities.py │ └── windows.py ├── tests ├── __init__.py ├── conftest.py ├── coroutine_tests.py ├── h2spectest.sh ├── helpers.py ├── test_basic_logic.py ├── test_closed_streams.py ├── test_complex_logic.py ├── test_config.py ├── test_events.py ├── test_exceptions.py ├── test_flow_control_window.py ├── test_h2_upgrade.py ├── test_head_request.py ├── test_header_indexing.py ├── test_informational_responses.py ├── test_interacting_stacks.py ├── test_invalid_content_lengths.py ├── test_invalid_frame_sequences.py ├── test_invalid_headers.py ├── test_priority.py ├── test_related_events.py ├── test_rfc7838.py ├── test_rfc8441.py ├── test_settings.py ├── test_state_machines.py ├── test_stream_reset.py └── test_utility_functions.py └── visualizer ├── NOTICES.visualizer └── visualize.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | tox: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | max-parallel: 5 14 | matrix: 15 | python-version: 16 | - "3.9" 17 | - "3.10" 18 | - "3.11" 19 | - "3.12" 20 | - "3.13" 21 | - "pypy3.9" 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install tox 29 | run: | 30 | python -m pip install --upgrade pip setuptools 31 | pip install --upgrade tox tox-gh-actions 32 | - name: Install h2spec 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: | 36 | .github/workflows/install_h2spec.sh 37 | - name: Initialize tox envs 38 | run: | 39 | tox --parallel auto --notest 40 | - name: Test with tox 41 | run: | 42 | tox --parallel 0 43 | - uses: codecov/codecov-action@v5 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | files: ./coverage.xml 47 | disable_search: true 48 | -------------------------------------------------------------------------------- /.github/workflows/install_h2spec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # For some reason it helps to have this here. 6 | echo $(curl -s --url https://api.github.com/repos/summerwind/h2spec/releases/latest --header "authorization: Bearer ${GITHUB_TOKEN}") 7 | 8 | # We want to get the latest release of h2spec. We do that by asking the 9 | # Github API for it, and then parsing the JSON for the appropriate kind of 10 | # binary. Happily, the binary is always called "h2spec" so we don't need 11 | # even more shenanigans to get this to work. 12 | TARBALL=$(curl -s --url https://api.github.com/repos/summerwind/h2spec/releases/latest --header "authorization: Bearer ${GITHUB_TOKEN}" | jq --raw-output '.assets[] | .browser_download_url | select(endswith("linux_amd64.tar.gz"))') 13 | 14 | curl -s -L "$TARBALL" -o h2spec.tgz 15 | tar xvf h2spec.tgz 16 | mkdir bin 17 | mv h2spec ./bin/ 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | env/ 3 | dist/ 4 | *.egg-info/ 5 | *.pyc 6 | __pycache__ 7 | .coverage 8 | coverage.xml 9 | .tox/ 10 | .hypothesis/ 11 | .cache/ 12 | _trial_temp/ 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | version: 2 3 | 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.13" 8 | 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2020 Cory Benfield and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src/h2 2 | graft docs 3 | graft tests 4 | graft visualizer 5 | graft examples 6 | 7 | prune docs/build 8 | 9 | recursive-include examples *.py *.crt *.key *.pem *.csr 10 | 11 | include README.rst LICENSE CHANGELOG.rst pyproject.toml 12 | 13 | global-exclude *.pyc *.pyo *.swo *.swp *.map *.yml *.DS_Store 14 | exclude .readthedocs.yaml 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | h2: HTTP/2 Protocol Stack 3 | ========================= 4 | 5 | .. image:: https://github.com/python-hyper/h2/workflows/CI/badge.svg 6 | :target: https://github.com/python-hyper/h2/actions 7 | :alt: Build Status 8 | .. image:: https://codecov.io/gh/python-hyper/h2/branch/master/graph/badge.svg 9 | :target: https://codecov.io/gh/python-hyper/h2 10 | :alt: Code Coverage 11 | .. image:: https://readthedocs.org/projects/h2/badge/?version=latest 12 | :target: https://h2.readthedocs.io/en/latest/ 13 | :alt: Documentation Status 14 | .. image:: https://img.shields.io/badge/chat-join_now-brightgreen.svg 15 | :target: https://gitter.im/python-hyper/community 16 | :alt: Chat community 17 | 18 | .. image:: https://raw.github.com/python-hyper/documentation/master/source/logo/hyper-black-bg-white.png 19 | 20 | This repository contains a pure-Python implementation of a HTTP/2 protocol 21 | stack. It's written from the ground up to be embeddable in whatever program you 22 | choose to use, ensuring that you can speak HTTP/2 regardless of your 23 | programming paradigm. 24 | 25 | You use it like this: 26 | 27 | .. code-block:: python 28 | 29 | import h2.connection 30 | import h2.config 31 | 32 | config = h2.config.H2Configuration() 33 | conn = h2.connection.H2Connection(config=config) 34 | conn.send_headers(stream_id=stream_id, headers=headers) 35 | conn.send_data(stream_id, data) 36 | socket.sendall(conn.data_to_send()) 37 | events = conn.receive_data(socket_data) 38 | 39 | This repository does not provide a parsing layer, a network layer, or any rules 40 | about concurrency. Instead, it's a purely in-memory solution, defined in terms 41 | of data actions and HTTP/2 frames. This is one building block of a full Python 42 | HTTP implementation. 43 | 44 | To install it, just run: 45 | 46 | .. code-block:: console 47 | 48 | $ python -m pip install h2 49 | 50 | Documentation 51 | ============= 52 | 53 | Documentation is available at https://h2.readthedocs.io . 54 | 55 | Contributing 56 | ============ 57 | 58 | ``h2`` welcomes contributions from anyone! Unlike many other projects we 59 | are happy to accept cosmetic contributions and small contributions, in addition 60 | to large feature requests and changes. 61 | 62 | Before you contribute (either by opening an issue or filing a pull request), 63 | please `read the contribution guidelines`_. 64 | 65 | .. _read the contribution guidelines: https://python-hyper.org/en/latest/contributing.html 66 | 67 | License 68 | ======= 69 | 70 | ``h2`` is made available under the MIT License. For more details, see the 71 | ``LICENSE`` file in the repository. 72 | 73 | Authors 74 | ======= 75 | 76 | ``h2`` was authored by Cory Benfield and is maintained 77 | by the members of `python-hyper `_. 78 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-hyper/h2/0583911b29d05764bbe3f7691d59f4d5e83e249b/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/_static/h2.connection.H2ConnectionStateMachine.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-hyper/h2/0583911b29d05764bbe3f7691d59f4d5e83e249b/docs/source/_static/h2.connection.H2ConnectionStateMachine.dot.png -------------------------------------------------------------------------------- /docs/source/_static/h2.stream.H2StreamStateMachine.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-hyper/h2/0583911b29d05764bbe3f7691d59f4d5e83e249b/docs/source/_static/h2.stream.H2StreamStateMachine.dot.png -------------------------------------------------------------------------------- /docs/source/_templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-hyper/h2/0583911b29d05764bbe3f7691d59f4d5e83e249b/docs/source/_templates/.gitkeep -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | h2 API 2 | ====== 3 | 4 | This document details the API of h2. 5 | 6 | Semantic Versioning 7 | ------------------- 8 | 9 | h2 follows semantic versioning for its public API. Please note that the 10 | guarantees of semantic versioning apply only to the API that is *documented 11 | here*. Simply because a method or data field is not prefaced by an underscore 12 | does not make it part of h2's public API. Anything not documented here is 13 | subject to change at any time. 14 | 15 | Connection 16 | ---------- 17 | 18 | .. autoclass:: h2.connection.H2Connection 19 | :members: 20 | :exclude-members: inbound_flow_control_window 21 | 22 | 23 | Configuration 24 | ------------- 25 | 26 | .. autoclass:: h2.config.H2Configuration 27 | :members: 28 | 29 | 30 | .. _h2-events-api: 31 | 32 | Events 33 | ------ 34 | 35 | .. autoclass:: h2.events.RequestReceived 36 | :members: 37 | 38 | .. autoclass:: h2.events.ResponseReceived 39 | :members: 40 | 41 | .. autoclass:: h2.events.TrailersReceived 42 | :members: 43 | 44 | .. autoclass:: h2.events.InformationalResponseReceived 45 | :members: 46 | 47 | .. autoclass:: h2.events.DataReceived 48 | :members: 49 | 50 | .. autoclass:: h2.events.WindowUpdated 51 | :members: 52 | 53 | .. autoclass:: h2.events.RemoteSettingsChanged 54 | :members: 55 | 56 | .. autoclass:: h2.events.PingReceived 57 | :members: 58 | 59 | .. autoclass:: h2.events.PingAckReceived 60 | :members: 61 | 62 | .. autoclass:: h2.events.StreamEnded 63 | :members: 64 | 65 | .. autoclass:: h2.events.StreamReset 66 | :members: 67 | 68 | .. autoclass:: h2.events.PushedStreamReceived 69 | :members: 70 | 71 | .. autoclass:: h2.events.SettingsAcknowledged 72 | :members: 73 | 74 | .. autoclass:: h2.events.PriorityUpdated 75 | :members: 76 | 77 | .. autoclass:: h2.events.ConnectionTerminated 78 | :members: 79 | 80 | .. autoclass:: h2.events.AlternativeServiceAvailable 81 | :members: 82 | 83 | .. autoclass:: h2.events.UnknownFrameReceived 84 | :members: 85 | 86 | 87 | Exceptions 88 | ---------- 89 | 90 | .. autoclass:: h2.exceptions.H2Error 91 | :members: 92 | 93 | .. autoclass:: h2.exceptions.NoSuchStreamError 94 | :show-inheritance: 95 | :members: 96 | 97 | .. autoclass:: h2.exceptions.StreamClosedError 98 | :show-inheritance: 99 | :members: 100 | 101 | .. autoclass:: h2.exceptions.RFC1122Error 102 | :show-inheritance: 103 | :members: 104 | 105 | 106 | Protocol Errors 107 | ~~~~~~~~~~~~~~~ 108 | 109 | .. autoclass:: h2.exceptions.ProtocolError 110 | :show-inheritance: 111 | :members: 112 | 113 | .. autoclass:: h2.exceptions.FrameTooLargeError 114 | :show-inheritance: 115 | :members: 116 | 117 | .. autoclass:: h2.exceptions.FrameDataMissingError 118 | :show-inheritance: 119 | :members: 120 | 121 | .. autoclass:: h2.exceptions.TooManyStreamsError 122 | :show-inheritance: 123 | :members: 124 | 125 | .. autoclass:: h2.exceptions.FlowControlError 126 | :show-inheritance: 127 | :members: 128 | 129 | .. autoclass:: h2.exceptions.StreamIDTooLowError 130 | :show-inheritance: 131 | :members: 132 | 133 | .. autoclass:: h2.exceptions.InvalidSettingsValueError 134 | :members: 135 | 136 | .. autoclass:: h2.exceptions.NoAvailableStreamIDError 137 | :show-inheritance: 138 | :members: 139 | 140 | .. autoclass:: h2.exceptions.InvalidBodyLengthError 141 | :show-inheritance: 142 | :members: 143 | 144 | .. autoclass:: h2.exceptions.UnsupportedFrameError 145 | :members: 146 | 147 | .. autoclass:: h2.exceptions.DenialOfServiceError 148 | :show-inheritance: 149 | :members: 150 | 151 | 152 | HTTP/2 Error Codes 153 | ------------------ 154 | 155 | .. automodule:: h2.errors 156 | :members: 157 | 158 | 159 | Settings 160 | -------- 161 | 162 | .. autoclass:: h2.settings.SettingCodes 163 | :members: 164 | 165 | .. autoclass:: h2.settings.Settings 166 | :inherited-members: 167 | 168 | .. autoclass:: h2.settings.ChangedSetting 169 | :members: 170 | -------------------------------------------------------------------------------- /docs/source/asyncio-example.rst: -------------------------------------------------------------------------------- 1 | Asyncio Example Server 2 | ====================== 3 | 4 | This example is a basic HTTP/2 server written using `asyncio`_, using some 5 | functionality that was introduced in Python 3.5. This server represents 6 | basically just the same JSON-headers-returning server that was built in the 7 | :doc:`basic-usage` document. 8 | 9 | This example demonstrates some basic asyncio techniques. 10 | 11 | .. literalinclude:: ../../examples/asyncio/asyncio-server.py 12 | :language: python 13 | :linenos: 14 | :encoding: utf-8 15 | 16 | 17 | You can use ``cert.crt`` and ``cert.key`` files provided within the repository 18 | or generate your own certificates using `OpenSSL`_: 19 | 20 | .. code-block:: console 21 | 22 | $ openssl req -x509 -newkey rsa:2048 -keyout cert.key -out cert.crt -days 365 -nodes 23 | 24 | 25 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 26 | .. _OpenSSL: https://openssl-library.org/source/index.html -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | import re 16 | 17 | sys.path.insert(0, os.path.abspath('../..')) 18 | 19 | PROJECT_ROOT = os.path.dirname(__file__) 20 | # Get the version 21 | version_regex = r'__version__ = ["\']([^"\']*)["\']' 22 | with open(os.path.join(PROJECT_ROOT, '../../', 'src/h2/__init__.py')) as file_: 23 | text = file_.read() 24 | match = re.search(version_regex, text) 25 | version = match.group(1) 26 | 27 | 28 | # -- Project information ----------------------------------------------------- 29 | 30 | project = 'hyper-h2' 31 | copyright = '2020, Cory Benfield' 32 | author = 'Cory Benfield' 33 | release = version 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx.ext.intersphinx', 43 | 'sphinx.ext.viewcode', 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = [] 53 | 54 | # Example configuration for intersphinx: refer to the Python standard library. 55 | intersphinx_mapping = { 56 | 'python': ('https://docs.python.org/', None), 57 | 'hpack': ('https://hpack.readthedocs.io/en/stable/', None), 58 | 'pyopenssl': ('https://pyopenssl.readthedocs.org/en/latest/', None), 59 | } 60 | 61 | master_doc = 'index' 62 | 63 | 64 | # -- Options for HTML output ------------------------------------------------- 65 | 66 | # The theme to use for HTML and HTML Help pages. See the documentation for 67 | # a list of builtin themes. 68 | # 69 | html_theme = 'default' 70 | 71 | # Add any paths that contain custom static files (such as style sheets) here, 72 | # relative to this directory. They are copied after the builtin static files, 73 | # so a file named "default.css" will overwrite the builtin "default.css". 74 | html_static_path = ['_static'] 75 | -------------------------------------------------------------------------------- /docs/source/curio-example.rst: -------------------------------------------------------------------------------- 1 | Curio Example Server 2 | ==================== 3 | 4 | This example is a basic HTTP/2 server written using `curio`_, David Beazley's 5 | example of how to build a concurrent networking framework using Python 3.5's 6 | new ``async``/``await`` syntax. 7 | 8 | This example is notable for demonstrating the correct use of HTTP/2 flow 9 | control with h2. It is also a good example of the brand new syntax. 10 | 11 | .. literalinclude:: ../../examples/curio/curio-server.py 12 | :language: python 13 | :linenos: 14 | :encoding: utf-8 15 | 16 | 17 | .. _curio: https://curio.readthedocs.org/en/latest/ 18 | -------------------------------------------------------------------------------- /docs/source/eventlet-example.rst: -------------------------------------------------------------------------------- 1 | Eventlet Example Server 2 | ======================= 3 | 4 | This example is a basic HTTP/2 server written using the `eventlet`_ concurrent 5 | networking framework. This example is notable for demonstrating how to 6 | configure `PyOpenSSL`_, which `eventlet`_ uses for its TLS layer. 7 | 8 | In terms of HTTP/2 functionality, this example is very simple: it returns the 9 | request headers as a JSON document to the caller. It does not obey HTTP/2 flow 10 | control, which is a flaw, but it is otherwise functional. 11 | 12 | .. literalinclude:: ../../examples/eventlet/eventlet-server.py 13 | :language: python 14 | :linenos: 15 | :encoding: utf-8 16 | 17 | 18 | .. _eventlet: http://eventlet.net/ 19 | .. _PyOpenSSL: https://pyopenssl.readthedocs.org/en/stable/ 20 | -------------------------------------------------------------------------------- /docs/source/examples.rst: -------------------------------------------------------------------------------- 1 | Code Examples 2 | ============= 3 | 4 | This section of the documentation contains long-form code examples. These are 5 | intended as references for developers that would like to get an understanding 6 | of how h2 fits in with various Python I/O frameworks. 7 | 8 | Example Servers 9 | --------------- 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | asyncio-example 15 | curio-example 16 | eventlet-example 17 | gevent-example 18 | tornado-example 19 | twisted-example 20 | wsgi-example 21 | 22 | Example Clients 23 | --------------- 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | plain-sockets-example 29 | twisted-head-example 30 | twisted-post-example 31 | -------------------------------------------------------------------------------- /docs/source/gevent-example.rst: -------------------------------------------------------------------------------- 1 | Gevent Example Server 2 | ===================== 3 | 4 | This example is a basic HTTP/2 server written using `gevent`_, a powerful 5 | coroutine-based Python networking library that uses `greenlet`_ 6 | to provide a high-level synchronous API on top of the `libev`_ or `libuv`_ 7 | event loop. 8 | 9 | This example is inspired by the curio one and also demonstrates the correct use 10 | of HTTP/2 flow control with h2 and how gevent can be simple to use. 11 | 12 | .. literalinclude:: ../../examples/gevent/gevent-server.py 13 | :language: python 14 | :linenos: 15 | :encoding: utf-8 16 | 17 | .. _gevent: http://www.gevent.org/ 18 | .. _greenlet: https://greenlet.readthedocs.io/en/latest/ 19 | .. _libev: http://software.schmorp.de/pkg/libev.html 20 | .. _libuv: http://libuv.org/ -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. hyper-h2 documentation master file, created by 2 | sphinx-quickstart on Thu Sep 17 10:06:02 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | h2: A pure-Python HTTP/2 protocol stack 7 | ======================================= 8 | 9 | h2 is a HTTP/2 protocol stack, written entirely in Python. The goal of 10 | h2 is to be a common HTTP/2 stack for the Python ecosystem, 11 | usable in all programs regardless of concurrency model or environment. 12 | 13 | To achieve this, h2 is entirely self-contained: it does no I/O of any 14 | kind, leaving that up to a wrapper library to control. This ensures that it can 15 | seamlessly work in all kinds of environments, from single-threaded code to 16 | Twisted. 17 | 18 | Its goal is to be 100% compatible with RFC 7540, implementing a complete HTTP/2 19 | protocol stack build on a set of finite state machines. Its secondary goals are 20 | to be fast, clear, and efficient. 21 | 22 | For usage examples, see :doc:`basic-usage` or consult the examples in the 23 | repository. 24 | 25 | Contents 26 | -------- 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | 31 | installation 32 | basic-usage 33 | negotiating-http2 34 | examples 35 | advanced-usage 36 | low-level 37 | api 38 | testimonials 39 | release-process 40 | release-notes 41 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | h2 is a pure-python project. This means installing it is extremely 5 | simple. To get the latest release from PyPI, simply run: 6 | 7 | .. code-block:: console 8 | 9 | $ python -m pip install h2 10 | 11 | Alternatively, feel free to download one of the release tarballs from 12 | `our GitHub page`_, extract it to your favourite directory, and then run 13 | 14 | .. code-block:: console 15 | 16 | $ python setup.py install 17 | 18 | .. _our GitHub page: https://github.com/python-hyper/h2 19 | -------------------------------------------------------------------------------- /docs/source/low-level.rst: -------------------------------------------------------------------------------- 1 | Low-Level Details 2 | ================= 3 | 4 | .. warning:: This section of the documentation covers low-level implementation 5 | details of h2. This is most likely to be of use to h2 6 | developers and to other HTTP/2 implementers, though it could well 7 | be of general interest. Feel free to peruse it, but if you're 8 | looking for information about how to *use* h2 you should 9 | consider looking elsewhere. 10 | 11 | State Machines 12 | -------------- 13 | 14 | h2 is fundamentally built on top of a pair of interacting Finite State 15 | Machines. One of these FSMs manages per-connection state, and another manages 16 | per-stream state. Almost without exception (see :ref:`priority` for more 17 | details) every single frame is unconditionally translated into events for 18 | both state machines and those state machines are turned. 19 | 20 | The advantages of a system such as this is that the finite state machines can 21 | very densely encode the kinds of things that are allowed at any particular 22 | moment in a HTTP/2 connection. However, most importantly, almost all protocols 23 | are defined *in terms* of finite state machines: that is, protocol descriptions 24 | can be reduced to a number of states and inputs. That makes FSMs a very natural 25 | tool for implementing protocol stacks. 26 | 27 | Indeed, most protocol implementations that do not explicitly encode a finite 28 | state machine almost always *implicitly* encode a finite state machine, by 29 | using classes with a bunch of variables that amount to state-tracking 30 | variables, or by using the call-stack as an implicit state tracking mechanism. 31 | While these methods are not immediately problematic, they tend to lack 32 | *explicitness*, and can lead to subtle bugs of the form "protocol action X is 33 | incorrectly allowed in state Y". 34 | 35 | For these reasons, we have implemented two *explicit* finite state machines. 36 | These machines aim to encode most of the protocol-specific state, in particular 37 | regarding what frame is allowed at what time. This target goal is sometimes not 38 | achieved: in particular, as of this writing the *stream* FSM contains a number 39 | of other state variables that really ought to be rolled into the state machine 40 | itself in the form of new states, or in the form of a transformation of the 41 | FSM to use state *vectors* instead of state *scalars*. 42 | 43 | The following sections contain some implementers notes on these FSMs. 44 | 45 | Connection State Machine 46 | ~~~~~~~~~~~~~~~~~~~~~~~~ 47 | 48 | The "outer" state machine, the first one that is encountered when sending or 49 | receiving data, is the connection state machine. This state machine tracks 50 | whole-connection state. 51 | 52 | This state machine is primarily intended to forbid certain actions on the basis 53 | of whether the implementation is acting as a client or a server. For example, 54 | clients are not permitted to send ``PUSH_PROMISE`` frames: this state machine 55 | forbids that by refusing to define a valid transition from the ``CLIENT_OPEN`` 56 | state for the ``SEND_PUSH_PROMISE`` event. 57 | 58 | Otherwise, this particular state machine triggers no side-effects. It has a 59 | very coarse, high-level, functionality. 60 | 61 | A visual representation of this FSM is shown below: 62 | 63 | .. image:: _static/h2.connection.H2ConnectionStateMachine.dot.png 64 | :alt: A visual representation of the connection FSM. 65 | :target: _static/h2.connection.H2ConnectionStateMachine.dot.png 66 | 67 | 68 | .. _stream-state-machine: 69 | 70 | Stream State Machine 71 | ~~~~~~~~~~~~~~~~~~~~ 72 | 73 | Once the connection state machine has been spun, any frame that belongs to a 74 | stream is passed to the stream state machine for its given stream. Each stream 75 | has its own instance of the state machine, but all of them share the transition 76 | table: this is because the table itself is sufficiently large that having it be 77 | per-instance would be a ridiculous memory overhead. 78 | 79 | Unlike the connection state machine, the stream state machine is quite complex. 80 | This is because it frequently needs to encode some side-effects. The most 81 | common side-effect is emitting a ``RST_STREAM`` frame when an error is 82 | encountered: the need to do this means that far more transitions need to be 83 | encoded than for the connection state machine. 84 | 85 | Many of the side-effect functions in this state machine also raise 86 | :class:`ProtocolError ` exceptions. This is almost 87 | always done on the basis of an extra state variable, which is an annoying code 88 | smell: it should always be possible for the state machine itself to police 89 | these using explicit state management. A future refactor will hopefully address 90 | this problem by making these additional state variables part of the state 91 | definitions in the FSM, which will lead to an expansion of the number of states 92 | but a greater degree of simplicity in understanding and tracking what is going 93 | on in the state machine. 94 | 95 | The other action taken by the side-effect functions defined here is returning 96 | :ref:`events `. Most of these events are returned directly to 97 | the user, and reflect the specific state transition that has taken place, but 98 | some of the events are purely *internal*: they are used to signal to other 99 | parts of the h2 codebase what action has been taken. 100 | 101 | The major use of the internal events functionality at this time is for 102 | validating header blocks: there are different rules for request headers than 103 | there are for response headers, and different rules again for trailers. The 104 | internal events are used to determine *exactly what* kind of data the user is 105 | attempting to send, and using that information to do the correct kind of 106 | validation. This approach ensures that the final source of truth about what's 107 | happening at the protocol level lives inside the FSM, which is an extremely 108 | important design principle we want to continue to enshrine in h2. 109 | 110 | A visual representation of this FSM is shown below: 111 | 112 | .. image:: _static/h2.stream.H2StreamStateMachine.dot.png 113 | :alt: A visual representation of the stream FSM. 114 | :target: _static/h2.stream.H2StreamStateMachine.dot.png 115 | 116 | 117 | .. _priority: 118 | 119 | Priority 120 | ~~~~~~~~ 121 | 122 | In the :ref:`stream-state-machine` section we said that any frame that belongs 123 | to a stream is passed to the stream state machine. This turns out to be not 124 | quite true. 125 | 126 | Specifically, while ``PRIORITY`` frames are technically sent on a given stream 127 | (that is, `RFC 7540 Section 6.3`_ defines them as "always identifying a stream" 128 | and forbids the use of stream ID ``0`` for them), in practice they are almost 129 | completely exempt from the usual stream FSM behaviour. Specifically, the RFC 130 | has this to say: 131 | 132 | The ``PRIORITY`` frame can be sent on a stream in any state, though it 133 | cannot be sent between consecutive frames that comprise a single 134 | header block (Section 4.3). 135 | 136 | Given that the consecutive header block requirement is handled outside of the 137 | FSMs, this section of the RFC essentially means that there is *never* a 138 | situation where it is invalid to receive a ``PRIORITY`` frame. This means that 139 | including it in the stream FSM would require that we allow ``SEND_PRIORITY`` 140 | and ``RECV_PRIORITY`` in all states. 141 | 142 | This is not a totally onerous task: however, another key note is that h2 143 | uses the *absence* of a stream state machine to flag a closed stream. This is 144 | primarily for memory conservation reasons: if we needed to keep around an FSM 145 | for every stream we've ever seen, that would cause long-lived HTTP/2 146 | connections to consume increasingly large amounts of memory. On top of this, 147 | it would require us to create a stream FSM each time we received a ``PRIORITY`` 148 | frame for a given stream, giving a malicious peer an easy route to force a 149 | h2 user to allocate nearly unbounded amounts of memory. 150 | 151 | For this reason, h2 circumvents the stream FSM entirely for ``PRIORITY`` 152 | frames. Instead, these frames are treated as being connection-level frames that 153 | *just happen* to identify a specific stream. They do not bring streams into 154 | being, or in any sense interact with h2's view of streams. Their stream 155 | details are treated as strictly metadata that h2 is not interested in 156 | beyond being able to parse it out. 157 | 158 | 159 | .. _RFC 7540 Section 6.3: https://tools.ietf.org/html/rfc7540#section-6.3 160 | -------------------------------------------------------------------------------- /docs/source/negotiating-http2.rst: -------------------------------------------------------------------------------- 1 | Negotiating HTTP/2 2 | ================== 3 | 4 | `RFC 7540`_ specifies three methods of negotiating HTTP/2 connections. This document outlines how to use h2 with each one. 5 | 6 | .. _starting-alpn: 7 | 8 | HTTPS URLs (ALPN) 9 | ------------------------- 10 | 11 | Starting HTTP/2 for HTTPS URLs is outlined in `RFC 7540 Section 3.3`_. In this case, the client and server use a TLS extension to negotiate HTTP/2: `ALPN`_. How to use ALPN is currently not covered in this document: please consult the documentation for either the :mod:`ssl module ` in the standard library, or the :mod:`PyOpenSSL ` third-party modules, for more on this topic. 12 | 13 | This method is the simplest to use once the TLS connection is established. To use it with h2, after you've established the connection and confirmed that HTTP/2 has been negotiated with `ALPN`_, create a :class:`H2Connection ` object and call :meth:`H2Connection.initiate_connection `. This will ensure that the appropriate preamble data is placed in the data buffer. You should then immediately send the data returned by :meth:`H2Connection.data_to_send ` on your TLS connection. 14 | 15 | At this point, you're free to use all the HTTP/2 functionality provided by h2. 16 | 17 | .. note:: 18 | Although h2 is not concerned with negotiating protocol versions, it is important to note that support for `ALPN`_ is not available in the standard library of Python versions < 2.7.9. 19 | As a consequence, clients may encounter various errors due to protocol versions mismatch. 20 | 21 | Server Setup Example 22 | ~~~~~~~~~~~~~~~~~~~~ 23 | 24 | This example uses the APIs as defined in Python 3.5. If you are using an older version of Python you may not have access to the APIs used here. As noted above, please consult the documentation for the :mod:`ssl module ` to confirm. 25 | 26 | .. literalinclude:: ../../examples/fragments/server_https_setup_fragment.py 27 | :language: python 28 | :linenos: 29 | :encoding: utf-8 30 | 31 | 32 | Client Setup Example 33 | ~~~~~~~~~~~~~~~~~~~~ 34 | 35 | The client example is very similar to the server example above. The :class:`SSLContext ` object requires some minor changes, as does the :class:`H2Connection `, but the bulk of the code is the same. 36 | 37 | .. literalinclude:: ../../examples/fragments/client_https_setup_fragment.py 38 | :language: python 39 | :linenos: 40 | :encoding: utf-8 41 | 42 | 43 | .. _starting-upgrade: 44 | 45 | HTTP URLs (Upgrade) 46 | ------------------- 47 | 48 | Starting HTTP/2 for HTTP URLs is outlined in `RFC 7540 Section 3.2`_. In this case, the client and server use the HTTP Upgrade mechanism originally described in `RFC 7230 Section 6.7`_. The client sends its initial HTTP/1.1 request with two extra headers. The first is ``Upgrade: h2c``, which requests upgrade to cleartext HTTP/2. The second is a ``HTTP2-Settings`` header, which contains a specially formatted string that encodes a HTTP/2 Settings frame. 49 | 50 | To do this with h2 you have two slightly different flows: one for clients, one for servers. 51 | 52 | Clients 53 | ~~~~~~~ 54 | 55 | For a client, when sending the first request you should manually add your ``Upgrade`` header. You should then create a :class:`H2Connection ` object and call :meth:`H2Connection.initiate_upgrade_connection ` with no arguments. This method will return a bytestring to use as the value of your ``HTTP2-Settings`` header. 56 | 57 | If the server returns a ``101`` status code, it has accepted the upgrade, and you should immediately send the data returned by :meth:`H2Connection.data_to_send `. Now you should consume the entire ``101`` header block. All data after the ``101`` header block is HTTP/2 data that should be fed directly to :meth:`H2Connection.receive_data ` and handled as normal with h2. 58 | 59 | If the server does not return a ``101`` status code then it is not upgrading. Continue with HTTP/1.1 as normal: you may throw away your :class:`H2Connection ` object, as it is of no further use. 60 | 61 | The server will respond to your original request in HTTP/2. Please pay attention to the events received from h2, as they will define the server's response. 62 | 63 | Client Example 64 | ^^^^^^^^^^^^^^ 65 | 66 | The code below demonstrates how to handle a plaintext upgrade from the perspective of the client. For the purposes of keeping the example code as simple and generic as possible it uses the synchronous socket API that comes with the Python standard library: if you want to use asynchronous I/O, you will need to translate this code to the appropriate idiom. 67 | 68 | .. literalinclude:: ../../examples/fragments/client_upgrade_fragment.py 69 | :language: python 70 | :linenos: 71 | :encoding: utf-8 72 | 73 | 74 | Servers 75 | ~~~~~~~ 76 | 77 | If the first request you receive on a connection from the client contains an ``Upgrade`` header with the ``h2c`` token in it, and you're willing to upgrade, you should create a :class:`H2Connection ` object and call :meth:`H2Connection.initiate_upgrade_connection ` with the value of the ``HTTP2-Settings`` header (as a bytestring) as the only argument. 78 | 79 | Then, you should send back a ``101`` response that contains ``h2c`` in the ``Upgrade`` header. That response will inform the client that you're switching to HTTP/2. Then, you should immediately send the data that is returned to you by :meth:`H2Connection.data_to_send ` on the connection: this is a necessary part of the HTTP/2 upgrade process. 80 | 81 | At this point, you may now respond to the original HTTP/1.1 request in HTTP/2 by calling the appropriate methods on the :class:`H2Connection ` object. No further HTTP/1.1 may be sent on this connection: from this point onward, all data sent by you and the client will be HTTP/2 data. 82 | 83 | Server Example 84 | ^^^^^^^^^^^^^^ 85 | 86 | The code below demonstrates how to handle a plaintext upgrade from the perspective of the server. For the purposes of keeping the example code as simple and generic as possible it uses the synchronous socket API that comes with the Python standard library: if you want to use asynchronous I/O, you will need to translate this code to the appropriate idiom. 87 | 88 | .. literalinclude:: ../../examples/fragments/server_upgrade_fragment.py 89 | :language: python 90 | :linenos: 91 | :encoding: utf-8 92 | 93 | 94 | Prior Knowledge 95 | --------------- 96 | 97 | It's possible that you as a client know that a particular server supports HTTP/2, and that you do not need to perform any of the negotiations described above. In that case, you may follow the steps in :ref:`starting-alpn`, ignoring all references to ALPN: there's no need to perform the upgrade dance described in :ref:`starting-upgrade`. 98 | 99 | .. _RFC 7540: https://tools.ietf.org/html/rfc7540 100 | .. _RFC 7540 Section 3.2: https://tools.ietf.org/html/rfc7540#section-3.2 101 | .. _RFC 7540 Section 3.3: https://tools.ietf.org/html/rfc7540#section-3.3 102 | .. _ALPN: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation 103 | .. _RFC 7230 Section 6.7: https://tools.ietf.org/html/rfc7230#section-6.7 104 | -------------------------------------------------------------------------------- /docs/source/plain-sockets-example.rst: -------------------------------------------------------------------------------- 1 | Plain Sockets Example Client 2 | ============================ 3 | 4 | This example is a basic HTTP/2 client written using plain Python `sockets`_, and 5 | `ssl`_ TLS/SSL wrapper for socket objects. 6 | 7 | This client is *not* a complete production-ready HTTP/2 client and only intended 8 | as a demonstration sample. 9 | 10 | This example shows the bare minimum that is needed to send an HTTP/2 request to 11 | a server, and read back a response body. 12 | 13 | .. literalinclude:: ../../examples/plain_sockets/plain_sockets_client.py 14 | :language: python 15 | :linenos: 16 | :encoding: utf-8 17 | 18 | .. _sockets: https://docs.python.org/3/library/socket.html 19 | .. _ssl: https://docs.python.org/3/library/ssl.html 20 | -------------------------------------------------------------------------------- /docs/source/release-notes.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | This document contains release notes for Hyper-h2. In addition to the 5 | :ref:`detailed-release-notes` found at the bottom of this document, this 6 | document also includes a high-level prose overview of each major release after 7 | 1.0.0. 8 | 9 | High Level Notes 10 | ---------------- 11 | 12 | 3.0.0: 24 March 2017 13 | ~~~~~~~~~~~~~~~~~~~~ 14 | 15 | The Hyper-h2 team and the Hyper project are delighted to announce the release 16 | of Hyper-h2 version 3.0.0! Unlike the really notable 2.0.0 release, this 17 | release is proportionally quite small: however, it has the effect of removing a 18 | lot of cruft and complexity that has built up in the codebase over the lifetime 19 | of the v2 release series. 20 | 21 | This release was motivated primarily by discovering that applications that 22 | attempted to use both HTTP/1.1 and HTTP/2 using hyper-h2 would encounter 23 | problems with cookies, because hyper-h2 did not join together cookie headers as 24 | required by RFC 7540. Normally adding such behaviour would be a non-breaking 25 | change, but we previously had no flags to prevent normalization of received 26 | HTTP headers. 27 | 28 | Because it makes no sense for the cookie to be split *by default*, we needed to 29 | add a controlling flag and set it to true. The breaking nature of this change 30 | is very subtle, and it's possible most users would never notice, but 31 | nevertheless it *is* a breaking change and we need to treat it as such. 32 | 33 | Happily, we can take this opportunity to finalise a bunch of deprecations we'd 34 | made over the past year. The v2 release series was long-lived and successful, 35 | having had a series of releases across the past year-and-a-bit, and the Hyper 36 | team are very proud of it. However, it's time to open a new chapter, and remove 37 | the deprecated code. 38 | 39 | The past year has been enormously productive for the Hyper team. A total of 30 40 | v2 releases were made, an enormous amount of work. A good number of people have 41 | made their first contribution in this time, more than I can thank reasonably 42 | without taking up an unreasonable amount of space in this document, so instead 43 | I invite you to check out `our awesome contributor list`_. 44 | 45 | We're looking forward to the next chapter in hyper-h2: it's been a fun ride so 46 | far, and we hope even more of you come along and join in the fun over the next 47 | year! 48 | 49 | .. _our awesome contributor list: https://github.com/python-hyper/hyper-h2/graphs/contributors 50 | 51 | 52 | 2.0.0: 25 January 2016 53 | ~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | The Hyper-h2 team and the Hyper project are delighted to announce the release 56 | of Hyper-h2 version 2.0.0! This is an enormous release that contains a gigantic 57 | collection of new features and fixes, with the goal of making it easier than 58 | ever to use Hyper-h2 to build a compliant HTTP/2 server or client. 59 | 60 | An enormous chunk of this work has been focused on tighter enforcement of 61 | restrictions in RFC 7540, ensuring that we correctly police the actions of 62 | remote peers, and error appropriately when those peers violate the 63 | specification. Several of these constitute breaking changes, because data that 64 | was previously received and handled without obvious error now raises 65 | ``ProtocolError`` exceptions and causes the connection to be terminated. 66 | 67 | Additionally, the public API was cleaned up and had several helper methods that 68 | had been inavertently exposed removed from the public API. The team wants to 69 | stress that while Hyper-h2 follows semantic versioning, the guarantees of 70 | semver apply only to the public API as documented in :doc:`api`. Reducing the 71 | surface area of these APIs makes it easier for us to continue to ensure that 72 | the guarantees of semver are respected on our public API. 73 | 74 | We also attempted to clear up some of the warts that had appeared in the API, 75 | and add features that are helpful for implementing HTTP/2 endpoints. For 76 | example, the :class:`H2Connection ` object now 77 | exposes a method for generating the next stream ID that your client or server 78 | can use to initiate a connection (:meth:`get_next_available_stream_id 79 | `). We also removed 80 | some needless return values that were guaranteed to return empty lists, which 81 | were an attempt to make a forward-looking guarantee that was entirely unneeded. 82 | 83 | Altogether, this has been an extremely productive period for Hyper-h2, and a 84 | lot of great work has been done by the community. To that end, we'd also like 85 | to extend a great thankyou to those contributors who made their first contribution 86 | to the project between release 1.0.0 and 2.0.0. Many thanks to: 87 | `Thomas Kriechbaumer`_, `Alex Chan`_, `Maximilian Hils`_, and `Glyph`_. For a 88 | full historical list of contributors, see `contributors`_. 89 | 90 | We're looking forward to the next few months of Python HTTP/2 work, and hoping 91 | that you'll find lots of excellent HTTP/2 applications to build with Hyper-h2! 92 | 93 | 94 | .. _Thomas Kriechbaumer: https://github.com/Kriechi 95 | .. _Alex Chan: https://github.com/alexwlchan 96 | .. _Maximilian Hils: https://github.com/mhils 97 | .. _Glyph: https://github.com/glyph 98 | .. _contributors: https://github.com/python-hyper/hyper-h2/blob/b14817b79c7bb1661e1aa84ef7920c009ef1e75b/CONTRIBUTORS.rst 99 | 100 | 101 | .. _detailed-release-notes: 102 | .. include:: ../../CHANGELOG.rst 103 | -------------------------------------------------------------------------------- /docs/source/release-process.rst: -------------------------------------------------------------------------------- 1 | Release Process 2 | =============== 3 | 4 | Because of h2's place at the bottom of the dependency tree, it is 5 | extremely important that the project maintains a diligent release schedule. 6 | This document outlines our process for managing releases. 7 | 8 | Versioning 9 | ---------- 10 | 11 | h2 follows `semantic versioning`_ of its public API when it comes to 12 | numbering releases. The public API of h2 is strictly limited to the 13 | entities listed in the :doc:`api` documentation: anything not mentioned in that 14 | document is not considered part of the public API and is not covered by the 15 | versioning guarantees given by semantic versioning. 16 | 17 | Maintenance 18 | ----------- 19 | 20 | h2 has the notion of a "release series", given by a major and minor 21 | version number: for example, there is the 2.1 release series. When each minor 22 | release is made and a release series is born, a branch is made off the release 23 | tag: for example, for the 2.1 release series, the 2.1.X branch. 24 | 25 | All changes merged into the master branch will be evaluated for whether they 26 | can be considered 'bugfixes' only (that is, they do not affect the public API). 27 | If they can, they will also be cherry-picked back to all active maintenance 28 | branches that require the bugfix. If the bugfix is not necessary, because the 29 | branch in question is unaffected by that bug, the bugfix will not be 30 | backported. 31 | 32 | Supported Release Series' 33 | ------------------------- 34 | 35 | The developers of h2 commit to supporting the following release series: 36 | 37 | - The most recent, as identified by the first two numbers in the highest 38 | version currently released. 39 | - The immediately prior release series. 40 | 41 | The only exception to this policy is that no release series earlier than the 42 | 2.1 series will be supported. In this context, "supported" means that they will 43 | continue to receive bugfix releases. 44 | 45 | For releases other than the ones identified above, no support is guaranteed. 46 | The developers may *choose* to support such a release series, but they do not 47 | promise to. 48 | 49 | The exception here is for security vulnerabilities. If a security vulnerability 50 | is identified in an out-of-support release series, the developers will do their 51 | best to patch it and issue an emergency release. For more information, see 52 | `our security documentation`_. 53 | 54 | 55 | .. _semantic versioning: http://semver.org/ 56 | .. _our security documentation: http://python-hyper.org/en/latest/security.html 57 | -------------------------------------------------------------------------------- /docs/source/testimonials.rst: -------------------------------------------------------------------------------- 1 | Testimonials 2 | ============ 3 | 4 | Glyph Lefkowitz 5 | ~~~~~~~~~~~~~~~ 6 | 7 | Frankly, Hyper-h2 is almost SURREAL in how well-factored and decoupled the implementation is from I/O. If libraries in the Python ecosystem looked like this generally, Twisted would be a much better platform than it is. (Frankly, most of Twisted's _own_ protocol implementations should aspire to such cleanliness.) 8 | 9 | (`Source `_) 10 | -------------------------------------------------------------------------------- /docs/source/tornado-example.rst: -------------------------------------------------------------------------------- 1 | Tornado Example Server 2 | ====================== 3 | 4 | This example is a basic HTTP/2 server written using the `Tornado`_ asynchronous 5 | networking library. 6 | 7 | The server returns the request headers as a JSON document to the caller, just 8 | like the example from the :doc:`basic-usage` document. 9 | 10 | .. literalinclude:: ../../examples/tornado/tornado-server.py 11 | :language: python 12 | :linenos: 13 | :encoding: utf-8 14 | 15 | 16 | .. _Tornado: http://www.tornadoweb.org/ 17 | -------------------------------------------------------------------------------- /docs/source/twisted-example.rst: -------------------------------------------------------------------------------- 1 | Twisted Example Server 2 | ====================== 3 | 4 | This example is a basic HTTP/2 server written for the `Twisted`_ asynchronous 5 | networking framework. This is a relatively fleshed out example, and in 6 | particular it makes sure to obey HTTP/2 flow control rules. 7 | 8 | This server differs from some of the other example servers by serving files, 9 | rather than simply sending JSON responses. This makes the example lengthier, 10 | but also brings it closer to a real-world use-case. 11 | 12 | .. literalinclude:: ../../examples/twisted/twisted-server.py 13 | :language: python 14 | :linenos: 15 | :encoding: utf-8 16 | 17 | 18 | .. _Twisted: https://twistedmatrix.com/ -------------------------------------------------------------------------------- /docs/source/twisted-head-example.rst: -------------------------------------------------------------------------------- 1 | Twisted Example Client: Head Requests 2 | ===================================== 3 | 4 | This example is a basic HTTP/2 client written for the `Twisted`_ asynchronous 5 | networking framework. 6 | 7 | This client is fairly simple: it makes a hard-coded HEAD request to 8 | nghttp2.org/httpbin/ and prints out the response data. Its purpose is to demonstrate 9 | how to write a very basic HTTP/2 client implementation. 10 | 11 | .. literalinclude:: ../../examples/twisted/head_request.py 12 | :language: python 13 | :linenos: 14 | :encoding: utf-8 15 | 16 | 17 | .. _Twisted: https://twistedmatrix.com/ 18 | -------------------------------------------------------------------------------- /docs/source/twisted-post-example.rst: -------------------------------------------------------------------------------- 1 | Twisted Example Client: Post Requests 2 | ===================================== 3 | 4 | This example is a basic HTTP/2 client written for the `Twisted`_ asynchronous 5 | networking framework. 6 | 7 | This client is fairly simple: it makes a hard-coded POST request to 8 | nghttp2.org/httpbin/post and prints out the response data, sending a file that is provided 9 | on the command line or the script itself. Its purpose is to demonstrate how to 10 | write a HTTP/2 client implementation that handles flow control. 11 | 12 | .. literalinclude:: ../../examples/twisted/post_request.py 13 | :language: python 14 | :linenos: 15 | :encoding: utf-8 16 | 17 | 18 | .. _Twisted: https://twistedmatrix.com/ 19 | -------------------------------------------------------------------------------- /docs/source/wsgi-example.rst: -------------------------------------------------------------------------------- 1 | Example HTTP/2-only WSGI Server 2 | =============================== 3 | 4 | This example is a more complex HTTP/2 server that acts as a WSGI server, 5 | passing data to an arbitrary WSGI application. This example is written using 6 | `asyncio`_. The server supports most of PEP-3333, and so could in principle be 7 | used as a production WSGI server: however, that's *not recommended* as certain 8 | shortcuts have been taken to ensure ease of implementation and understanding. 9 | 10 | The main advantages of this example are: 11 | 12 | 1. It properly demonstrates HTTP/2 flow control management. 13 | 2. It demonstrates how to plug h2 into a larger, more complex 14 | application. 15 | 16 | 17 | .. literalinclude:: ../../examples/asyncio/wsgi-server.py 18 | :language: python 19 | :linenos: 20 | :encoding: utf-8 21 | 22 | 23 | You can use ``cert.crt`` and ``cert.key`` files provided within the repository 24 | or generate your own certificates using `OpenSSL`_: 25 | 26 | .. code-block:: console 27 | 28 | $ openssl req -x509 -newkey rsa:2048 -keyout cert.key -out cert.crt -days 365 -nodes 29 | 30 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 31 | .. _OpenSSL: https://openssl-library.org/source/index.html 32 | -------------------------------------------------------------------------------- /examples/asyncio/asyncio-server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | asyncio-server.py 4 | ~~~~~~~~~~~~~~~~~ 5 | 6 | A fully-functional HTTP/2 server using asyncio. Requires Python 3.5+. 7 | 8 | This example demonstrates handling requests with bodies, as well as handling 9 | those without. In particular, it demonstrates the fact that DataReceived may 10 | be called multiple times, and that applications must handle that possibility. 11 | """ 12 | import asyncio 13 | import io 14 | import json 15 | import ssl 16 | import collections 17 | from typing import List, Tuple 18 | 19 | from h2.config import H2Configuration 20 | from h2.connection import H2Connection 21 | from h2.events import ( 22 | ConnectionTerminated, DataReceived, RemoteSettingsChanged, 23 | RequestReceived, StreamEnded, StreamReset, WindowUpdated 24 | ) 25 | from h2.errors import ErrorCodes 26 | from h2.exceptions import ProtocolError, StreamClosedError 27 | from h2.settings import SettingCodes 28 | 29 | 30 | RequestData = collections.namedtuple('RequestData', ['headers', 'data']) 31 | 32 | 33 | class H2Protocol(asyncio.Protocol): 34 | def __init__(self): 35 | config = H2Configuration(client_side=False, header_encoding='utf-8') 36 | self.conn = H2Connection(config=config) 37 | self.transport = None 38 | self.stream_data = {} 39 | self.flow_control_futures = {} 40 | 41 | def connection_made(self, transport: asyncio.Transport): 42 | self.transport = transport 43 | self.conn.initiate_connection() 44 | self.transport.write(self.conn.data_to_send()) 45 | 46 | def connection_lost(self, exc): 47 | for future in self.flow_control_futures.values(): 48 | future.cancel() 49 | self.flow_control_futures = {} 50 | 51 | def data_received(self, data: bytes): 52 | try: 53 | events = self.conn.receive_data(data) 54 | except ProtocolError as e: 55 | self.transport.write(self.conn.data_to_send()) 56 | self.transport.close() 57 | else: 58 | self.transport.write(self.conn.data_to_send()) 59 | for event in events: 60 | if isinstance(event, RequestReceived): 61 | self.request_received(event.headers, event.stream_id) 62 | elif isinstance(event, DataReceived): 63 | self.receive_data( 64 | event.data, event.flow_controlled_length, event.stream_id 65 | ) 66 | elif isinstance(event, StreamEnded): 67 | self.stream_complete(event.stream_id) 68 | elif isinstance(event, ConnectionTerminated): 69 | self.transport.close() 70 | elif isinstance(event, StreamReset): 71 | self.stream_reset(event.stream_id) 72 | elif isinstance(event, WindowUpdated): 73 | self.window_updated(event.stream_id, event.delta) 74 | elif isinstance(event, RemoteSettingsChanged): 75 | if SettingCodes.INITIAL_WINDOW_SIZE in event.changed_settings: 76 | self.window_updated(None, 0) 77 | 78 | self.transport.write(self.conn.data_to_send()) 79 | 80 | def request_received(self, headers: List[Tuple[str, str]], stream_id: int): 81 | headers = collections.OrderedDict(headers) 82 | method = headers[':method'] 83 | 84 | # Store off the request data. 85 | request_data = RequestData(headers, io.BytesIO()) 86 | self.stream_data[stream_id] = request_data 87 | 88 | def stream_complete(self, stream_id: int): 89 | """ 90 | When a stream is complete, we can send our response. 91 | """ 92 | try: 93 | request_data = self.stream_data[stream_id] 94 | except KeyError: 95 | # Just return, we probably 405'd this already 96 | return 97 | 98 | headers = request_data.headers 99 | body = request_data.data.getvalue().decode('utf-8') 100 | 101 | data = json.dumps( 102 | {"headers": headers, "body": body}, indent=4 103 | ).encode("utf8") 104 | 105 | response_headers = ( 106 | (':status', '200'), 107 | ('content-type', 'application/json'), 108 | ('content-length', str(len(data))), 109 | ('server', 'asyncio-h2'), 110 | ) 111 | self.conn.send_headers(stream_id, response_headers) 112 | asyncio.ensure_future(self.send_data(data, stream_id)) 113 | 114 | def receive_data(self, data: bytes, flow_controlled_length: int, stream_id: int): 115 | """ 116 | We've received some data on a stream. If that stream is one we're 117 | expecting data on, save it off (and account for the received amount of 118 | data in flow control so that the client can send more data). 119 | Otherwise, reset the stream. 120 | """ 121 | try: 122 | stream_data = self.stream_data[stream_id] 123 | except KeyError: 124 | self.conn.reset_stream( 125 | stream_id, error_code=ErrorCodes.PROTOCOL_ERROR 126 | ) 127 | else: 128 | stream_data.data.write(data) 129 | self.conn.acknowledge_received_data(flow_controlled_length, stream_id) 130 | 131 | def stream_reset(self, stream_id): 132 | """ 133 | A stream reset was sent. Stop sending data. 134 | """ 135 | if stream_id in self.flow_control_futures: 136 | future = self.flow_control_futures.pop(stream_id) 137 | future.cancel() 138 | 139 | async def send_data(self, data, stream_id): 140 | """ 141 | Send data according to the flow control rules. 142 | """ 143 | while data: 144 | while self.conn.local_flow_control_window(stream_id) < 1: 145 | try: 146 | await self.wait_for_flow_control(stream_id) 147 | except asyncio.CancelledError: 148 | return 149 | 150 | chunk_size = min( 151 | self.conn.local_flow_control_window(stream_id), 152 | len(data), 153 | self.conn.max_outbound_frame_size, 154 | ) 155 | 156 | try: 157 | self.conn.send_data( 158 | stream_id, 159 | data[:chunk_size], 160 | end_stream=(chunk_size == len(data)) 161 | ) 162 | except (StreamClosedError, ProtocolError): 163 | # The stream got closed and we didn't get told. We're done 164 | # here. 165 | break 166 | 167 | self.transport.write(self.conn.data_to_send()) 168 | data = data[chunk_size:] 169 | 170 | async def wait_for_flow_control(self, stream_id): 171 | """ 172 | Waits for a Future that fires when the flow control window is opened. 173 | """ 174 | f = asyncio.Future() 175 | self.flow_control_futures[stream_id] = f 176 | await f 177 | 178 | def window_updated(self, stream_id, delta): 179 | """ 180 | A window update frame was received. Unblock some number of flow control 181 | Futures. 182 | """ 183 | if stream_id and stream_id in self.flow_control_futures: 184 | f = self.flow_control_futures.pop(stream_id) 185 | f.set_result(delta) 186 | elif not stream_id: 187 | for f in self.flow_control_futures.values(): 188 | f.set_result(delta) 189 | 190 | self.flow_control_futures = {} 191 | 192 | 193 | ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 194 | ssl_context.options |= ( 195 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION 196 | ) 197 | ssl_context.load_cert_chain(certfile="cert.crt", keyfile="cert.key") 198 | ssl_context.set_alpn_protocols(["h2"]) 199 | 200 | loop = asyncio.get_event_loop() 201 | # Each client connection will create a new protocol instance 202 | coro = loop.create_server(H2Protocol, '127.0.0.1', 8443, ssl=ssl_context) 203 | server = loop.run_until_complete(coro) 204 | 205 | # Serve requests until Ctrl+C is pressed 206 | print('Serving on {}'.format(server.sockets[0].getsockname())) 207 | try: 208 | loop.run_forever() 209 | except KeyboardInterrupt: 210 | pass 211 | finally: 212 | # Close the server 213 | server.close() 214 | loop.run_until_complete(server.wait_closed()) 215 | loop.close() 216 | -------------------------------------------------------------------------------- /examples/asyncio/cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDhTCCAm2gAwIBAgIJAOrxh0dOYJLdMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNTA5MTkxNDE2 5 | NDRaFw0xNTEwMTkxNDE2NDRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l 6 | LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV 7 | BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqt 8 | A1iu8EN00FU0eBcBGlLVmNEgV7Jkbukra+kwS8j/U2y50QPGJc/FiIVDfuBqk5dL 9 | ACTNc6A/FQcXvWmOc5ixmC3QKKasMpuofqKz0V9C6irZdYXZ9rcsW0gHQIr989yd 10 | R+N1VbIlEVW/T9FJL3B2UD9GVIkUELzm47CSOWZvAxQUlsx8CUNuUCWqyZJoqTFN 11 | j0LeJDOWGCsug1Pkj0Q1x+jMVL6l6Zf6vMkLNOMsOsWsxUk+0L3tl/OzcTgUOCsw 12 | UzY59RIi6Rudrp0oaU8NuHr91yiSqPbKFlX10M9KwEEdnIpcxhND3dacrDycj3ux 13 | eWlqKync2vOFUkhwiaMCAwEAAaNQME4wHQYDVR0OBBYEFA0PN+PGoofZ+QIys2Jy 14 | 1Zz94vBOMB8GA1UdIwQYMBaAFA0PN+PGoofZ+QIys2Jy1Zz94vBOMAwGA1UdEwQF 15 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEplethBoPpcP3EbR5Rz6snDDIcbtAJu 16 | Ngd0YZppGT+P0DYnPJva4vRG3bb84ZMSuppz5j67qD6DdWte8UXhK8BzWiHzwmQE 17 | QmbKyzzTMKQgTNFntpx5cgsSvTtrHpNYoMHzHOmyAOboNeM0DWiRXsYLkWTitLTN 18 | qbOpstwPubExbT9lPjLclntShT/lCupt+zsbnrR9YiqlYFY/fDzfAybZhrD5GMBY 19 | XdMPItwAc/sWvH31yztarjkLmld76AGCcO5r8cSR/cX98SicyfjOBbSco8GkjYNY 20 | 582gTPkKGYpStuN7GNT5tZmxvMq935HRa2XZvlAIe8ufp8EHVoYiF3c= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /examples/asyncio/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAyq0DWK7wQ3TQVTR4FwEaUtWY0SBXsmRu6Str6TBLyP9TbLnR 3 | A8Ylz8WIhUN+4GqTl0sAJM1zoD8VBxe9aY5zmLGYLdAopqwym6h+orPRX0LqKtl1 4 | hdn2tyxbSAdAiv3z3J1H43VVsiURVb9P0UkvcHZQP0ZUiRQQvObjsJI5Zm8DFBSW 5 | zHwJQ25QJarJkmipMU2PQt4kM5YYKy6DU+SPRDXH6MxUvqXpl/q8yQs04yw6xazF 6 | ST7Qve2X87NxOBQ4KzBTNjn1EiLpG52unShpTw24ev3XKJKo9soWVfXQz0rAQR2c 7 | ilzGE0Pd1pysPJyPe7F5aWorKdza84VSSHCJowIDAQABAoIBACp+nh4BB/VMz8Wd 8 | q7Q/EfLeQB1Q57JKpoqTBRwueSVai3ZXe4CMEi9/HkG6xiZtkiZ9njkZLq4hq9oB 9 | 2z//kzMnwV2RsIRJxI6ohGy+wR51HD4BvEdlTPpY/Yabpqe92VyfSYxidKZWaU0O 10 | QMED1EODOw4ZQ+4928iPrJu//PMB4e7TFao0b9Fk/XLWtu5/tQZz9jsrlTi1zthh 11 | 7n+oaGNhfTeIJJL4jrhTrKW1CLHXATtr9SJlfZ3wbMxQVeyj2wUlP1V0M6kBuhNj 12 | tbGbMpixD5iCNJ49Cm2PHg+wBOfS3ADGIpi3PcGw5mb8nB3N9eGBRPhLShAlq5Hi 13 | Lv4tyykCgYEA8u3b3xJ04pxWYN25ou/Sc8xzgDCK4XvDNdHVTuZDjLVA+VTVPzql 14 | lw7VvJArsx47MSPvsaX/+4hQXYtfnR7yJpx6QagvQ+z4ludnIZYrQwdUmb9pFL1s 15 | 8UNj+3j9QFRPenIiIQ8qxxNIQ9w2HsVQ8scvc9CjYop/YYAPaQyHaL8CgYEA1ZSz 16 | CR4NcpfgRSILdhb1dLcyw5Qus1VOSAx3DYkhDkMiB8XZwgMdJjwehJo9yaqRCLE8 17 | Sw5znMnkfoZpu7+skrjK0FqmMpXMH9gIszHvFG8wSw/6+2HIWS19/wOu8dh95LuC 18 | 0zurMk8rFqxgWMWF20afhgYrUz42cvUTo10FVB0CgYEAt7mW6W3PArfUSCxIwmb4 19 | VmXREKkl0ATHDYQl/Cb//YHzot467TgQll883QB4XF5HzBFurX9rSzO7/BN1e6I0 20 | 52i+ubtWC9xD4fUetXMaQvZfUGxIL8xXgVxDWKQXfLiG54c8Mp6C7s6xf8kjEUCP 21 | yR1F0SSA/Pzb+8RbY0p7eocCgYA+1rs+SXtHZev0KyoYGnUpW+Uxqd17ofOgOxqj 22 | /t6c5Z+TjeCdtnDTGQkZlo/rT6XQWuUUaDIXxUbW+xEMzj4mBPyXBLS1WWFvVQ5q 23 | OpzO9E/PJeqAH6rkof/aEelc+oc/zvOU1o9uA+D3kMvgEm1psIOq2RHSMhGvDPA0 24 | NmAk+QKBgQCwd1681GagdIYSZUCBecnLtevXmIsJyDW2yR1NNcIe/ukcVQREMDvy 25 | 5DDkhnGDgnV1D5gYcXb34g9vYvbfTnBMl/JXmMAAG1kIS+3pvHyN6f1poVe3yJV1 26 | yHVuvymnJxKnyaV0L3ntepVvV0vVNIkA3oauoUTLto6txBI+b/ImDA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/curio/curio-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.5 2 | # -*- coding: utf-8 -*- 3 | """ 4 | curio-server.py 5 | ~~~~~~~~~~~~~~~ 6 | 7 | A fully-functional HTTP/2 server written for curio. 8 | 9 | Requires Python 3.5+. 10 | """ 11 | import mimetypes 12 | import os 13 | import sys 14 | 15 | from curio import Event, spawn, socket, ssl, run 16 | 17 | import h2.config 18 | import h2.connection 19 | import h2.events 20 | 21 | 22 | # The maximum amount of a file we'll send in a single DATA frame. 23 | READ_CHUNK_SIZE = 8192 24 | 25 | 26 | async def create_listening_ssl_socket(address, certfile, keyfile): 27 | """ 28 | Create and return a listening TLS socket on a given address. 29 | """ 30 | ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 31 | ssl_context.options |= ( 32 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION 33 | ) 34 | ssl_context.set_ciphers("ECDHE+AESGCM") 35 | ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) 36 | ssl_context.set_alpn_protocols(["h2"]) 37 | 38 | sock = socket.socket() 39 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 40 | sock = await ssl_context.wrap_socket(sock) 41 | sock.bind(address) 42 | sock.listen() 43 | 44 | return sock 45 | 46 | 47 | async def h2_server(address, root, certfile, keyfile): 48 | """ 49 | Create an HTTP/2 server at the given address. 50 | """ 51 | sock = await create_listening_ssl_socket(address, certfile, keyfile) 52 | print("Now listening on %s:%d" % address) 53 | 54 | async with sock: 55 | while True: 56 | client, _ = await sock.accept() 57 | server = H2Server(client, root) 58 | await spawn(server.run()) 59 | 60 | 61 | class H2Server: 62 | """ 63 | A basic HTTP/2 file server. This is essentially very similar to 64 | SimpleHTTPServer from the standard library, but uses HTTP/2 instead of 65 | HTTP/1.1. 66 | """ 67 | def __init__(self, sock, root): 68 | config = h2.config.H2Configuration( 69 | client_side=False, header_encoding='utf-8' 70 | ) 71 | self.sock = sock 72 | self.conn = h2.connection.H2Connection(config=config) 73 | self.root = root 74 | self.flow_control_events = {} 75 | 76 | async def run(self): 77 | """ 78 | Loop over the connection, managing it appropriately. 79 | """ 80 | self.conn.initiate_connection() 81 | await self.sock.sendall(self.conn.data_to_send()) 82 | 83 | while True: 84 | # 65535 is basically arbitrary here: this amounts to "give me 85 | # whatever data you have". 86 | data = await self.sock.recv(65535) 87 | if not data: 88 | break 89 | 90 | events = self.conn.receive_data(data) 91 | for event in events: 92 | if isinstance(event, h2.events.RequestReceived): 93 | await spawn( 94 | self.request_received(event.headers, event.stream_id) 95 | ) 96 | elif isinstance(event, h2.events.DataReceived): 97 | self.conn.reset_stream(event.stream_id) 98 | elif isinstance(event, h2.events.WindowUpdated): 99 | await self.window_updated(event) 100 | 101 | await self.sock.sendall(self.conn.data_to_send()) 102 | 103 | async def request_received(self, headers, stream_id): 104 | """ 105 | Handle a request by attempting to serve a suitable file. 106 | """ 107 | headers = dict(headers) 108 | assert headers[':method'] == 'GET' 109 | 110 | path = headers[':path'].lstrip('/') 111 | full_path = os.path.join(self.root, path) 112 | 113 | if not os.path.exists(full_path): 114 | response_headers = ( 115 | (':status', '404'), 116 | ('content-length', '0'), 117 | ('server', 'curio-h2'), 118 | ) 119 | self.conn.send_headers( 120 | stream_id, response_headers, end_stream=True 121 | ) 122 | await self.sock.sendall(self.conn.data_to_send()) 123 | else: 124 | await self.send_file(full_path, stream_id) 125 | 126 | async def send_file(self, file_path, stream_id): 127 | """ 128 | Send a file, obeying the rules of HTTP/2 flow control. 129 | """ 130 | filesize = os.stat(file_path).st_size 131 | content_type, content_encoding = mimetypes.guess_type(file_path) 132 | response_headers = [ 133 | (':status', '200'), 134 | ('content-length', str(filesize)), 135 | ('server', 'curio-h2'), 136 | ] 137 | if content_type: 138 | response_headers.append(('content-type', content_type)) 139 | if content_encoding: 140 | response_headers.append(('content-encoding', content_encoding)) 141 | 142 | self.conn.send_headers(stream_id, response_headers) 143 | await self.sock.sendall(self.conn.data_to_send()) 144 | 145 | with open(file_path, 'rb', buffering=0) as f: 146 | await self._send_file_data(f, stream_id) 147 | 148 | async def _send_file_data(self, fileobj, stream_id): 149 | """ 150 | Send the data portion of a file. Handles flow control rules. 151 | """ 152 | while True: 153 | while self.conn.local_flow_control_window(stream_id) < 1: 154 | await self.wait_for_flow_control(stream_id) 155 | 156 | chunk_size = min( 157 | self.conn.local_flow_control_window(stream_id), 158 | READ_CHUNK_SIZE, 159 | ) 160 | 161 | data = fileobj.read(chunk_size) 162 | keep_reading = (len(data) == chunk_size) 163 | 164 | self.conn.send_data(stream_id, data, not keep_reading) 165 | await self.sock.sendall(self.conn.data_to_send()) 166 | 167 | if not keep_reading: 168 | break 169 | 170 | async def wait_for_flow_control(self, stream_id): 171 | """ 172 | Blocks until the flow control window for a given stream is opened. 173 | """ 174 | evt = Event() 175 | self.flow_control_events[stream_id] = evt 176 | await evt.wait() 177 | 178 | async def window_updated(self, event): 179 | """ 180 | Unblock streams waiting on flow control, if needed. 181 | """ 182 | stream_id = event.stream_id 183 | 184 | if stream_id and stream_id in self.flow_control_events: 185 | evt = self.flow_control_events.pop(stream_id) 186 | await evt.set() 187 | elif not stream_id: 188 | # Need to keep a real list here to use only the events present at 189 | # this time. 190 | blocked_streams = list(self.flow_control_events.keys()) 191 | for stream_id in blocked_streams: 192 | event = self.flow_control_events.pop(stream_id) 193 | await event.set() 194 | return 195 | 196 | 197 | if __name__ == '__main__': 198 | host = sys.argv[2] if len(sys.argv) > 2 else "localhost" 199 | print("Try GETting:") 200 | print(" On OSX after 'brew install curl --with-c-ares --with-libidn --with-nghttp2 --with-openssl':") 201 | print("/usr/local/opt/curl/bin/curl --tlsv1.2 --http2 -k https://localhost:5000/bundle.js") 202 | print("Or open a browser to: https://localhost:5000/") 203 | print(" (Accept all the warnings)") 204 | run(h2_server((host, 5000), sys.argv[1], 205 | "{}.crt.pem".format(host), 206 | "{}.key".format(host)), with_monitor=True) 207 | -------------------------------------------------------------------------------- /examples/curio/localhost.crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDhTCCAm2gAwIBAgIJAOrxh0dOYJLdMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNTA5MTkxNDE2 5 | NDRaFw0xNTEwMTkxNDE2NDRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l 6 | LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV 7 | BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqt 8 | A1iu8EN00FU0eBcBGlLVmNEgV7Jkbukra+kwS8j/U2y50QPGJc/FiIVDfuBqk5dL 9 | ACTNc6A/FQcXvWmOc5ixmC3QKKasMpuofqKz0V9C6irZdYXZ9rcsW0gHQIr989yd 10 | R+N1VbIlEVW/T9FJL3B2UD9GVIkUELzm47CSOWZvAxQUlsx8CUNuUCWqyZJoqTFN 11 | j0LeJDOWGCsug1Pkj0Q1x+jMVL6l6Zf6vMkLNOMsOsWsxUk+0L3tl/OzcTgUOCsw 12 | UzY59RIi6Rudrp0oaU8NuHr91yiSqPbKFlX10M9KwEEdnIpcxhND3dacrDycj3ux 13 | eWlqKync2vOFUkhwiaMCAwEAAaNQME4wHQYDVR0OBBYEFA0PN+PGoofZ+QIys2Jy 14 | 1Zz94vBOMB8GA1UdIwQYMBaAFA0PN+PGoofZ+QIys2Jy1Zz94vBOMAwGA1UdEwQF 15 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEplethBoPpcP3EbR5Rz6snDDIcbtAJu 16 | Ngd0YZppGT+P0DYnPJva4vRG3bb84ZMSuppz5j67qD6DdWte8UXhK8BzWiHzwmQE 17 | QmbKyzzTMKQgTNFntpx5cgsSvTtrHpNYoMHzHOmyAOboNeM0DWiRXsYLkWTitLTN 18 | qbOpstwPubExbT9lPjLclntShT/lCupt+zsbnrR9YiqlYFY/fDzfAybZhrD5GMBY 19 | XdMPItwAc/sWvH31yztarjkLmld76AGCcO5r8cSR/cX98SicyfjOBbSco8GkjYNY 20 | 582gTPkKGYpStuN7GNT5tZmxvMq935HRa2XZvlAIe8ufp8EHVoYiF3c= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /examples/curio/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAyq0DWK7wQ3TQVTR4FwEaUtWY0SBXsmRu6Str6TBLyP9TbLnR 3 | A8Ylz8WIhUN+4GqTl0sAJM1zoD8VBxe9aY5zmLGYLdAopqwym6h+orPRX0LqKtl1 4 | hdn2tyxbSAdAiv3z3J1H43VVsiURVb9P0UkvcHZQP0ZUiRQQvObjsJI5Zm8DFBSW 5 | zHwJQ25QJarJkmipMU2PQt4kM5YYKy6DU+SPRDXH6MxUvqXpl/q8yQs04yw6xazF 6 | ST7Qve2X87NxOBQ4KzBTNjn1EiLpG52unShpTw24ev3XKJKo9soWVfXQz0rAQR2c 7 | ilzGE0Pd1pysPJyPe7F5aWorKdza84VSSHCJowIDAQABAoIBACp+nh4BB/VMz8Wd 8 | q7Q/EfLeQB1Q57JKpoqTBRwueSVai3ZXe4CMEi9/HkG6xiZtkiZ9njkZLq4hq9oB 9 | 2z//kzMnwV2RsIRJxI6ohGy+wR51HD4BvEdlTPpY/Yabpqe92VyfSYxidKZWaU0O 10 | QMED1EODOw4ZQ+4928iPrJu//PMB4e7TFao0b9Fk/XLWtu5/tQZz9jsrlTi1zthh 11 | 7n+oaGNhfTeIJJL4jrhTrKW1CLHXATtr9SJlfZ3wbMxQVeyj2wUlP1V0M6kBuhNj 12 | tbGbMpixD5iCNJ49Cm2PHg+wBOfS3ADGIpi3PcGw5mb8nB3N9eGBRPhLShAlq5Hi 13 | Lv4tyykCgYEA8u3b3xJ04pxWYN25ou/Sc8xzgDCK4XvDNdHVTuZDjLVA+VTVPzql 14 | lw7VvJArsx47MSPvsaX/+4hQXYtfnR7yJpx6QagvQ+z4ludnIZYrQwdUmb9pFL1s 15 | 8UNj+3j9QFRPenIiIQ8qxxNIQ9w2HsVQ8scvc9CjYop/YYAPaQyHaL8CgYEA1ZSz 16 | CR4NcpfgRSILdhb1dLcyw5Qus1VOSAx3DYkhDkMiB8XZwgMdJjwehJo9yaqRCLE8 17 | Sw5znMnkfoZpu7+skrjK0FqmMpXMH9gIszHvFG8wSw/6+2HIWS19/wOu8dh95LuC 18 | 0zurMk8rFqxgWMWF20afhgYrUz42cvUTo10FVB0CgYEAt7mW6W3PArfUSCxIwmb4 19 | VmXREKkl0ATHDYQl/Cb//YHzot467TgQll883QB4XF5HzBFurX9rSzO7/BN1e6I0 20 | 52i+ubtWC9xD4fUetXMaQvZfUGxIL8xXgVxDWKQXfLiG54c8Mp6C7s6xf8kjEUCP 21 | yR1F0SSA/Pzb+8RbY0p7eocCgYA+1rs+SXtHZev0KyoYGnUpW+Uxqd17ofOgOxqj 22 | /t6c5Z+TjeCdtnDTGQkZlo/rT6XQWuUUaDIXxUbW+xEMzj4mBPyXBLS1WWFvVQ5q 23 | OpzO9E/PJeqAH6rkof/aEelc+oc/zvOU1o9uA+D3kMvgEm1psIOq2RHSMhGvDPA0 24 | NmAk+QKBgQCwd1681GagdIYSZUCBecnLtevXmIsJyDW2yR1NNcIe/ukcVQREMDvy 25 | 5DDkhnGDgnV1D5gYcXb34g9vYvbfTnBMl/JXmMAAG1kIS+3pvHyN6f1poVe3yJV1 26 | yHVuvymnJxKnyaV0L3ntepVvV0vVNIkA3oauoUTLto6txBI+b/ImDA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/eventlet/eventlet-server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | eventlet-server.py 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | A fully-functional HTTP/2 server written for Eventlet. 7 | """ 8 | import collections 9 | import json 10 | 11 | import eventlet 12 | 13 | from eventlet.green.OpenSSL import SSL, crypto 14 | from h2.config import H2Configuration 15 | from h2.connection import H2Connection 16 | from h2.events import RequestReceived, DataReceived 17 | 18 | 19 | class ConnectionManager(object): 20 | """ 21 | An object that manages a single HTTP/2 connection. 22 | """ 23 | def __init__(self, sock): 24 | config = H2Configuration(client_side=False) 25 | self.sock = sock 26 | self.conn = H2Connection(config=config) 27 | 28 | def run_forever(self): 29 | self.conn.initiate_connection() 30 | self.sock.sendall(self.conn.data_to_send()) 31 | 32 | while True: 33 | data = self.sock.recv(65535) 34 | if not data: 35 | break 36 | 37 | events = self.conn.receive_data(data) 38 | 39 | for event in events: 40 | if isinstance(event, RequestReceived): 41 | self.request_received(event.headers, event.stream_id) 42 | elif isinstance(event, DataReceived): 43 | self.conn.reset_stream(event.stream_id) 44 | 45 | self.sock.sendall(self.conn.data_to_send()) 46 | 47 | def request_received(self, headers, stream_id): 48 | headers = collections.OrderedDict(headers) 49 | data = json.dumps({'headers': headers}, indent=4).encode('utf-8') 50 | 51 | response_headers = ( 52 | (':status', '200'), 53 | ('content-type', 'application/json'), 54 | ('content-length', str(len(data))), 55 | ('server', 'eventlet-h2'), 56 | ) 57 | self.conn.send_headers(stream_id, response_headers) 58 | self.conn.send_data(stream_id, data, end_stream=True) 59 | 60 | 61 | def alpn_callback(conn, protos): 62 | if b'h2' in protos: 63 | return b'h2' 64 | 65 | raise RuntimeError("No acceptable protocol offered!") 66 | 67 | 68 | def npn_advertise_cb(conn): 69 | return [b'h2'] 70 | 71 | 72 | # Let's set up SSL. This is a lot of work in PyOpenSSL. 73 | options = ( 74 | SSL.OP_NO_COMPRESSION | 75 | SSL.OP_NO_SSLv2 | 76 | SSL.OP_NO_SSLv3 | 77 | SSL.OP_NO_TLSv1 | 78 | SSL.OP_NO_TLSv1_1 79 | ) 80 | context = SSL.Context(SSL.SSLv23_METHOD) 81 | context.set_options(options) 82 | context.set_verify(SSL.VERIFY_NONE, lambda *args: True) 83 | context.use_privatekey_file('server.key') 84 | context.use_certificate_file('server.crt') 85 | context.set_npn_advertise_callback(npn_advertise_cb) 86 | context.set_alpn_select_callback(alpn_callback) 87 | context.set_cipher_list( 88 | "ECDHE+AESGCM" 89 | ) 90 | context.set_tmp_ecdh(crypto.get_elliptic_curve(u'prime256v1')) 91 | 92 | server = eventlet.listen(('0.0.0.0', 443)) 93 | server = SSL.Connection(context, server) 94 | pool = eventlet.GreenPool() 95 | 96 | while True: 97 | try: 98 | new_sock, _ = server.accept() 99 | manager = ConnectionManager(new_sock) 100 | pool.spawn_n(manager.run_forever) 101 | except (SystemExit, KeyboardInterrupt): 102 | break 103 | -------------------------------------------------------------------------------- /examples/eventlet/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDUjCCAjoCCQCQmNzzpQTCijANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQGEwJH 3 | QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5 4 | cGVyLWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20w 5 | HhcNMTUwOTE2MjAyOTA0WhcNMTYwOTE1MjAyOTA0WjBrMQswCQYDVQQGEwJHQjEP 6 | MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5cGVy 7 | LWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEi 8 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74ZeB4Jdb5cnC9KXXLJuzjwTg 9 | 45q5EeShDYQe0TbKgreiUP6clU3BR0fFAVedN1q/LOuQ1HhvrDk1l4TfGF2bpCIq 10 | K+U9CnzcQknvdpyyVeOLtSsCjOPk4xydHwkQxwJvHVdtJx4CzDDqGbHNHCF/9gpQ 11 | lsa3JZW+tIZLK0XMEPFQ4XFXgegxTStO7kBBPaVIgG9Ooqc2MG4rjMNUpxa28WF1 12 | SyqWTICf2N8T/C+fPzbQLKCWrFrKUP7WQlOaqPNQL9bCDhSTPRTwQOc2/MzVZ9gT 13 | Xr0Z+JMTXwkSMKO52adE1pmKt00jJ1ecZBiJFyjx0X6hH+/59dLbG/7No+PzAgMB 14 | AAEwDQYJKoZIhvcNAQEFBQADggEBAG3UhOCa0EemL2iY+C+PR6CwEHQ+n7vkBzNz 15 | gKOG+Q39spyzqU1qJAzBxLTE81bIQbDg0R8kcLWHVH2y4zViRxZ0jHUFKMgjONW+ 16 | Aj4evic/2Y/LxpLxCajECq/jeMHYrmQONszf9pbc0+exrQpgnwd8asfsM3d/FJS2 17 | 5DIWryCKs/61m9vYL8icWx/9cnfPkBoNv1ER+V1L1TH3ARvABh406SBaeqLTm/kG 18 | MNuKytKWJsQbNlxzWHVgkKzVsBKvYj0uIEJpClIhbe6XNYRDy8T8mKXVWhJuxH4p 19 | /agmCG3nxO8aCrUK/EVmbWmVIfCH3t7jlwMX1nJ8MsRE7Ydnk8I= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /examples/eventlet/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+ 3 | nJVNwUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7Ur 4 | Aozj5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFx 5 | V4HoMU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyg 6 | lqxaylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZ 7 | irdNIydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABAoIBAQCsdq278+0c13d4 8 | tViSh4k5r1w8D9IUdp9XU2/nVgckqA9nOVAvbkJc3FC+P7gsQgbUHKj0XoVbhU1S 9 | q461t8kduPH/oiGhAcKR8WurHEdE0OC6ewhLJAeCMRQwCrAorXXHh7icIt9ClCuG 10 | iSWUcXEy5Cidx3oL3r1xvIbV85fzdDtE9RC1I/kMjAy63S47YGiqh5vYmJkCa8rG 11 | Dsd1sEMDPr63XJpqJj3uHRcPvySgXTa+ssTmUH8WJlPTjvDB5hnPz+lkk2JKVPNu 12 | 8adzftZ6hSun+tsc4ZJp8XhGu/m/7MjxWh8MeupLHlXcOEsnj4uHQQsOM3zHojr3 13 | aDCZiC1pAoGBAOAhwe1ujoS2VJ5RXJ9KMs7eBER/02MDgWZjo54Jv/jFxPWGslKk 14 | QQceuTe+PruRm41nzvk3q4iZXt8pG0bvpgigN2epcVx/O2ouRsUWWBT0JrVlEzha 15 | TIvWjtZ5tSQExXgHL3VlM9+ka40l+NldLSPn25+prizaqhalWuvTpP23AoGBANaY 16 | VhEI6yhp0BBUSATEv9lRgkwx3EbcnXNXPQjDMOthsyfq7FxbdOBEK1rwSDyuE6Ij 17 | zQGcTOfdiur5Ttg0OQilTJIXJAlpoeecOQ9yGma08c5FMXVJJvcZUuWRZWg1ocQj 18 | /hx0WVE9NwOoKwTBERv8HX7vJOFRZyvgkJwFxoulAoGAe4m/1XoZrga9z2GzNs10 19 | AdgX7BW00x+MhH4pIiPnn1yK+nYa9jg4647Asnv3IfXZEnEEgRNxReKbi0+iDFBt 20 | aNW+lDGuHTi37AfD1EBDnpEQgO1MUcRb6rwBkTAWatsCaO00+HUmyX9cFLm4Vz7n 21 | caILyQ6CxZBlLgRIgDHxADMCgYEAtubsJGTHmZBmSCStpXLUWbOBLNQqfTM398DZ 22 | QoirP1PsUQ+IGUfSG/u+QCogR6fPEBkXeFHxsoY/Cvsm2lvYaKgK1VFn46Xm2vNq 23 | JuIH4pZCqp6LAv4weddZslT0a5eaowRSZ4o7PmTAaRuCXvD3VjTSJwhJFMo+90TV 24 | vEWn7gkCgYEAkk+unX9kYmKoUdLh22/tzQekBa8WqMxXDwzBCECTAs2GlpL/f73i 25 | zD15TnaNfLP6Q5RNb0N9tb0Gz1wSkwI1+jGAQLnh2K9X9cIVIqJn8Mf/KQa/wUDV 26 | Tb1j7FoGUEgX7vbsyWuTd8P76kNYyGqCss1XmbttcSolqpbIdlSUcO0= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/fragments/client_https_setup_fragment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Client HTTPS Setup 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | This example code fragment demonstrates how to set up a HTTP/2 client that 7 | negotiates HTTP/2 using NPN and ALPN. For the sake of maximum explanatory value 8 | this code uses the synchronous, low-level sockets API: however, if you're not 9 | using sockets directly (e.g. because you're using asyncio), you should focus on 10 | the set up required for the SSLContext object. For other concurrency libraries 11 | you may need to use other setup (e.g. for Twisted you'll need to use 12 | IProtocolNegotiationFactory). 13 | 14 | This code requires Python 3.5 or later. 15 | """ 16 | import h2.connection 17 | import socket 18 | import ssl 19 | 20 | 21 | def establish_tcp_connection(): 22 | """ 23 | This function establishes a client-side TCP connection. How it works isn't 24 | very important to this example. For the purpose of this example we connect 25 | to localhost. 26 | """ 27 | return socket.create_connection(('localhost', 443)) 28 | 29 | 30 | def get_http2_ssl_context(): 31 | """ 32 | This function creates an SSLContext object that is suitably configured for 33 | HTTP/2. If you're working with Python TLS directly, you'll want to do the 34 | exact same setup as this function does. 35 | """ 36 | # Get the basic context from the standard library. 37 | ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) 38 | 39 | # RFC 7540 Section 9.2: Implementations of HTTP/2 MUST use TLS version 1.2 40 | # or higher. Disable TLS 1.1 and lower. 41 | ctx.options |= ( 42 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 43 | ) 44 | 45 | # RFC 7540 Section 9.2.1: A deployment of HTTP/2 over TLS 1.2 MUST disable 46 | # compression. 47 | ctx.options |= ssl.OP_NO_COMPRESSION 48 | 49 | # RFC 7540 Section 9.2.2: "deployments of HTTP/2 that use TLS 1.2 MUST 50 | # support TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256". In practice, the 51 | # blocklist defined in this section allows only the AES GCM and ChaCha20 52 | # cipher suites with ephemeral key negotiation. 53 | ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20") 54 | 55 | # We want to negotiate using NPN and ALPN. ALPN is mandatory, but NPN may 56 | # be absent, so allow that. This setup allows for negotiation of HTTP/1.1. 57 | ctx.set_alpn_protocols(["h2", "http/1.1"]) 58 | 59 | try: 60 | ctx.set_npn_protocols(["h2", "http/1.1"]) 61 | except NotImplementedError: 62 | pass 63 | 64 | return ctx 65 | 66 | 67 | def negotiate_tls(tcp_conn, context): 68 | """ 69 | Given an established TCP connection and a HTTP/2-appropriate TLS context, 70 | this function: 71 | 72 | 1. wraps TLS around the TCP connection. 73 | 2. confirms that HTTP/2 was negotiated and, if it was not, throws an error. 74 | """ 75 | # Note that SNI is mandatory for HTTP/2, so you *must* pass the 76 | # server_hostname argument. 77 | tls_conn = context.wrap_socket(tcp_conn, server_hostname='localhost') 78 | 79 | # Always prefer the result from ALPN to that from NPN. 80 | # You can only check what protocol was negotiated once the handshake is 81 | # complete. 82 | negotiated_protocol = tls_conn.selected_alpn_protocol() 83 | if negotiated_protocol is None: 84 | negotiated_protocol = tls_conn.selected_npn_protocol() 85 | 86 | if negotiated_protocol != "h2": 87 | raise RuntimeError("Didn't negotiate HTTP/2!") 88 | 89 | return tls_conn 90 | 91 | 92 | def main(): 93 | # Step 1: Set up your TLS context. 94 | context = get_http2_ssl_context() 95 | 96 | # Step 2: Create a TCP connection. 97 | connection = establish_tcp_connection() 98 | 99 | # Step 3: Wrap the connection in TLS and validate that we negotiated HTTP/2 100 | tls_connection = negotiate_tls(connection, context) 101 | 102 | # Step 4: Create a client-side H2 connection. 103 | http2_connection = h2.connection.H2Connection() 104 | 105 | # Step 5: Initiate the connection 106 | http2_connection.initiate_connection() 107 | tls_connection.sendall(http2_connection.data_to_send()) 108 | 109 | # The TCP, TLS, and HTTP/2 handshakes are now complete. You can enter your 110 | # main loop now. 111 | -------------------------------------------------------------------------------- /examples/fragments/client_upgrade_fragment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Client Plaintext Upgrade 4 | ~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | This example code fragment demonstrates how to set up a HTTP/2 client that uses 7 | the plaintext HTTP Upgrade mechanism to negotiate HTTP/2 connectivity. For 8 | maximum explanatory value it uses the synchronous socket API that comes with 9 | the Python standard library. In product code you will want to use an actual 10 | HTTP/1.1 client if possible. 11 | 12 | This code requires Python 3.5 or later. 13 | """ 14 | import h2.connection 15 | import socket 16 | 17 | 18 | def establish_tcp_connection(): 19 | """ 20 | This function establishes a client-side TCP connection. How it works isn't 21 | very important to this example. For the purpose of this example we connect 22 | to localhost. 23 | """ 24 | return socket.create_connection(('localhost', 80)) 25 | 26 | 27 | def send_initial_request(connection, settings): 28 | """ 29 | For the sake of this upgrade demonstration, we're going to issue a GET 30 | request against the root of the site. In principle the best request to 31 | issue for an upgrade is actually ``OPTIONS *``, but this is remarkably 32 | poorly supported and can break in weird ways. 33 | """ 34 | # Craft our initial request per RFC 7540 Section 3.2. This requires two 35 | # special header fields: the Upgrade headre, and the HTTP2-Settings header. 36 | # The value of the HTTP2-Settings header field comes from h2. 37 | request = ( 38 | b"GET / HTTP/1.1\r\n" + 39 | b"Host: localhost\r\n" + 40 | b"Upgrade: h2c\r\n" + 41 | b"HTTP2-Settings: " + settings + b"\r\n" + 42 | b"\r\n" 43 | ) 44 | connection.sendall(request) 45 | 46 | 47 | def get_upgrade_response(connection): 48 | """ 49 | This function reads from the socket until the HTTP/1.1 end-of-headers 50 | sequence (CRLFCRLF) is received. It then checks what the status code of the 51 | response is. 52 | 53 | This is not a substitute for proper HTTP/1.1 parsing, but it's good enough 54 | for example purposes. 55 | """ 56 | data = b'' 57 | while b'\r\n\r\n' not in data: 58 | data += connection.recv(8192) 59 | 60 | headers, rest = data.split(b'\r\n\r\n', 1) 61 | 62 | # An upgrade response begins HTTP/1.1 101 Switching Protocols. Look for the 63 | # code. In production code you should also check that the upgrade is to 64 | # h2c, but here we know we only offered one upgrade so there's only one 65 | # possible upgrade in use. 66 | split_headers = headers.split() 67 | if split_headers[1] != b'101': 68 | raise RuntimeError("Not upgrading!") 69 | 70 | # We don't care about the HTTP/1.1 data anymore, but we do care about 71 | # any other data we read from the socket: this is going to be HTTP/2 data 72 | # that must be passed to the H2Connection. 73 | return rest 74 | 75 | 76 | def main(): 77 | """ 78 | The client upgrade flow. 79 | """ 80 | # Step 1: Establish the TCP connection. 81 | connection = establish_tcp_connection() 82 | 83 | # Step 2: Create H2 Connection object, put it in upgrade mode, and get the 84 | # value of the HTTP2-Settings header we want to use. 85 | h2_connection = h2.connection.H2Connection() 86 | settings_header_value = h2_connection.initiate_upgrade_connection() 87 | 88 | # Step 3: Send the initial HTTP/1.1 request with the upgrade fields. 89 | send_initial_request(connection, settings_header_value) 90 | 91 | # Step 4: Read the HTTP/1.1 response, look for 101 response. 92 | extra_data = get_upgrade_response(connection) 93 | 94 | # Step 5: Immediately send the pending HTTP/2 data. 95 | connection.sendall(h2_connection.data_to_send()) 96 | 97 | # Step 6: Feed the body data to the connection. 98 | events = connection.receive_data(extra_data) 99 | 100 | # Now you can enter your main loop, beginning by processing the first set 101 | # of events above. These events may include ResponseReceived, which will 102 | # contain the response to the request we made in Step 3. 103 | main_loop(events) 104 | -------------------------------------------------------------------------------- /examples/fragments/server_https_setup_fragment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Server HTTPS Setup 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | This example code fragment demonstrates how to set up a HTTP/2 server that 7 | negotiates HTTP/2 using NPN and ALPN. For the sake of maximum explanatory value 8 | this code uses the synchronous, low-level sockets API: however, if you're not 9 | using sockets directly (e.g. because you're using asyncio), you should focus on 10 | the set up required for the SSLContext object. For other concurrency libraries 11 | you may need to use other setup (e.g. for Twisted you'll need to use 12 | IProtocolNegotiationFactory). 13 | 14 | This code requires Python 3.5 or later. 15 | """ 16 | import h2.config 17 | import h2.connection 18 | import socket 19 | import ssl 20 | 21 | 22 | def establish_tcp_connection(): 23 | """ 24 | This function establishes a server-side TCP connection. How it works isn't 25 | very important to this example. 26 | """ 27 | bind_socket = socket.socket() 28 | bind_socket.bind(('', 443)) 29 | bind_socket.listen(5) 30 | return bind_socket.accept()[0] 31 | 32 | 33 | def get_http2_ssl_context(): 34 | """ 35 | This function creates an SSLContext object that is suitably configured for 36 | HTTP/2. If you're working with Python TLS directly, you'll want to do the 37 | exact same setup as this function does. 38 | """ 39 | # Get the basic context from the standard library. 40 | ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) 41 | 42 | # RFC 7540 Section 9.2: Implementations of HTTP/2 MUST use TLS version 1.2 43 | # or higher. Disable TLS 1.1 and lower. 44 | ctx.options |= ( 45 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 46 | ) 47 | 48 | # RFC 7540 Section 9.2.1: A deployment of HTTP/2 over TLS 1.2 MUST disable 49 | # compression. 50 | ctx.options |= ssl.OP_NO_COMPRESSION 51 | 52 | # RFC 7540 Section 9.2.2: "deployments of HTTP/2 that use TLS 1.2 MUST 53 | # support TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256". In practice, the 54 | # blocklist defined in this section allows only the AES GCM and ChaCha20 55 | # cipher suites with ephemeral key negotiation. 56 | ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20") 57 | 58 | # We want to negotiate using NPN and ALPN. ALPN is mandatory, but NPN may 59 | # be absent, so allow that. This setup allows for negotiation of HTTP/1.1. 60 | ctx.set_alpn_protocols(["h2", "http/1.1"]) 61 | 62 | try: 63 | ctx.set_npn_protocols(["h2", "http/1.1"]) 64 | except NotImplementedError: 65 | pass 66 | 67 | return ctx 68 | 69 | 70 | def negotiate_tls(tcp_conn, context): 71 | """ 72 | Given an established TCP connection and a HTTP/2-appropriate TLS context, 73 | this function: 74 | 75 | 1. wraps TLS around the TCP connection. 76 | 2. confirms that HTTP/2 was negotiated and, if it was not, throws an error. 77 | """ 78 | tls_conn = context.wrap_socket(tcp_conn, server_side=True) 79 | 80 | # Always prefer the result from ALPN to that from NPN. 81 | # You can only check what protocol was negotiated once the handshake is 82 | # complete. 83 | negotiated_protocol = tls_conn.selected_alpn_protocol() 84 | if negotiated_protocol is None: 85 | negotiated_protocol = tls_conn.selected_npn_protocol() 86 | 87 | if negotiated_protocol != "h2": 88 | raise RuntimeError("Didn't negotiate HTTP/2!") 89 | 90 | return tls_conn 91 | 92 | 93 | def main(): 94 | # Step 1: Set up your TLS context. 95 | context = get_http2_ssl_context() 96 | 97 | # Step 2: Receive a TCP connection. 98 | connection = establish_tcp_connection() 99 | 100 | # Step 3: Wrap the connection in TLS and validate that we negotiated HTTP/2 101 | tls_connection = negotiate_tls(connection, context) 102 | 103 | # Step 4: Create a server-side H2 connection. 104 | config = h2.config.H2Configuration(client_side=False) 105 | http2_connection = h2.connection.H2Connection(config=config) 106 | 107 | # Step 5: Initiate the connection 108 | http2_connection.initiate_connection() 109 | tls_connection.sendall(http2_connection.data_to_send()) 110 | 111 | # The TCP, TLS, and HTTP/2 handshakes are now complete. You can enter your 112 | # main loop now. 113 | -------------------------------------------------------------------------------- /examples/fragments/server_upgrade_fragment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Server Plaintext Upgrade 4 | ~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | This example code fragment demonstrates how to set up a HTTP/2 server that uses 7 | the plaintext HTTP Upgrade mechanism to negotiate HTTP/2 connectivity. For 8 | maximum explanatory value it uses the synchronous socket API that comes with 9 | the Python standard library. In product code you will want to use an actual 10 | HTTP/1.1 server library if possible. 11 | 12 | This code requires Python 3.5 or later. 13 | """ 14 | import h2.config 15 | import h2.connection 16 | import re 17 | import socket 18 | 19 | 20 | def establish_tcp_connection(): 21 | """ 22 | This function establishes a server-side TCP connection. How it works isn't 23 | very important to this example. 24 | """ 25 | bind_socket = socket.socket() 26 | bind_socket.bind(('', 443)) 27 | bind_socket.listen(5) 28 | return bind_socket.accept()[0] 29 | 30 | 31 | def receive_initial_request(connection): 32 | """ 33 | We're going to receive a request. For the sake of this example, we're going 34 | to assume that the first request has no body. If it doesn't have the 35 | Upgrade: h2c header field and the HTTP2-Settings header field, we'll throw 36 | errors. 37 | 38 | In production code, you should use a proper HTTP/1.1 parser and actually 39 | serve HTTP/1.1 requests! 40 | 41 | Returns the value of the HTTP2-Settings header field. 42 | """ 43 | data = b'' 44 | while not data.endswith(b'\r\n\r\n'): 45 | data += connection.recv(8192) 46 | 47 | match = re.search(b'Upgrade: h2c\r\n', data) 48 | if match is None: 49 | raise RuntimeError("HTTP/2 upgrade not requested!") 50 | 51 | # We need to look for the HTTP2-Settings header field. Again, in production 52 | # code you shouldn't use regular expressions for this, but it's good enough 53 | # for the example. 54 | match = re.search(b'HTTP2-Settings: (\\S+)\r\n', data) 55 | if match is None: 56 | raise RuntimeError("HTTP2-Settings header field not present!") 57 | 58 | return match.group(1) 59 | 60 | 61 | def send_upgrade_response(connection): 62 | """ 63 | This function writes the 101 Switching Protocols response. 64 | """ 65 | response = ( 66 | b"HTTP/1.1 101 Switching Protocols\r\n" 67 | b"Upgrade: h2c\r\n" 68 | b"\r\n" 69 | ) 70 | connection.sendall(response) 71 | 72 | 73 | def main(): 74 | """ 75 | The server upgrade flow. 76 | """ 77 | # Step 1: Establish the TCP connection. 78 | connection = establish_tcp_connection() 79 | 80 | # Step 2: Read the response. We expect this to request an upgrade. 81 | settings_header_value = receive_initial_request(connection) 82 | 83 | # Step 3: Create a H2Connection object in server mode, and pass it the 84 | # value of the HTTP2-Settings header field. 85 | config = h2.config.H2Configuration(client_side=False) 86 | h2_connection = h2.connection.H2Connection(config=config) 87 | h2_connection.initiate_upgrade_connection( 88 | settings_header=settings_header_value 89 | ) 90 | 91 | # Step 4: Send the 101 Switching Protocols response. 92 | send_upgrade_response(connection) 93 | 94 | # Step 5: Send pending HTTP/2 data. 95 | connection.sendall(h2_connection.data_to_send()) 96 | 97 | # At this point, you can enter your main loop. The first step has to be to 98 | # send the response to the initial HTTP/1.1 request you received on stream 99 | # 1. 100 | main_loop() 101 | -------------------------------------------------------------------------------- /examples/gevent/gevent-server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | gevent-server.py 4 | ================ 5 | 6 | A simple HTTP/2 server written for gevent serving static files from a directory specified as input. 7 | If no directory is provided, the current directory will be used. 8 | """ 9 | import mimetypes 10 | import sys 11 | from functools import partial 12 | from pathlib import Path 13 | from typing import Tuple, Dict, Optional 14 | 15 | from gevent import socket, ssl 16 | from gevent.event import Event 17 | from gevent.server import StreamServer 18 | from h2 import events 19 | from h2.config import H2Configuration 20 | from h2.connection import H2Connection 21 | 22 | 23 | def get_http2_tls_context() -> ssl.SSLContext: 24 | ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) 25 | ctx.options |= ( 26 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 27 | ) 28 | 29 | ctx.options |= ssl.OP_NO_COMPRESSION 30 | ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20') 31 | ctx.load_cert_chain(certfile='localhost.crt', keyfile='localhost.key') 32 | ctx.set_alpn_protocols(['h2']) 33 | try: 34 | ctx.set_npn_protocols(['h2']) 35 | except NotImplementedError: 36 | pass 37 | 38 | return ctx 39 | 40 | 41 | class H2Worker: 42 | 43 | def __init__(self, sock: socket, address: Tuple[str, str], source_dir: str = None): 44 | self._sock = sock 45 | self._address = address 46 | self._flow_control_events: Dict[int, Event] = {} 47 | self._server_name = 'gevent-h2' 48 | self._connection: Optional[H2Connection] = None 49 | self._read_chunk_size = 8192 # The maximum amount of a file we'll send in a single DATA frame 50 | 51 | self._check_sources_dir(source_dir) 52 | self._sources_dir = source_dir 53 | 54 | self._run() 55 | 56 | def _initiate_connection(self): 57 | config = H2Configuration(client_side=False, header_encoding='utf-8') 58 | self._connection = H2Connection(config=config) 59 | self._connection.initiate_connection() 60 | self._sock.sendall(self._connection.data_to_send()) 61 | 62 | @staticmethod 63 | def _check_sources_dir(sources_dir: str) -> None: 64 | p = Path(sources_dir) 65 | if not p.is_dir(): 66 | raise NotADirectoryError(f'{sources_dir} does not exists') 67 | 68 | def _send_error_response(self, status_code: str, event: events.RequestReceived) -> None: 69 | self._connection.send_headers( 70 | stream_id=event.stream_id, 71 | headers=[ 72 | (':status', status_code), 73 | ('content-length', '0'), 74 | ('server', self._server_name), 75 | ], 76 | end_stream=True 77 | ) 78 | self._sock.sendall(self._connection.data_to_send()) 79 | 80 | def _handle_request(self, event: events.RequestReceived) -> None: 81 | headers = dict(event.headers) 82 | if headers[':method'] != 'GET': 83 | self._send_error_response('405', event) 84 | return 85 | 86 | file_path = Path(self._sources_dir) / headers[':path'].lstrip('/') 87 | if not file_path.is_file(): 88 | self._send_error_response('404', event) 89 | return 90 | 91 | self._send_file(file_path, event.stream_id) 92 | 93 | def _send_file(self, file_path: Path, stream_id: int) -> None: 94 | """ 95 | Send a file, obeying the rules of HTTP/2 flow control. 96 | """ 97 | file_size = file_path.stat().st_size 98 | content_type, content_encoding = mimetypes.guess_type(str(file_path)) 99 | response_headers = [ 100 | (':status', '200'), 101 | ('content-length', str(file_size)), 102 | ('server', self._server_name) 103 | ] 104 | if content_type: 105 | response_headers.append(('content-type', content_type)) 106 | if content_encoding: 107 | response_headers.append(('content-encoding', content_encoding)) 108 | 109 | self._connection.send_headers(stream_id, response_headers) 110 | self._sock.sendall(self._connection.data_to_send()) 111 | 112 | with file_path.open(mode='rb', buffering=0) as f: 113 | self._send_file_data(f, stream_id) 114 | 115 | def _send_file_data(self, file_obj, stream_id: int) -> None: 116 | """ 117 | Send the data portion of a file. Handles flow control rules. 118 | """ 119 | while True: 120 | while self._connection.local_flow_control_window(stream_id) < 1: 121 | self._wait_for_flow_control(stream_id) 122 | 123 | chunk_size = min(self._connection.local_flow_control_window(stream_id), self._read_chunk_size) 124 | data = file_obj.read(chunk_size) 125 | keep_reading = (len(data) == chunk_size) 126 | 127 | self._connection.send_data(stream_id, data, not keep_reading) 128 | self._sock.sendall(self._connection.data_to_send()) 129 | 130 | if not keep_reading: 131 | break 132 | 133 | def _wait_for_flow_control(self, stream_id: int) -> None: 134 | """ 135 | Blocks until the flow control window for a given stream is opened. 136 | """ 137 | event = Event() 138 | self._flow_control_events[stream_id] = event 139 | event.wait() 140 | 141 | def _handle_window_update(self, event: events.WindowUpdated) -> None: 142 | """ 143 | Unblock streams waiting on flow control, if needed. 144 | """ 145 | stream_id = event.stream_id 146 | 147 | if stream_id and stream_id in self._flow_control_events: 148 | g_event = self._flow_control_events.pop(stream_id) 149 | g_event.set() 150 | elif not stream_id: 151 | # Need to keep a real list here to use only the events present at this time. 152 | blocked_streams = list(self._flow_control_events.keys()) 153 | for stream_id in blocked_streams: 154 | g_event = self._flow_control_events.pop(stream_id) 155 | g_event.set() 156 | 157 | def _run(self) -> None: 158 | self._initiate_connection() 159 | 160 | while True: 161 | data = self._sock.recv(65535) 162 | if not data: 163 | break 164 | 165 | h2_events = self._connection.receive_data(data) 166 | for event in h2_events: 167 | if isinstance(event, events.RequestReceived): 168 | self._handle_request(event) 169 | elif isinstance(event, events.DataReceived): 170 | self._connection.reset_stream(event.stream_id) 171 | elif isinstance(event, events.WindowUpdated): 172 | self._handle_window_update(event) 173 | 174 | data_to_send = self._connection.data_to_send() 175 | if data_to_send: 176 | self._sock.sendall(data_to_send) 177 | 178 | 179 | if __name__ == '__main__': 180 | files_dir = sys.argv[1] if len(sys.argv) > 1 else f'{Path().cwd()}' 181 | server = StreamServer(('127.0.0.1', 8080), partial(H2Worker, source_dir=files_dir), 182 | ssl_context=get_http2_tls_context()) 183 | try: 184 | server.serve_forever() 185 | except KeyboardInterrupt: 186 | server.close() 187 | -------------------------------------------------------------------------------- /examples/gevent/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDVDCCAjwCCQC1RoAIsDX89zANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJG 3 | UjEWMBQGA1UECAwNSWxlLWRlLUZyYW5jZTEOMAwGA1UEBwwFUGFyaXMxITAfBgNV 4 | BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0 5 | MB4XDTE5MDgzMDAzNDcwN1oXDTIwMDgyOTAzNDcwN1owbDELMAkGA1UEBhMCRlIx 6 | FjAUBgNVBAgMDUlsZS1kZS1GcmFuY2UxDjAMBgNVBAcMBVBhcmlzMSEwHwYDVQQK 7 | DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC 8 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANexqhW+b7Mx2lj5csv7Uq7T 9 | St6RWEocEUtuwBUsQ3VGOUTjN0dRxGeeZdDvZYrVsKhMwT1EeoXYlleQJPSEgW1d 10 | E5Mx6t4okWtG5D8YbIDMLRqLMpOqGSH1VZDU6l9ZN2UCTBtIVZN+Mg/q36cQOkwE 11 | Rp+BBiOU4dgKKms5d5QfOi3wPNEdeU0z77qxOuI9WmnMxvxM+vySkt2mHV/ptW4w 12 | XZDZ9QC/IHhXhjkSlQuL/TUptJ2UtlEXtn5NcNAWROl7xTMVHfIiFVW4oW39KIrA 13 | zGH5qYlJG/gUhJBTU7K5N5J1c/Y3FyNIeOgHMQr6MnKugV7JyOZYxjXGgdXOUNMC 14 | AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUuGdTOWJ0SspGs6zU50lt+GxXo5xO8HE 15 | y7RiOnorJVE23dgVAyaDDcnIfsF+usobACHRlqirn2jvY5A7TIEM7Wlw5c1zrCAW 16 | bDzPJS0h6Hub05WQQmU734Ro1et7WFUrFRlDQUHJADgEAXRhXlm2Uk7Ay1PlCIz9 17 | 802neyhWErL6bb7zxV5fWTu7RA3XUAX2ZrYO3XLhcsK7SnpT0whoBI06c1GxDkgV 18 | wFeYv8PBH5iQWhIft8EeTrNvNa+dusnppuM21sWWzItRnFef+ATixdDGIpDitAfs 19 | cfIUalb3BBdn4PXamN7EhWzHJlcME1cerjQ4YoEynWmT2blk1XUg9A== 20 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /examples/gevent/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDXsaoVvm+zMdpY 3 | +XLL+1Ku00rekVhKHBFLbsAVLEN1RjlE4zdHUcRnnmXQ72WK1bCoTME9RHqF2JZX 4 | kCT0hIFtXROTMereKJFrRuQ/GGyAzC0aizKTqhkh9VWQ1OpfWTdlAkwbSFWTfjIP 5 | 6t+nEDpMBEafgQYjlOHYCiprOXeUHzot8DzRHXlNM++6sTriPVppzMb8TPr8kpLd 6 | ph1f6bVuMF2Q2fUAvyB4V4Y5EpULi/01KbSdlLZRF7Z+TXDQFkTpe8UzFR3yIhVV 7 | uKFt/SiKwMxh+amJSRv4FISQU1OyuTeSdXP2NxcjSHjoBzEK+jJyroFeycjmWMY1 8 | xoHVzlDTAgMBAAECggEBAKqtYIxyNAtVMJVlVmyJBAVpFv6FfpquGRVans5iR0L+ 9 | fYTAU1axIjxoP+MT/ILe0zpp+iNUE6vkFhtV6Zg/Xfc/RqUcQ+DlsyRzZVt0JS/J 10 | 4Qr3CN+GIvsXGk1P3eHzQ/0+0yBnnafnnQ+xaKbXFXpfi87dlxEC169PY/+S6see 11 | dcPYw8LB8rI+mIIPvM/V2VtobZb9BFUsN4Dq8H1tRR97ST4TRbwov66o17Fvn5ww 12 | mXUwHjhdgxaJLtxJwppMhhSuST64mwoNY8XNWE0PlaB5jxKCNuWYYurjEJLJcBa7 13 | 3lYadsRTucoiRbsTcqpivsa3KWxNRJcZERWVN4LVQvkCgYEA+E5zgVHbKfjCGs9x 14 | Xv1uVLjpdsh2S/7Zkx95vc4rBoLe8ii1uSpcLHytB7F5bPZV3Tiivu8OpW3E+8n6 15 | 8mxQHSomSioxTE+xXF4JMf5XY2l9Tvz/60mo/dxk9Yo9k79OIJDUJrnpc18iYtRp 16 | B3X2g7JfqpT/RbG0bs2YVa/R3z8CgYEA3mCIm0Y0b6mpTxQNk6fcH2YXe1mdRp5Y 17 | 9O+wVTNwmwB2vZZVm/K/wmOstsO1P+/PzwJ/WcBV9TlLlZRxxs/yNNU8NM7+7l3O 18 | e0ucCNRqhi9P19SA7l18FroOtOv2DatvXpNJTM6fXgE7dPQm5NKrNKK6nv03OUzQ 19 | BLLBBtzv/W0CgYEA6LU9cvkwGQnVgCLh8VA6UpRp2LTOiTJy3nslMUlC8Xs9Tl3w 20 | 0XRtphPCZe9iCUhj+EvX2nFYnJlff0owMXppKqwR7nfUc9xMMHDA1WW0qKp4kcpy 21 | XiROiHxA8g144DruEX8qFJEvxLxoEY9YT3GycoJ9PfUduEdu/lkYZ1W7rykCgYB0 22 | mw/mw9hpGQDzu2MnIuUU/dagUqxaxFuHDExdUNziGkspPLRlUtPknZmKOHNJNHm2 23 | ZevbZzRrowCUTcOfaZjqxUmNs2EQItZL5qjKJIA7HoHyfbahxxlzXVqq2fQq1NNQ 24 | N1E/WjVM+L5xpDjk0eb+cboD9mlHvZRycj0vWRjqvQKBgQDP1xotkMqYwnMnpoRQ 25 | e3bqhIQFeoqIehsl2e/F+5yZ43plvEXavxshNJS3gGr1MflexcjqEaiHdfdK84zL 26 | mJuPYn0Uz5fP336aepzVsxK78tW4ilZri2mPbpBYgJskc6Ud7ue5zhBtuQnu5rV4 27 | zcuL1QjSQA+KW88b4DU/Usp6Eg== 28 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /examples/plain_sockets/plain_sockets_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | plain_sockets_client.py 5 | ~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Just enough code to send a GET request via h2 to an HTTP/2 server and receive a response body. 8 | This is *not* a complete production-ready HTTP/2 client! 9 | """ 10 | 11 | import socket 12 | import ssl 13 | import certifi 14 | 15 | import h2.connection 16 | import h2.events 17 | 18 | 19 | SERVER_NAME = 'http2.golang.org' 20 | SERVER_PORT = 443 21 | 22 | # generic socket and ssl configuration 23 | socket.setdefaulttimeout(15) 24 | ctx = ssl.create_default_context(cafile=certifi.where()) 25 | ctx.set_alpn_protocols(['h2']) 26 | 27 | # open a socket to the server and initiate TLS/SSL 28 | s = socket.create_connection((SERVER_NAME, SERVER_PORT)) 29 | s = ctx.wrap_socket(s, server_hostname=SERVER_NAME) 30 | 31 | c = h2.connection.H2Connection() 32 | c.initiate_connection() 33 | s.sendall(c.data_to_send()) 34 | 35 | headers = [ 36 | (':method', 'GET'), 37 | (':path', '/reqinfo'), 38 | (':authority', SERVER_NAME), 39 | (':scheme', 'https'), 40 | ] 41 | c.send_headers(1, headers, end_stream=True) 42 | s.sendall(c.data_to_send()) 43 | 44 | body = b'' 45 | response_stream_ended = False 46 | while not response_stream_ended: 47 | # read raw data from the socket 48 | data = s.recv(65536 * 1024) 49 | if not data: 50 | break 51 | 52 | # feed raw data into h2, and process resulting events 53 | events = c.receive_data(data) 54 | for event in events: 55 | print(event) 56 | if isinstance(event, h2.events.DataReceived): 57 | # update flow control so the server doesn't starve us 58 | c.acknowledge_received_data(event.flow_controlled_length, event.stream_id) 59 | # more response body data received 60 | body += event.data 61 | if isinstance(event, h2.events.StreamEnded): 62 | # response body completed, let's exit the loop 63 | response_stream_ended = True 64 | break 65 | # send any pending data to the server 66 | s.sendall(c.data_to_send()) 67 | 68 | print("Response fully received:") 69 | print(body.decode()) 70 | 71 | # tell the server we are closing the h2 connection 72 | c.close_connection() 73 | s.sendall(c.data_to_send()) 74 | 75 | # close the socket 76 | s.close() 77 | -------------------------------------------------------------------------------- /examples/tornado/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDUjCCAjoCCQCQmNzzpQTCijANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQGEwJH 3 | QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5 4 | cGVyLWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20w 5 | HhcNMTUwOTE2MjAyOTA0WhcNMTYwOTE1MjAyOTA0WjBrMQswCQYDVQQGEwJHQjEP 6 | MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5cGVy 7 | LWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEi 8 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74ZeB4Jdb5cnC9KXXLJuzjwTg 9 | 45q5EeShDYQe0TbKgreiUP6clU3BR0fFAVedN1q/LOuQ1HhvrDk1l4TfGF2bpCIq 10 | K+U9CnzcQknvdpyyVeOLtSsCjOPk4xydHwkQxwJvHVdtJx4CzDDqGbHNHCF/9gpQ 11 | lsa3JZW+tIZLK0XMEPFQ4XFXgegxTStO7kBBPaVIgG9Ooqc2MG4rjMNUpxa28WF1 12 | SyqWTICf2N8T/C+fPzbQLKCWrFrKUP7WQlOaqPNQL9bCDhSTPRTwQOc2/MzVZ9gT 13 | Xr0Z+JMTXwkSMKO52adE1pmKt00jJ1ecZBiJFyjx0X6hH+/59dLbG/7No+PzAgMB 14 | AAEwDQYJKoZIhvcNAQEFBQADggEBAG3UhOCa0EemL2iY+C+PR6CwEHQ+n7vkBzNz 15 | gKOG+Q39spyzqU1qJAzBxLTE81bIQbDg0R8kcLWHVH2y4zViRxZ0jHUFKMgjONW+ 16 | Aj4evic/2Y/LxpLxCajECq/jeMHYrmQONszf9pbc0+exrQpgnwd8asfsM3d/FJS2 17 | 5DIWryCKs/61m9vYL8icWx/9cnfPkBoNv1ER+V1L1TH3ARvABh406SBaeqLTm/kG 18 | MNuKytKWJsQbNlxzWHVgkKzVsBKvYj0uIEJpClIhbe6XNYRDy8T8mKXVWhJuxH4p 19 | /agmCG3nxO8aCrUK/EVmbWmVIfCH3t7jlwMX1nJ8MsRE7Ydnk8I= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /examples/tornado/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+ 3 | nJVNwUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7Ur 4 | Aozj5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFx 5 | V4HoMU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyg 6 | lqxaylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZ 7 | irdNIydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABAoIBAQCsdq278+0c13d4 8 | tViSh4k5r1w8D9IUdp9XU2/nVgckqA9nOVAvbkJc3FC+P7gsQgbUHKj0XoVbhU1S 9 | q461t8kduPH/oiGhAcKR8WurHEdE0OC6ewhLJAeCMRQwCrAorXXHh7icIt9ClCuG 10 | iSWUcXEy5Cidx3oL3r1xvIbV85fzdDtE9RC1I/kMjAy63S47YGiqh5vYmJkCa8rG 11 | Dsd1sEMDPr63XJpqJj3uHRcPvySgXTa+ssTmUH8WJlPTjvDB5hnPz+lkk2JKVPNu 12 | 8adzftZ6hSun+tsc4ZJp8XhGu/m/7MjxWh8MeupLHlXcOEsnj4uHQQsOM3zHojr3 13 | aDCZiC1pAoGBAOAhwe1ujoS2VJ5RXJ9KMs7eBER/02MDgWZjo54Jv/jFxPWGslKk 14 | QQceuTe+PruRm41nzvk3q4iZXt8pG0bvpgigN2epcVx/O2ouRsUWWBT0JrVlEzha 15 | TIvWjtZ5tSQExXgHL3VlM9+ka40l+NldLSPn25+prizaqhalWuvTpP23AoGBANaY 16 | VhEI6yhp0BBUSATEv9lRgkwx3EbcnXNXPQjDMOthsyfq7FxbdOBEK1rwSDyuE6Ij 17 | zQGcTOfdiur5Ttg0OQilTJIXJAlpoeecOQ9yGma08c5FMXVJJvcZUuWRZWg1ocQj 18 | /hx0WVE9NwOoKwTBERv8HX7vJOFRZyvgkJwFxoulAoGAe4m/1XoZrga9z2GzNs10 19 | AdgX7BW00x+MhH4pIiPnn1yK+nYa9jg4647Asnv3IfXZEnEEgRNxReKbi0+iDFBt 20 | aNW+lDGuHTi37AfD1EBDnpEQgO1MUcRb6rwBkTAWatsCaO00+HUmyX9cFLm4Vz7n 21 | caILyQ6CxZBlLgRIgDHxADMCgYEAtubsJGTHmZBmSCStpXLUWbOBLNQqfTM398DZ 22 | QoirP1PsUQ+IGUfSG/u+QCogR6fPEBkXeFHxsoY/Cvsm2lvYaKgK1VFn46Xm2vNq 23 | JuIH4pZCqp6LAv4weddZslT0a5eaowRSZ4o7PmTAaRuCXvD3VjTSJwhJFMo+90TV 24 | vEWn7gkCgYEAkk+unX9kYmKoUdLh22/tzQekBa8WqMxXDwzBCECTAs2GlpL/f73i 25 | zD15TnaNfLP6Q5RNb0N9tb0Gz1wSkwI1+jGAQLnh2K9X9cIVIqJn8Mf/KQa/wUDV 26 | Tb1j7FoGUEgX7vbsyWuTd8P76kNYyGqCss1XmbttcSolqpbIdlSUcO0= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/tornado/tornado-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | tornado-server.py 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | A fully-functional HTTP/2 server written for Tornado. 8 | """ 9 | import collections 10 | import json 11 | import ssl 12 | 13 | import tornado.gen 14 | import tornado.ioloop 15 | import tornado.iostream 16 | import tornado.tcpserver 17 | 18 | from h2.config import H2Configuration 19 | from h2.connection import H2Connection 20 | from h2.events import RequestReceived, DataReceived 21 | 22 | 23 | def create_ssl_context(certfile, keyfile): 24 | ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 25 | ssl_context.options |= ( 26 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION 27 | ) 28 | ssl_context.set_ciphers("ECDHE+AESGCM") 29 | ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) 30 | ssl_context.set_alpn_protocols(["h2"]) 31 | return ssl_context 32 | 33 | 34 | class H2Server(tornado.tcpserver.TCPServer): 35 | 36 | @tornado.gen.coroutine 37 | def handle_stream(self, stream, address): 38 | handler = EchoHeadersHandler(stream) 39 | yield handler.handle() 40 | 41 | 42 | class EchoHeadersHandler(object): 43 | 44 | def __init__(self, stream): 45 | self.stream = stream 46 | 47 | config = H2Configuration(client_side=False) 48 | self.conn = H2Connection(config=config) 49 | 50 | @tornado.gen.coroutine 51 | def handle(self): 52 | self.conn.initiate_connection() 53 | yield self.stream.write(self.conn.data_to_send()) 54 | 55 | while True: 56 | try: 57 | data = yield self.stream.read_bytes(65535, partial=True) 58 | if not data: 59 | break 60 | 61 | events = self.conn.receive_data(data) 62 | for event in events: 63 | if isinstance(event, RequestReceived): 64 | self.request_received(event.headers, event.stream_id) 65 | elif isinstance(event, DataReceived): 66 | self.conn.reset_stream(event.stream_id) 67 | 68 | yield self.stream.write(self.conn.data_to_send()) 69 | 70 | except tornado.iostream.StreamClosedError: 71 | break 72 | 73 | def request_received(self, headers, stream_id): 74 | headers = collections.OrderedDict(headers) 75 | data = json.dumps({'headers': headers}, indent=4).encode('utf-8') 76 | 77 | response_headers = ( 78 | (':status', '200'), 79 | ('content-type', 'application/json'), 80 | ('content-length', str(len(data))), 81 | ('server', 'tornado-h2'), 82 | ) 83 | self.conn.send_headers(stream_id, response_headers) 84 | self.conn.send_data(stream_id, data, end_stream=True) 85 | 86 | 87 | if __name__ == '__main__': 88 | ssl_context = create_ssl_context('server.crt', 'server.key') 89 | server = H2Server(ssl_options=ssl_context) 90 | server.listen(8888) 91 | io_loop = tornado.ioloop.IOLoop.current() 92 | io_loop.start() 93 | -------------------------------------------------------------------------------- /examples/twisted/head_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | head_request.py 4 | ~~~~~~~~~~~~~~~ 5 | 6 | A short example that demonstrates a client that makes HEAD requests to certain 7 | websites. 8 | 9 | This example is intended as a reproduction of nghttp2 issue 396, for the 10 | purposes of compatibility testing. 11 | """ 12 | from __future__ import print_function 13 | 14 | from twisted.internet import reactor 15 | from twisted.internet.endpoints import connectProtocol, SSL4ClientEndpoint 16 | from twisted.internet.protocol import Protocol 17 | from twisted.internet.ssl import optionsForClientTLS 18 | from hyperframe.frame import SettingsFrame 19 | from h2.connection import H2Connection 20 | from h2.events import ( 21 | ResponseReceived, DataReceived, StreamEnded, 22 | StreamReset, SettingsAcknowledged, 23 | ) 24 | 25 | 26 | AUTHORITY = u'nghttp2.org' 27 | PATH = '/httpbin/' 28 | SIZE = 4096 29 | 30 | 31 | class H2Protocol(Protocol): 32 | def __init__(self): 33 | self.conn = H2Connection() 34 | self.known_proto = None 35 | self.request_made = False 36 | 37 | def connectionMade(self): 38 | self.conn.initiate_connection() 39 | 40 | # This reproduces the error in #396, by changing the header table size. 41 | self.conn.update_settings({SettingsFrame.HEADER_TABLE_SIZE: SIZE}) 42 | 43 | self.transport.write(self.conn.data_to_send()) 44 | 45 | def dataReceived(self, data): 46 | if not self.known_proto: 47 | self.known_proto = self.transport.negotiatedProtocol 48 | assert self.known_proto == b'h2' 49 | 50 | events = self.conn.receive_data(data) 51 | 52 | for event in events: 53 | if isinstance(event, ResponseReceived): 54 | self.handleResponse(event.headers, event.stream_id) 55 | elif isinstance(event, DataReceived): 56 | self.handleData(event.data, event.stream_id) 57 | elif isinstance(event, StreamEnded): 58 | self.endStream(event.stream_id) 59 | elif isinstance(event, SettingsAcknowledged): 60 | self.settingsAcked(event) 61 | elif isinstance(event, StreamReset): 62 | reactor.stop() 63 | raise RuntimeError("Stream reset: %d" % event.error_code) 64 | else: 65 | print(event) 66 | 67 | data = self.conn.data_to_send() 68 | if data: 69 | self.transport.write(data) 70 | 71 | def settingsAcked(self, event): 72 | # Having received the remote settings change, lets send our request. 73 | if not self.request_made: 74 | self.sendRequest() 75 | 76 | def handleResponse(self, response_headers, stream_id): 77 | for name, value in response_headers: 78 | print("%s: %s" % (name.decode('utf-8'), value.decode('utf-8'))) 79 | 80 | print("") 81 | 82 | def handleData(self, data, stream_id): 83 | print(data, end='') 84 | 85 | def endStream(self, stream_id): 86 | self.conn.close_connection() 87 | self.transport.write(self.conn.data_to_send()) 88 | self.transport.loseConnection() 89 | reactor.stop() 90 | 91 | def sendRequest(self): 92 | request_headers = [ 93 | (':method', 'HEAD'), 94 | (':authority', AUTHORITY), 95 | (':scheme', 'https'), 96 | (':path', PATH), 97 | ] 98 | self.conn.send_headers(1, request_headers, end_stream=True) 99 | self.request_made = True 100 | 101 | options = optionsForClientTLS( 102 | hostname=AUTHORITY, 103 | acceptableProtocols=[b'h2'], 104 | ) 105 | 106 | connectProtocol( 107 | SSL4ClientEndpoint(reactor, AUTHORITY, 443, options), 108 | H2Protocol() 109 | ) 110 | reactor.run() 111 | -------------------------------------------------------------------------------- /examples/twisted/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDUjCCAjoCCQCQmNzzpQTCijANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQGEwJH 3 | QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5 4 | cGVyLWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20w 5 | HhcNMTUwOTE2MjAyOTA0WhcNMTYwOTE1MjAyOTA0WjBrMQswCQYDVQQGEwJHQjEP 6 | MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5cGVy 7 | LWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEi 8 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74ZeB4Jdb5cnC9KXXLJuzjwTg 9 | 45q5EeShDYQe0TbKgreiUP6clU3BR0fFAVedN1q/LOuQ1HhvrDk1l4TfGF2bpCIq 10 | K+U9CnzcQknvdpyyVeOLtSsCjOPk4xydHwkQxwJvHVdtJx4CzDDqGbHNHCF/9gpQ 11 | lsa3JZW+tIZLK0XMEPFQ4XFXgegxTStO7kBBPaVIgG9Ooqc2MG4rjMNUpxa28WF1 12 | SyqWTICf2N8T/C+fPzbQLKCWrFrKUP7WQlOaqPNQL9bCDhSTPRTwQOc2/MzVZ9gT 13 | Xr0Z+JMTXwkSMKO52adE1pmKt00jJ1ecZBiJFyjx0X6hH+/59dLbG/7No+PzAgMB 14 | AAEwDQYJKoZIhvcNAQEFBQADggEBAG3UhOCa0EemL2iY+C+PR6CwEHQ+n7vkBzNz 15 | gKOG+Q39spyzqU1qJAzBxLTE81bIQbDg0R8kcLWHVH2y4zViRxZ0jHUFKMgjONW+ 16 | Aj4evic/2Y/LxpLxCajECq/jeMHYrmQONszf9pbc0+exrQpgnwd8asfsM3d/FJS2 17 | 5DIWryCKs/61m9vYL8icWx/9cnfPkBoNv1ER+V1L1TH3ARvABh406SBaeqLTm/kG 18 | MNuKytKWJsQbNlxzWHVgkKzVsBKvYj0uIEJpClIhbe6XNYRDy8T8mKXVWhJuxH4p 19 | /agmCG3nxO8aCrUK/EVmbWmVIfCH3t7jlwMX1nJ8MsRE7Ydnk8I= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /examples/twisted/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICsDCCAZgCAQAwazELMAkGA1UEBhMCR0IxDzANBgNVBAgTBkxvbmRvbjEPMA0G 3 | A1UEBxMGTG9uZG9uMREwDwYDVQQKEwhoeXBlci1oMjERMA8GA1UECxMIaHlwZXkt 4 | aDIxFDASBgNVBAMTC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 5 | MIIBCgKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+nJVN 6 | wUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7UrAozj 7 | 5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFxV4Ho 8 | MU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyglqxa 9 | ylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZirdN 10 | IydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABoAAwDQYJKoZIhvcNAQEFBQAD 11 | ggEBACZpSoZWxHU5uagpM2Vinh2E7CXiMAlBc6NXhQMD/3fycr9sX4d/+y9Gy3bL 12 | OfEOHBPlQVGrt05aiTh7m5s3HQfsH8l3RfKpfzCfoqd2ESVwgB092bJwY9fBnkw/ 13 | UzIHvSnlaKc78h+POUoATOb4faQ8P04wzJHzckbCDI8zRzBZTMVGuiWUopq7K5Ce 14 | VSesbqHHnW9ob/apigKNE0k7et/28NOXNEP90tTsz98yN3TP+Nv9puwvT9JZOOoG 15 | 0PZIQKJIaZ1NZoNQHLN9gXz012XWa99cBE0qNiBUugXlNhXjkIIM8FIhDQOREB18 16 | 0KDxEma+A0quyjnDMwPSoZsMca4= 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /examples/twisted/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+ 3 | nJVNwUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7Ur 4 | Aozj5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFx 5 | V4HoMU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyg 6 | lqxaylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZ 7 | irdNIydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABAoIBAQCsdq278+0c13d4 8 | tViSh4k5r1w8D9IUdp9XU2/nVgckqA9nOVAvbkJc3FC+P7gsQgbUHKj0XoVbhU1S 9 | q461t8kduPH/oiGhAcKR8WurHEdE0OC6ewhLJAeCMRQwCrAorXXHh7icIt9ClCuG 10 | iSWUcXEy5Cidx3oL3r1xvIbV85fzdDtE9RC1I/kMjAy63S47YGiqh5vYmJkCa8rG 11 | Dsd1sEMDPr63XJpqJj3uHRcPvySgXTa+ssTmUH8WJlPTjvDB5hnPz+lkk2JKVPNu 12 | 8adzftZ6hSun+tsc4ZJp8XhGu/m/7MjxWh8MeupLHlXcOEsnj4uHQQsOM3zHojr3 13 | aDCZiC1pAoGBAOAhwe1ujoS2VJ5RXJ9KMs7eBER/02MDgWZjo54Jv/jFxPWGslKk 14 | QQceuTe+PruRm41nzvk3q4iZXt8pG0bvpgigN2epcVx/O2ouRsUWWBT0JrVlEzha 15 | TIvWjtZ5tSQExXgHL3VlM9+ka40l+NldLSPn25+prizaqhalWuvTpP23AoGBANaY 16 | VhEI6yhp0BBUSATEv9lRgkwx3EbcnXNXPQjDMOthsyfq7FxbdOBEK1rwSDyuE6Ij 17 | zQGcTOfdiur5Ttg0OQilTJIXJAlpoeecOQ9yGma08c5FMXVJJvcZUuWRZWg1ocQj 18 | /hx0WVE9NwOoKwTBERv8HX7vJOFRZyvgkJwFxoulAoGAe4m/1XoZrga9z2GzNs10 19 | AdgX7BW00x+MhH4pIiPnn1yK+nYa9jg4647Asnv3IfXZEnEEgRNxReKbi0+iDFBt 20 | aNW+lDGuHTi37AfD1EBDnpEQgO1MUcRb6rwBkTAWatsCaO00+HUmyX9cFLm4Vz7n 21 | caILyQ6CxZBlLgRIgDHxADMCgYEAtubsJGTHmZBmSCStpXLUWbOBLNQqfTM398DZ 22 | QoirP1PsUQ+IGUfSG/u+QCogR6fPEBkXeFHxsoY/Cvsm2lvYaKgK1VFn46Xm2vNq 23 | JuIH4pZCqp6LAv4weddZslT0a5eaowRSZ4o7PmTAaRuCXvD3VjTSJwhJFMo+90TV 24 | vEWn7gkCgYEAkk+unX9kYmKoUdLh22/tzQekBa8WqMxXDwzBCECTAs2GlpL/f73i 25 | zD15TnaNfLP6Q5RNb0N9tb0Gz1wSkwI1+jGAQLnh2K9X9cIVIqJn8Mf/KQa/wUDV 26 | Tb1j7FoGUEgX7vbsyWuTd8P76kNYyGqCss1XmbttcSolqpbIdlSUcO0= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/twisted/twisted-server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | twisted-server.py 4 | ~~~~~~~~~~~~~~~~~ 5 | 6 | A fully-functional HTTP/2 server written for Twisted. 7 | """ 8 | import functools 9 | import mimetypes 10 | import os 11 | import os.path 12 | import sys 13 | 14 | from OpenSSL import crypto 15 | from twisted.internet.defer import Deferred, inlineCallbacks 16 | from twisted.internet.protocol import Protocol, Factory 17 | from twisted.internet import endpoints, reactor, ssl 18 | from h2.config import H2Configuration 19 | from h2.connection import H2Connection 20 | from h2.events import ( 21 | RequestReceived, DataReceived, WindowUpdated 22 | ) 23 | from h2.exceptions import ProtocolError 24 | 25 | 26 | def close_file(file, d): 27 | file.close() 28 | 29 | 30 | READ_CHUNK_SIZE = 8192 31 | 32 | 33 | class H2Protocol(Protocol): 34 | def __init__(self, root): 35 | config = H2Configuration(client_side=False) 36 | self.conn = H2Connection(config=config) 37 | self.known_proto = None 38 | self.root = root 39 | 40 | self._flow_control_deferreds = {} 41 | 42 | def connectionMade(self): 43 | self.conn.initiate_connection() 44 | self.transport.write(self.conn.data_to_send()) 45 | 46 | def dataReceived(self, data): 47 | if not self.known_proto: 48 | self.known_proto = True 49 | 50 | try: 51 | events = self.conn.receive_data(data) 52 | except ProtocolError: 53 | if self.conn.data_to_send: 54 | self.transport.write(self.conn.data_to_send()) 55 | self.transport.loseConnection() 56 | else: 57 | for event in events: 58 | if isinstance(event, RequestReceived): 59 | self.requestReceived(event.headers, event.stream_id) 60 | elif isinstance(event, DataReceived): 61 | self.dataFrameReceived(event.stream_id) 62 | elif isinstance(event, WindowUpdated): 63 | self.windowUpdated(event) 64 | 65 | if self.conn.data_to_send: 66 | self.transport.write(self.conn.data_to_send()) 67 | 68 | def requestReceived(self, headers, stream_id): 69 | headers = dict(headers) # Invalid conversion, fix later. 70 | assert headers[b':method'] == b'GET' 71 | 72 | path = headers[b':path'].lstrip(b'/') 73 | full_path = os.path.join(self.root, path) 74 | 75 | if not os.path.exists(full_path): 76 | response_headers = ( 77 | (':status', '404'), 78 | ('content-length', '0'), 79 | ('server', 'twisted-h2'), 80 | ) 81 | self.conn.send_headers( 82 | stream_id, response_headers, end_stream=True 83 | ) 84 | self.transport.write(self.conn.data_to_send()) 85 | else: 86 | self.sendFile(full_path, stream_id) 87 | 88 | return 89 | 90 | def dataFrameReceived(self, stream_id): 91 | self.conn.reset_stream(stream_id) 92 | self.transport.write(self.conn.data_to_send()) 93 | 94 | def sendFile(self, file_path, stream_id): 95 | filesize = os.stat(file_path).st_size 96 | content_type, content_encoding = mimetypes.guess_type( 97 | file_path.decode('utf-8') 98 | ) 99 | response_headers = [ 100 | (':status', '200'), 101 | ('content-length', str(filesize)), 102 | ('server', 'twisted-h2'), 103 | ] 104 | if content_type: 105 | response_headers.append(('content-type', content_type)) 106 | if content_encoding: 107 | response_headers.append(('content-encoding', content_encoding)) 108 | 109 | self.conn.send_headers(stream_id, response_headers) 110 | self.transport.write(self.conn.data_to_send()) 111 | 112 | f = open(file_path, 'rb') 113 | d = self._send_file(f, stream_id) 114 | d.addErrback(functools.partial(close_file, f)) 115 | 116 | def windowUpdated(self, event): 117 | """ 118 | Handle a WindowUpdated event by firing any waiting data sending 119 | callbacks. 120 | """ 121 | stream_id = event.stream_id 122 | 123 | if stream_id and stream_id in self._flow_control_deferreds: 124 | d = self._flow_control_deferreds.pop(stream_id) 125 | d.callback(event.delta) 126 | elif not stream_id: 127 | for d in self._flow_control_deferreds.values(): 128 | d.callback(event.delta) 129 | 130 | self._flow_control_deferreds = {} 131 | 132 | return 133 | 134 | @inlineCallbacks 135 | def _send_file(self, file, stream_id): 136 | """ 137 | This callback sends more data for a given file on the stream. 138 | """ 139 | keep_reading = True 140 | while keep_reading: 141 | while not self.conn.remote_flow_control_window(stream_id): 142 | yield self.wait_for_flow_control(stream_id) 143 | 144 | chunk_size = min( 145 | self.conn.remote_flow_control_window(stream_id), READ_CHUNK_SIZE 146 | ) 147 | data = file.read(chunk_size) 148 | keep_reading = len(data) == chunk_size 149 | self.conn.send_data(stream_id, data, not keep_reading) 150 | self.transport.write(self.conn.data_to_send()) 151 | 152 | if not keep_reading: 153 | break 154 | 155 | file.close() 156 | 157 | def wait_for_flow_control(self, stream_id): 158 | """ 159 | Returns a Deferred that fires when the flow control window is opened. 160 | """ 161 | d = Deferred() 162 | self._flow_control_deferreds[stream_id] = d 163 | return d 164 | 165 | 166 | class H2Factory(Factory): 167 | def __init__(self, root): 168 | self.root = root 169 | 170 | def buildProtocol(self, addr): 171 | print(H2Protocol) 172 | return H2Protocol(self.root) 173 | 174 | 175 | root = sys.argv[1].encode('utf-8') 176 | 177 | with open('server.crt', 'r') as f: 178 | cert_data = f.read() 179 | with open('server.key', 'r') as f: 180 | key_data = f.read() 181 | 182 | cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) 183 | key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_data) 184 | options = ssl.CertificateOptions( 185 | privateKey=key, 186 | certificate=cert, 187 | acceptableProtocols=[b'h2'], 188 | ) 189 | 190 | endpoint = endpoints.SSL4ServerEndpoint(reactor, 8080, options, backlog=128) 191 | endpoint.listen(H2Factory(root)) 192 | reactor.run() 193 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ 2 | # https://packaging.python.org/en/latest/specifications/pyproject-toml/ 3 | 4 | [build-system] 5 | requires = ["setuptools"] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "h2" 10 | description = "Pure-Python HTTP/2 protocol implementation" 11 | readme = { file = "README.rst", content-type = "text/x-rst" } 12 | license = { file = "LICENSE" } 13 | 14 | authors = [ 15 | { name = "Cory Benfield", email = "cory@lukasa.co.uk" } 16 | ] 17 | maintainers = [ 18 | { name = "Thomas Kriechbaumer", email = "thomas@kriechbaumer.name" }, 19 | ] 20 | 21 | requires-python = ">=3.9" 22 | dependencies = [ 23 | "hyperframe>=6.1,<7", 24 | "hpack>=4.1,<5", 25 | ] 26 | dynamic = ["version"] 27 | 28 | # For a list of valid classifiers, see https://pypi.org/classifiers/ 29 | classifiers = [ 30 | "Development Status :: 5 - Production/Stable", 31 | "Intended Audience :: Developers", 32 | "License :: OSI Approved :: MIT License", 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3 :: Only", 35 | "Programming Language :: Python :: 3", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3.10", 38 | "Programming Language :: Python :: 3.11", 39 | "Programming Language :: Python :: 3.12", 40 | "Programming Language :: Python :: 3.13", 41 | "Programming Language :: Python :: Implementation :: CPython", 42 | "Programming Language :: Python :: Implementation :: PyPy", 43 | ] 44 | 45 | [project.urls] 46 | "Homepage" = "https://github.com/python-hyper/h2/" 47 | "Bug Reports" = "https://github.com/python-hyper/h2/issues" 48 | "Source" = "https://github.com/python-hyper/h2/" 49 | "Documentation" = "https://python-hyper.org/" 50 | 51 | [dependency-groups] 52 | dev = [ 53 | { include-group = "testing" }, 54 | { include-group = "linting" }, 55 | { include-group = "packaging" }, 56 | { include-group = "docs" }, 57 | ] 58 | 59 | testing = [ 60 | "pytest>=8.3.3,<9", 61 | "pytest-cov>=6.0.0,<7", 62 | "pytest-xdist>=3.6.1,<4", 63 | "hypothesis>=6.119.4,<7", 64 | ] 65 | 66 | linting = [ 67 | "ruff>=0.8.0,<1", 68 | "mypy>=1.13.0,<2", 69 | "typing_extensions>=4.12.2", 70 | ] 71 | 72 | packaging = [ 73 | "check-manifest==0.50", 74 | "readme-renderer==44.0", 75 | "build>=1.2.2,<2", 76 | "twine>=6.1.0,<7", 77 | "wheel>=0.45.0,<1", 78 | ] 79 | 80 | docs = [ 81 | "sphinx>=7.4.7,<9", 82 | ] 83 | 84 | [tool.setuptools.packages.find] 85 | where = [ "src" ] 86 | 87 | [tool.setuptools.package-data] 88 | h2 = [ "py.typed" ] 89 | 90 | [tool.setuptools.dynamic] 91 | version = { attr = "h2.__version__" } 92 | 93 | [tool.ruff] 94 | line-length = 150 95 | target-version = "py39" 96 | format.preview = true 97 | format.docstring-code-line-length = 100 98 | format.docstring-code-format = true 99 | lint.select = [ 100 | "ALL", 101 | ] 102 | lint.ignore = [ 103 | "PYI034", # PEP 673 not yet available in Python 3.9 - only in 3.11+ 104 | "ANN001", # args with typing.Any 105 | "ANN002", # args with typing.Any 106 | "ANN003", # kwargs with typing.Any 107 | "ANN401", # kwargs with typing.Any 108 | "SLF001", # implementation detail 109 | "CPY", # not required 110 | "D101", # docs readability 111 | "D102", # docs readability 112 | "D105", # docs readability 113 | "D107", # docs readability 114 | "D200", # docs readability 115 | "D205", # docs readability 116 | "D205", # docs readability 117 | "D203", # docs readability 118 | "D212", # docs readability 119 | "D400", # docs readability 120 | "D401", # docs readability 121 | "D415", # docs readability 122 | "PLR2004", # readability 123 | "SIM108", # readability 124 | "RUF012", # readability 125 | "FBT001", # readability 126 | "FBT002", # readability 127 | "PGH003", # readability 128 | "PGH004", # readability 129 | "FIX001", # readability 130 | "FIX002", # readability 131 | "TD001", # readability 132 | "TD002", # readability 133 | "TD003", # readability 134 | "S101", # readability 135 | "PD901", # readability 136 | "ERA001", # readability 137 | "ARG001", # readability 138 | "ARG002", # readability 139 | "PLR0913", # readability 140 | ] 141 | lint.isort.required-imports = [ "from __future__ import annotations" ] 142 | 143 | [tool.mypy] 144 | show_error_codes = true 145 | strict = true 146 | 147 | [tool.pytest.ini_options] 148 | testpaths = [ "tests" ] 149 | 150 | [tool.coverage.run] 151 | branch = true 152 | source = [ "h2" ] 153 | 154 | [tool.coverage.report] 155 | fail_under = 100 156 | show_missing = true 157 | exclude_lines = [ 158 | "pragma: no cover", 159 | "raise NotImplementedError()", 160 | 'assert False, "Should not be reachable"', 161 | # .*:.* # Python \d.* 162 | # .*:.* # Platform-specific: 163 | ] 164 | 165 | [tool.coverage.paths] 166 | source = [ 167 | "src/", 168 | ".tox/**/site-packages/", 169 | ] 170 | 171 | [tool.tox] 172 | min_version = "4.23.2" 173 | env_list = [ "py39", "py310", "py311", "py312", "py313", "pypy3", "lint", "docs", "packaging" ] 174 | 175 | [tool.tox.gh-actions] 176 | python = """ 177 | 3.9: py39, h2spec, lint, docs, packaging 178 | 3.10: py310 179 | 3.11: py311 180 | 3.12: py312 181 | 3.13: py313 182 | pypy3: pypy3 183 | """ 184 | 185 | [tool.tox.env_run_base] 186 | dependency_groups = ["testing"] 187 | commands = [ 188 | ["python", "-bb", "-m", "pytest", "--cov-report=xml", "--cov-report=term", "--cov=h2", { replace = "posargs", extend = true }] 189 | ] 190 | 191 | [tool.tox.env.pypy3] 192 | # temporarily disable coverage testing on PyPy due to performance problems 193 | commands = [ 194 | ["pytest", { replace = "posargs", extend = true }] 195 | ] 196 | 197 | [tool.tox.env.lint] 198 | dependency_groups = ["linting"] 199 | commands = [ 200 | ["ruff", "check", "src/"], 201 | ["mypy", "src/"], 202 | ] 203 | 204 | [tool.tox.env.docs] 205 | dependency_groups = ["docs"] 206 | allowlist_externals = ["make"] 207 | changedir = "{toxinidir}/docs" 208 | commands = [ 209 | ["make", "clean"], 210 | ["make", "html"], 211 | ] 212 | 213 | [tool.tox.env.packaging] 214 | base_python = ["python39"] 215 | dependency_groups = ["packaging"] 216 | allowlist_externals = ["rm"] 217 | commands = [ 218 | ["rm", "-rf", "dist/"], 219 | ["check-manifest"], 220 | ["python", "-m", "build", "--outdir", "dist/"], 221 | ["twine", "check", "dist/*"], 222 | ] 223 | 224 | [tool.tox.env.publish] 225 | base_python = ["python39"] 226 | dependency_groups = ["packaging"] 227 | commands = [ 228 | ["python", "-m", "build", "--outdir", "dist/"], 229 | ["twine", "check", "dist/*"], 230 | ["twine", "upload", "dist/*"], 231 | ] 232 | 233 | [tool.tox.env.graphs] 234 | basepython = ["python3.9"] 235 | deps = [ 236 | "graphviz==0.14.1", 237 | ] 238 | commands = [ 239 | ["python", "visualizer/visualize.py", "-i", "docs/source/_static"], 240 | ] 241 | 242 | [tool.tox.env.h2spec] 243 | basepython = ["python3.9"] 244 | deps = [ 245 | "twisted[tls]==20.3.0", 246 | ] 247 | allowlist_externals = [ 248 | "{toxinidir}/tests/h2spectest.sh" 249 | ] 250 | commands = [ 251 | ["{toxinidir}/tests/h2spectest.sh"], 252 | ] 253 | -------------------------------------------------------------------------------- /src/h2/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP/2 protocol implementation for Python. 3 | """ 4 | from __future__ import annotations 5 | 6 | __version__ = "4.3.0+dev" 7 | -------------------------------------------------------------------------------- /src/h2/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | h2/errors 3 | ~~~~~~~~~ 4 | 5 | Global error code registry containing the established HTTP/2 error codes. 6 | 7 | The current registry is available at: 8 | https://tools.ietf.org/html/rfc7540#section-11.4 9 | """ 10 | from __future__ import annotations 11 | 12 | import enum 13 | 14 | 15 | class ErrorCodes(enum.IntEnum): 16 | """ 17 | All known HTTP/2 error codes. 18 | 19 | .. versionadded:: 2.5.0 20 | """ 21 | 22 | #: Graceful shutdown. 23 | NO_ERROR = 0x0 24 | 25 | #: Protocol error detected. 26 | PROTOCOL_ERROR = 0x1 27 | 28 | #: Implementation fault. 29 | INTERNAL_ERROR = 0x2 30 | 31 | #: Flow-control limits exceeded. 32 | FLOW_CONTROL_ERROR = 0x3 33 | 34 | #: Settings not acknowledged. 35 | SETTINGS_TIMEOUT = 0x4 36 | 37 | #: Frame received for closed stream. 38 | STREAM_CLOSED = 0x5 39 | 40 | #: Frame size incorrect. 41 | FRAME_SIZE_ERROR = 0x6 42 | 43 | #: Stream not processed. 44 | REFUSED_STREAM = 0x7 45 | 46 | #: Stream cancelled. 47 | CANCEL = 0x8 48 | 49 | #: Compression state not updated. 50 | COMPRESSION_ERROR = 0x9 51 | 52 | #: TCP connection error for CONNECT method. 53 | CONNECT_ERROR = 0xa 54 | 55 | #: Processing capacity exceeded. 56 | ENHANCE_YOUR_CALM = 0xb 57 | 58 | #: Negotiated TLS parameters not acceptable. 59 | INADEQUATE_SECURITY = 0xc 60 | 61 | #: Use HTTP/1.1 for the request. 62 | HTTP_1_1_REQUIRED = 0xd 63 | 64 | 65 | def _error_code_from_int(code: int) -> ErrorCodes | int: 66 | """ 67 | Given an integer error code, returns either one of :class:`ErrorCodes 68 | ` or, if not present in the known set of codes, 69 | returns the integer directly. 70 | """ 71 | try: 72 | return ErrorCodes(code) 73 | except ValueError: 74 | return code 75 | 76 | 77 | __all__ = ["ErrorCodes"] 78 | -------------------------------------------------------------------------------- /src/h2/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | h2/exceptions 3 | ~~~~~~~~~~~~~ 4 | 5 | Exceptions for the HTTP/2 module. 6 | """ 7 | from __future__ import annotations 8 | 9 | from .errors import ErrorCodes 10 | 11 | 12 | class H2Error(Exception): 13 | """ 14 | The base class for all exceptions for the HTTP/2 module. 15 | """ 16 | 17 | 18 | class ProtocolError(H2Error): 19 | """ 20 | An action was attempted in violation of the HTTP/2 protocol. 21 | """ 22 | 23 | #: The error code corresponds to this kind of Protocol Error. 24 | error_code = ErrorCodes.PROTOCOL_ERROR 25 | 26 | 27 | class FrameTooLargeError(ProtocolError): 28 | """ 29 | The frame that we tried to send or that we received was too large. 30 | """ 31 | 32 | #: The error code corresponds to this kind of Protocol Error. 33 | error_code = ErrorCodes.FRAME_SIZE_ERROR 34 | 35 | 36 | class FrameDataMissingError(ProtocolError): 37 | """ 38 | The frame that we received is missing some data. 39 | 40 | .. versionadded:: 2.0.0 41 | """ 42 | 43 | #: The error code corresponds to this kind of Protocol Error. 44 | error_code = ErrorCodes.FRAME_SIZE_ERROR 45 | 46 | 47 | class TooManyStreamsError(ProtocolError): 48 | """ 49 | An attempt was made to open a stream that would lead to too many concurrent 50 | streams. 51 | """ 52 | 53 | 54 | 55 | class FlowControlError(ProtocolError): 56 | """ 57 | An attempted action violates flow control constraints. 58 | """ 59 | 60 | #: The error code corresponds to this kind of Protocol Error. 61 | error_code = ErrorCodes.FLOW_CONTROL_ERROR 62 | 63 | 64 | class StreamIDTooLowError(ProtocolError): 65 | """ 66 | An attempt was made to open a stream that had an ID that is lower than the 67 | highest ID we have seen on this connection. 68 | """ 69 | 70 | def __init__(self, stream_id: int, max_stream_id: int) -> None: 71 | #: The ID of the stream that we attempted to open. 72 | self.stream_id = stream_id 73 | 74 | #: The current highest-seen stream ID. 75 | self.max_stream_id = max_stream_id 76 | 77 | def __str__(self) -> str: 78 | return f"StreamIDTooLowError: {self.stream_id} is lower than {self.max_stream_id}" 79 | 80 | 81 | class NoAvailableStreamIDError(ProtocolError): 82 | """ 83 | There are no available stream IDs left to the connection. All stream IDs 84 | have been exhausted. 85 | 86 | .. versionadded:: 2.0.0 87 | """ 88 | 89 | 90 | 91 | class NoSuchStreamError(ProtocolError): 92 | """ 93 | A stream-specific action referenced a stream that does not exist. 94 | 95 | .. versionchanged:: 2.0.0 96 | Became a subclass of :class:`ProtocolError 97 | ` 98 | """ 99 | 100 | def __init__(self, stream_id: int) -> None: 101 | #: The stream ID corresponds to the non-existent stream. 102 | self.stream_id = stream_id 103 | 104 | 105 | class StreamClosedError(NoSuchStreamError): 106 | """ 107 | A more specific form of 108 | :class:`NoSuchStreamError `. Indicates 109 | that the stream has since been closed, and that all state relating to that 110 | stream has been removed. 111 | """ 112 | 113 | def __init__(self, stream_id: int) -> None: 114 | #: The stream ID corresponds to the nonexistent stream. 115 | self.stream_id = stream_id 116 | 117 | #: The relevant HTTP/2 error code. 118 | self.error_code = ErrorCodes.STREAM_CLOSED 119 | 120 | # Any events that internal code may need to fire. Not relevant to 121 | # external users that may receive a StreamClosedError. 122 | self._events = [] # type: ignore 123 | 124 | 125 | class InvalidSettingsValueError(ProtocolError, ValueError): 126 | """ 127 | An attempt was made to set an invalid Settings value. 128 | 129 | .. versionadded:: 2.0.0 130 | """ 131 | 132 | def __init__(self, msg: str, error_code: ErrorCodes) -> None: 133 | super().__init__(msg) 134 | self.error_code = error_code 135 | 136 | 137 | class InvalidBodyLengthError(ProtocolError): 138 | """ 139 | The remote peer sent more or less data that the Content-Length header 140 | indicated. 141 | 142 | .. versionadded:: 2.0.0 143 | """ 144 | 145 | def __init__(self, expected: int, actual: int) -> None: 146 | self.expected_length = expected 147 | self.actual_length = actual 148 | 149 | def __str__(self) -> str: 150 | return f"InvalidBodyLengthError: Expected {self.expected_length} bytes, received {self.actual_length}" 151 | 152 | 153 | class UnsupportedFrameError(ProtocolError): 154 | """ 155 | The remote peer sent a frame that is unsupported in this context. 156 | 157 | .. versionadded:: 2.1.0 158 | 159 | .. versionchanged:: 4.0.0 160 | Removed deprecated KeyError parent class. 161 | """ 162 | 163 | 164 | 165 | class RFC1122Error(H2Error): 166 | """ 167 | Emitted when users attempt to do something that is literally allowed by the 168 | relevant RFC, but is sufficiently ill-defined that it's unwise to allow 169 | users to actually do it. 170 | 171 | While there is some disagreement about whether or not we should be liberal 172 | in what accept, it is a truth universally acknowledged that we should be 173 | conservative in what emit. 174 | 175 | .. versionadded:: 2.4.0 176 | """ 177 | 178 | # shazow says I'm going to regret naming the exception this way. If that 179 | # turns out to be true, TELL HIM NOTHING. 180 | 181 | 182 | class DenialOfServiceError(ProtocolError): 183 | """ 184 | Emitted when the remote peer exhibits a behaviour that is likely to be an 185 | attempt to perform a Denial of Service attack on the implementation. This 186 | is a form of ProtocolError that carries a different error code, and allows 187 | more easy detection of this kind of behaviour. 188 | 189 | .. versionadded:: 2.5.0 190 | """ 191 | 192 | #: The error code corresponds to this kind of 193 | #: :class:`ProtocolError ` 194 | error_code = ErrorCodes.ENHANCE_YOUR_CALM 195 | -------------------------------------------------------------------------------- /src/h2/frame_buffer.py: -------------------------------------------------------------------------------- 1 | """ 2 | h2/frame_buffer 3 | ~~~~~~~~~~~~~~~ 4 | 5 | A data structure that provides a way to iterate over a byte buffer in terms of 6 | frames. 7 | """ 8 | from __future__ import annotations 9 | 10 | from hyperframe.exceptions import InvalidDataError, InvalidFrameError 11 | from hyperframe.frame import ContinuationFrame, Frame, HeadersFrame, PushPromiseFrame 12 | 13 | from .exceptions import FrameDataMissingError, FrameTooLargeError, ProtocolError 14 | 15 | # To avoid a DOS attack based on sending loads of continuation frames, we limit 16 | # the maximum number we're perpared to receive. In this case, we'll set the 17 | # limit to 64, which means the largest encoded header block we can receive by 18 | # default is 262144 bytes long, and the largest possible *at all* is 1073741760 19 | # bytes long. 20 | # 21 | # This value seems reasonable for now, but in future we may want to evaluate 22 | # making it configurable. 23 | CONTINUATION_BACKLOG = 64 24 | 25 | 26 | class FrameBuffer: 27 | """ 28 | A buffer data structure for HTTP/2 data that allows iteraton in terms of 29 | H2 frames. 30 | """ 31 | 32 | def __init__(self, server: bool = False) -> None: 33 | self._data = bytearray() 34 | self.max_frame_size = 0 35 | self._preamble = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" if server else b"" 36 | self._preamble_len = len(self._preamble) 37 | self._headers_buffer: list[HeadersFrame | ContinuationFrame | PushPromiseFrame] = [] 38 | 39 | def add_data(self, data: bytes) -> None: 40 | """ 41 | Add more data to the frame buffer. 42 | 43 | :param data: A bytestring containing the byte buffer. 44 | """ 45 | if self._preamble_len: 46 | data_len = len(data) 47 | of_which_preamble = min(self._preamble_len, data_len) 48 | 49 | if self._preamble[:of_which_preamble] != data[:of_which_preamble]: 50 | msg = "Invalid HTTP/2 preamble." 51 | raise ProtocolError(msg) 52 | 53 | data = data[of_which_preamble:] 54 | self._preamble_len -= of_which_preamble 55 | self._preamble = self._preamble[of_which_preamble:] 56 | 57 | self._data += data 58 | 59 | def _validate_frame_length(self, length: int) -> None: 60 | """ 61 | Confirm that the frame is an appropriate length. 62 | """ 63 | if length > self.max_frame_size: 64 | msg = f"Received overlong frame: length {length}, max {self.max_frame_size}" 65 | raise FrameTooLargeError(msg) 66 | 67 | def _update_header_buffer(self, f: Frame | None) -> Frame | None: 68 | """ 69 | Updates the internal header buffer. Returns a frame that should replace 70 | the current one. May throw exceptions if this frame is invalid. 71 | """ 72 | # Check if we're in the middle of a headers block. If we are, this 73 | # frame *must* be a CONTINUATION frame with the same stream ID as the 74 | # leading HEADERS or PUSH_PROMISE frame. Anything else is a 75 | # ProtocolError. If the frame *is* valid, append it to the header 76 | # buffer. 77 | if self._headers_buffer: 78 | stream_id = self._headers_buffer[0].stream_id 79 | valid_frame = ( 80 | f is not None and 81 | isinstance(f, ContinuationFrame) and 82 | f.stream_id == stream_id 83 | ) 84 | if not valid_frame: 85 | msg = "Invalid frame during header block." 86 | raise ProtocolError(msg) 87 | assert isinstance(f, ContinuationFrame) 88 | 89 | # Append the frame to the buffer. 90 | self._headers_buffer.append(f) 91 | if len(self._headers_buffer) > CONTINUATION_BACKLOG: 92 | msg = "Too many continuation frames received." 93 | raise ProtocolError(msg) 94 | 95 | # If this is the end of the header block, then we want to build a 96 | # mutant HEADERS frame that's massive. Use the original one we got, 97 | # then set END_HEADERS and set its data appopriately. If it's not 98 | # the end of the block, lose the current frame: we can't yield it. 99 | if "END_HEADERS" in f.flags: 100 | f = self._headers_buffer[0] 101 | f.flags.add("END_HEADERS") 102 | f.data = b"".join(x.data for x in self._headers_buffer) 103 | self._headers_buffer = [] 104 | else: 105 | f = None 106 | elif (isinstance(f, (HeadersFrame, PushPromiseFrame)) and 107 | "END_HEADERS" not in f.flags): 108 | # This is the start of a headers block! Save the frame off and then 109 | # act like we didn't receive one. 110 | self._headers_buffer.append(f) 111 | f = None 112 | 113 | return f 114 | 115 | # The methods below support the iterator protocol. 116 | def __iter__(self) -> FrameBuffer: 117 | return self 118 | 119 | def __next__(self) -> Frame: 120 | # First, check that we have enough data to successfully parse the 121 | # next frame header. If not, bail. Otherwise, parse it. 122 | if len(self._data) < 9: 123 | raise StopIteration 124 | 125 | try: 126 | f, length = Frame.parse_frame_header(memoryview(self._data[:9])) 127 | except (InvalidDataError, InvalidFrameError) as err: # pragma: no cover 128 | msg = f"Received frame with invalid header: {err!s}" 129 | raise ProtocolError(msg) from err 130 | 131 | # Next, check that we have enough length to parse the frame body. If 132 | # not, bail, leaving the frame header data in the buffer for next time. 133 | if len(self._data) < length + 9: 134 | raise StopIteration 135 | 136 | # Confirm the frame has an appropriate length. 137 | self._validate_frame_length(length) 138 | 139 | # Try to parse the frame body 140 | try: 141 | f.parse_body(memoryview(self._data[9:9+length])) 142 | except InvalidDataError as err: 143 | msg = "Received frame with non-compliant data" 144 | raise ProtocolError(msg) from err 145 | except InvalidFrameError as err: 146 | msg = "Frame data missing or invalid" 147 | raise FrameDataMissingError(msg) from err 148 | 149 | # At this point, as we know we'll use or discard the entire frame, we 150 | # can update the data. 151 | self._data = self._data[9+length:] 152 | 153 | # Pass the frame through the header buffer. 154 | new_frame = self._update_header_buffer(f) 155 | 156 | # If we got a frame we didn't understand or shouldn't yield, rather 157 | # than return None it'd be better if we just tried to get the next 158 | # frame in the sequence instead. Recurse back into ourselves to do 159 | # that. This is safe because the amount of work we have to do here is 160 | # strictly bounded by the length of the buffer. 161 | return new_frame if new_frame is not None else self.__next__() 162 | -------------------------------------------------------------------------------- /src/h2/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-hyper/h2/0583911b29d05764bbe3f7691d59f4d5e83e249b/src/h2/py.typed -------------------------------------------------------------------------------- /src/h2/windows.py: -------------------------------------------------------------------------------- 1 | """ 2 | h2/windows 3 | ~~~~~~~~~~ 4 | 5 | Defines tools for managing HTTP/2 flow control windows. 6 | 7 | The objects defined in this module are used to automatically manage HTTP/2 8 | flow control windows. Specifically, they keep track of what the size of the 9 | window is, how much data has been consumed from that window, and how much data 10 | the user has already used. It then implements a basic algorithm that attempts 11 | to manage the flow control window without user input, trying to ensure that it 12 | does not emit too many WINDOW_UPDATE frames. 13 | """ 14 | from __future__ import annotations 15 | 16 | from .exceptions import FlowControlError 17 | 18 | # The largest acceptable value for a HTTP/2 flow control window. 19 | LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1 20 | 21 | 22 | class WindowManager: 23 | """ 24 | A basic HTTP/2 window manager. 25 | 26 | :param max_window_size: The maximum size of the flow control window. 27 | :type max_window_size: ``int`` 28 | """ 29 | 30 | def __init__(self, max_window_size: int) -> None: 31 | assert max_window_size <= LARGEST_FLOW_CONTROL_WINDOW 32 | self.max_window_size = max_window_size 33 | self.current_window_size = max_window_size 34 | self._bytes_processed = 0 35 | 36 | def window_consumed(self, size: int) -> None: 37 | """ 38 | We have received a certain number of bytes from the remote peer. This 39 | necessarily shrinks the flow control window! 40 | 41 | :param size: The number of flow controlled bytes we received from the 42 | remote peer. 43 | :type size: ``int`` 44 | :returns: Nothing. 45 | :rtype: ``None`` 46 | """ 47 | self.current_window_size -= size 48 | if self.current_window_size < 0: 49 | msg = "Flow control window shrunk below 0" 50 | raise FlowControlError(msg) 51 | 52 | def window_opened(self, size: int) -> None: 53 | """ 54 | The flow control window has been incremented, either because of manual 55 | flow control management or because of the user changing the flow 56 | control settings. This can have the effect of increasing what we 57 | consider to be the "maximum" flow control window size. 58 | 59 | This does not increase our view of how many bytes have been processed, 60 | only of how much space is in the window. 61 | 62 | :param size: The increment to the flow control window we received. 63 | :type size: ``int`` 64 | :returns: Nothing 65 | :rtype: ``None`` 66 | """ 67 | self.current_window_size += size 68 | 69 | if self.current_window_size > LARGEST_FLOW_CONTROL_WINDOW: 70 | msg = f"Flow control window mustn't exceed {LARGEST_FLOW_CONTROL_WINDOW}" 71 | raise FlowControlError(msg) 72 | 73 | self.max_window_size = max(self.current_window_size, self.max_window_size) 74 | 75 | def process_bytes(self, size: int) -> int | None: 76 | """ 77 | The application has informed us that it has processed a certain number 78 | of bytes. This may cause us to want to emit a window update frame. If 79 | we do want to emit a window update frame, this method will return the 80 | number of bytes that we should increment the window by. 81 | 82 | :param size: The number of flow controlled bytes that the application 83 | has processed. 84 | :type size: ``int`` 85 | :returns: The number of bytes to increment the flow control window by, 86 | or ``None``. 87 | :rtype: ``int`` or ``None`` 88 | """ 89 | self._bytes_processed += size 90 | return self._maybe_update_window() 91 | 92 | def _maybe_update_window(self) -> int | None: 93 | """ 94 | Run the algorithm. 95 | 96 | Our current algorithm can be described like this. 97 | 98 | 1. If no bytes have been processed, we immediately return 0. There is 99 | no meaningful way for us to hand space in the window back to the 100 | remote peer, so let's not even try. 101 | 2. If there is no space in the flow control window, and we have 102 | processed at least 1024 bytes (or 1/4 of the window, if the window 103 | is smaller), we will emit a window update frame. This is to avoid 104 | the risk of blocking a stream altogether. 105 | 3. If there is space in the flow control window, and we have processed 106 | at least 1/2 of the window worth of bytes, we will emit a window 107 | update frame. This is to minimise the number of window update frames 108 | we have to emit. 109 | 110 | In a healthy system with large flow control windows, this will 111 | irregularly emit WINDOW_UPDATE frames. This prevents us starving the 112 | connection by emitting eleventy bajillion WINDOW_UPDATE frames, 113 | especially in situations where the remote peer is sending a lot of very 114 | small DATA frames. 115 | """ 116 | # TODO: Can the window be smaller than 1024 bytes? If not, we can 117 | # streamline this algorithm. 118 | if not self._bytes_processed: 119 | return None 120 | 121 | max_increment = (self.max_window_size - self.current_window_size) 122 | increment = 0 123 | 124 | # Note that, even though we may increment less than _bytes_processed, 125 | # we still want to set it to zero whenever we emit an increment. This 126 | # is because we'll always increment up to the maximum we can. 127 | if ((self.current_window_size == 0) and ( 128 | self._bytes_processed > min(1024, self.max_window_size // 4))) or self._bytes_processed >= (self.max_window_size // 2): 129 | increment = min(self._bytes_processed, max_increment) 130 | self._bytes_processed = 0 131 | 132 | self.current_window_size += increment 133 | return increment 134 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-hyper/h2/0583911b29d05764bbe3f7691d59f4d5e83e249b/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from . import helpers 6 | 7 | 8 | @pytest.fixture 9 | def frame_factory(): 10 | return helpers.FrameFactory() 11 | -------------------------------------------------------------------------------- /tests/coroutine_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file gives access to a coroutine-based test class. This allows each test 3 | case to be defined as a pair of interacting coroutines, sending data to each 4 | other by yielding the flow of control. 5 | 6 | The advantage of this method is that we avoid the difficulty of using threads 7 | in Python, as well as the pain of using sockets and events to communicate and 8 | organise the communication. This makes the tests entirely deterministic and 9 | makes them behave identically on all platforms, as well as ensuring they both 10 | succeed and fail quickly. 11 | """ 12 | from __future__ import annotations 13 | 14 | import functools 15 | import itertools 16 | 17 | import pytest 18 | 19 | 20 | class CoroutineTestCase: 21 | """ 22 | A base class for tests that use interacting coroutines. 23 | 24 | The run_until_complete method takes a number of coroutines as arguments. 25 | Each one is, in order, passed the output of the previous coroutine until 26 | one is exhausted. If a coroutine does not initially yield data (that is, 27 | its first action is to receive data), the calling code should prime it by 28 | using the 'server' decorator on this class. 29 | """ 30 | 31 | def run_until_complete(self, *coroutines) -> None: 32 | """ 33 | Executes a set of coroutines that communicate between each other. Each 34 | one is, in order, passed the output of the previous coroutine until 35 | one is exhausted. If a coroutine does not initially yield data (that 36 | is, its first action is to receive data), the calling code should prime 37 | it by using the 'server' decorator on this class. 38 | 39 | Once a coroutine is exhausted, the method performs a final check to 40 | ensure that all other coroutines are exhausted. This ensures that all 41 | assertions in those coroutines got executed. 42 | """ 43 | looping_coroutines = itertools.cycle(coroutines) 44 | data = None 45 | 46 | for coro in looping_coroutines: 47 | try: 48 | data = coro.send(data) 49 | except StopIteration: 50 | break 51 | 52 | for coro in coroutines: 53 | try: 54 | next(coro) 55 | except StopIteration: 56 | continue 57 | else: 58 | pytest.fail(f"Coroutine {coro} not exhausted") 59 | 60 | def server(self, func): 61 | """ 62 | A decorator that marks a test coroutine as a 'server' coroutine: that 63 | is, one whose first action is to consume data, rather than one that 64 | initially emits data. The effect of this decorator is simply to prime 65 | the coroutine. 66 | """ 67 | @functools.wraps(func) 68 | def wrapper(*args, **kwargs): 69 | c = func(*args, **kwargs) 70 | next(c) 71 | return c 72 | 73 | return wrapper 74 | -------------------------------------------------------------------------------- /tests/h2spectest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # A test script that runs the example Python Twisted server and then runs 4 | # h2spec against it. Prints the output of h2spec. This script does not expect 5 | # to be run directly, but instead via `tox -e h2spec`. 6 | 7 | set -x 8 | 9 | # Kill all background jobs on exit. 10 | trap 'kill $(jobs -p)' EXIT 11 | 12 | pushd examples/asyncio 13 | python asyncio-server.py & 14 | popd 15 | 16 | # Wait briefly to let the server start up 17 | sleep 2 18 | 19 | # Go go go! 20 | h2spec -k -t -v -p 8443 $@ 21 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module for the h2 tests. 3 | """ 4 | from __future__ import annotations 5 | 6 | from hpack.hpack import Encoder 7 | from hyperframe.frame import ( 8 | AltSvcFrame, 9 | ContinuationFrame, 10 | DataFrame, 11 | GoAwayFrame, 12 | HeadersFrame, 13 | PingFrame, 14 | PriorityFrame, 15 | PushPromiseFrame, 16 | RstStreamFrame, 17 | SettingsFrame, 18 | WindowUpdateFrame, 19 | ) 20 | 21 | SAMPLE_SETTINGS = { 22 | SettingsFrame.HEADER_TABLE_SIZE: 4096, 23 | SettingsFrame.ENABLE_PUSH: 1, 24 | SettingsFrame.MAX_CONCURRENT_STREAMS: 2, 25 | } 26 | 27 | 28 | class FrameFactory: 29 | """ 30 | A class containing lots of helper methods and state to build frames. This 31 | allows test cases to easily build correct HTTP/2 frames to feed to 32 | hyper-h2. 33 | """ 34 | 35 | def __init__(self) -> None: 36 | self.encoder = Encoder() 37 | 38 | def refresh_encoder(self) -> None: 39 | self.encoder = Encoder() 40 | 41 | def preamble(self) -> bytes: 42 | return b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" 43 | 44 | def build_headers_frame(self, 45 | headers, 46 | flags=None, 47 | stream_id=1, 48 | **priority_kwargs): 49 | """ 50 | Builds a single valid headers frame out of the contained headers. 51 | """ 52 | if flags is None: 53 | flags = [] 54 | f = HeadersFrame(stream_id) 55 | f.data = self.encoder.encode(headers) 56 | f.flags.add("END_HEADERS") 57 | for flag in flags: 58 | f.flags.add(flag) 59 | 60 | for k, v in priority_kwargs.items(): 61 | setattr(f, k, v) 62 | 63 | return f 64 | 65 | def build_continuation_frame(self, header_block, flags=None, stream_id=1): 66 | """ 67 | Builds a single continuation frame out of the binary header block. 68 | """ 69 | if flags is None: 70 | flags = [] 71 | f = ContinuationFrame(stream_id) 72 | f.data = header_block 73 | f.flags = set(flags) 74 | 75 | return f 76 | 77 | def build_data_frame(self, data, flags=None, stream_id=1, padding_len=0): 78 | """ 79 | Builds a single data frame out of a chunk of data. 80 | """ 81 | flags = set(flags) if flags is not None else set() 82 | f = DataFrame(stream_id) 83 | f.data = data 84 | f.flags = flags 85 | 86 | if padding_len: 87 | flags.add("PADDED") 88 | f.pad_length = padding_len 89 | 90 | return f 91 | 92 | def build_settings_frame(self, settings, ack=False): 93 | """ 94 | Builds a single settings frame. 95 | """ 96 | f = SettingsFrame(0) 97 | if ack: 98 | f.flags.add("ACK") 99 | 100 | f.settings = settings 101 | return f 102 | 103 | def build_window_update_frame(self, stream_id, increment): 104 | """ 105 | Builds a single WindowUpdate frame. 106 | """ 107 | f = WindowUpdateFrame(stream_id) 108 | f.window_increment = increment 109 | return f 110 | 111 | def build_ping_frame(self, ping_data, flags=None): 112 | """ 113 | Builds a single Ping frame. 114 | """ 115 | f = PingFrame(0) 116 | f.opaque_data = ping_data 117 | if flags: 118 | f.flags = set(flags) 119 | 120 | return f 121 | 122 | def build_goaway_frame(self, 123 | last_stream_id, 124 | error_code=0, 125 | additional_data=b""): 126 | """ 127 | Builds a single GOAWAY frame. 128 | """ 129 | f = GoAwayFrame(0) 130 | f.error_code = error_code 131 | f.last_stream_id = last_stream_id 132 | f.additional_data = additional_data 133 | return f 134 | 135 | def build_rst_stream_frame(self, stream_id, error_code=0): 136 | """ 137 | Builds a single RST_STREAM frame. 138 | """ 139 | f = RstStreamFrame(stream_id) 140 | f.error_code = error_code 141 | return f 142 | 143 | def build_push_promise_frame(self, 144 | stream_id, 145 | promised_stream_id, 146 | headers, 147 | flags=None): 148 | """ 149 | Builds a single PUSH_PROMISE frame. 150 | """ 151 | if flags is None: 152 | flags = [] 153 | f = PushPromiseFrame(stream_id) 154 | f.promised_stream_id = promised_stream_id 155 | f.data = self.encoder.encode(headers) 156 | f.flags = set(flags) 157 | f.flags.add("END_HEADERS") 158 | return f 159 | 160 | def build_priority_frame(self, 161 | stream_id, 162 | weight, 163 | depends_on=0, 164 | exclusive=False): 165 | """ 166 | Builds a single priority frame. 167 | """ 168 | f = PriorityFrame(stream_id) 169 | f.depends_on = depends_on 170 | f.stream_weight = weight 171 | f.exclusive = exclusive 172 | return f 173 | 174 | def build_alt_svc_frame(self, stream_id, origin, field): 175 | """ 176 | Builds a single ALTSVC frame. 177 | """ 178 | f = AltSvcFrame(stream_id) 179 | f.origin = origin 180 | f.field = field 181 | return f 182 | 183 | def change_table_size(self, new_size) -> None: 184 | """ 185 | Causes the encoder to send a dynamic size update in the next header 186 | block it sends. 187 | """ 188 | self.encoder.header_table_size = new_size 189 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the configuration object. 3 | """ 4 | from __future__ import annotations 5 | 6 | import logging 7 | 8 | import pytest 9 | 10 | import h2.config 11 | 12 | 13 | class TestH2Config: 14 | """ 15 | Tests of the H2 config object. 16 | """ 17 | 18 | def test_defaults(self) -> None: 19 | """ 20 | The default values of the HTTP/2 config object are sensible. 21 | """ 22 | config = h2.config.H2Configuration() 23 | assert config.client_side 24 | assert config.header_encoding is None 25 | assert isinstance(config.logger, h2.config.DummyLogger) 26 | 27 | boolean_config_options = [ 28 | "client_side", 29 | "validate_outbound_headers", 30 | "normalize_outbound_headers", 31 | "validate_inbound_headers", 32 | "normalize_inbound_headers", 33 | ] 34 | 35 | @pytest.mark.parametrize("option_name", boolean_config_options) 36 | @pytest.mark.parametrize("value", [None, "False", 1]) 37 | def test_boolean_config_options_reject_non_bools_init( 38 | self, option_name, value, 39 | ) -> None: 40 | """ 41 | The boolean config options raise an error if you try to set a value 42 | that isn't a boolean via the initializer. 43 | """ 44 | with pytest.raises(ValueError): 45 | h2.config.H2Configuration(**{option_name: value}) 46 | 47 | @pytest.mark.parametrize("option_name", boolean_config_options) 48 | @pytest.mark.parametrize("value", [None, "False", 1]) 49 | def test_boolean_config_options_reject_non_bools_attr( 50 | self, option_name, value, 51 | ) -> None: 52 | """ 53 | The boolean config options raise an error if you try to set a value 54 | that isn't a boolean via attribute setter. 55 | """ 56 | config = h2.config.H2Configuration() 57 | with pytest.raises(ValueError): 58 | setattr(config, option_name, value) 59 | 60 | @pytest.mark.parametrize("option_name", boolean_config_options) 61 | @pytest.mark.parametrize("value", [True, False]) 62 | def test_boolean_config_option_is_reflected_init(self, option_name, value) -> None: 63 | """ 64 | The value of the boolean config options, when set, is reflected 65 | in the value via the initializer. 66 | """ 67 | config = h2.config.H2Configuration(**{option_name: value}) 68 | assert getattr(config, option_name) == value 69 | 70 | @pytest.mark.parametrize("option_name", boolean_config_options) 71 | @pytest.mark.parametrize("value", [True, False]) 72 | def test_boolean_config_option_is_reflected_attr(self, option_name, value) -> None: 73 | """ 74 | The value of the boolean config options, when set, is reflected 75 | in the value via attribute setter. 76 | """ 77 | config = h2.config.H2Configuration() 78 | setattr(config, option_name, value) 79 | assert getattr(config, option_name) == value 80 | 81 | @pytest.mark.parametrize("header_encoding", [True, 1, object()]) 82 | def test_header_encoding_must_be_false_str_none_init( 83 | self, header_encoding, 84 | ) -> None: 85 | """ 86 | The value of the ``header_encoding`` setting must be False, a string, 87 | or None via the initializer. 88 | """ 89 | with pytest.raises(ValueError): 90 | h2.config.H2Configuration(header_encoding=header_encoding) 91 | 92 | @pytest.mark.parametrize("header_encoding", [True, 1, object()]) 93 | def test_header_encoding_must_be_false_str_none_attr( 94 | self, header_encoding, 95 | ) -> None: 96 | """ 97 | The value of the ``header_encoding`` setting must be False, a string, 98 | or None via attribute setter. 99 | """ 100 | config = h2.config.H2Configuration() 101 | with pytest.raises(ValueError): 102 | config.header_encoding = header_encoding 103 | 104 | @pytest.mark.parametrize("header_encoding", [False, "ascii", None]) 105 | def test_header_encoding_is_reflected_init(self, header_encoding) -> None: 106 | """ 107 | The value of ``header_encoding``, when set, is reflected in the value 108 | via the initializer. 109 | """ 110 | config = h2.config.H2Configuration(header_encoding=header_encoding) 111 | assert config.header_encoding == header_encoding 112 | 113 | @pytest.mark.parametrize("header_encoding", [False, "ascii", None]) 114 | def test_header_encoding_is_reflected_attr(self, header_encoding) -> None: 115 | """ 116 | The value of ``header_encoding``, when set, is reflected in the value 117 | via the attribute setter. 118 | """ 119 | config = h2.config.H2Configuration() 120 | config.header_encoding = header_encoding 121 | assert config.header_encoding == header_encoding 122 | 123 | def test_logger_instance_is_reflected(self) -> None: 124 | """ 125 | The value of ``logger``, when set, is reflected in the value. 126 | """ 127 | logger = logging.getLogger("hyper-h2.test") 128 | config = h2.config.H2Configuration() 129 | config.logger = logger 130 | assert config.logger is logger 131 | 132 | @pytest.mark.parametrize("trace_level", [False, True]) 133 | def test_output_logger(self, capsys, trace_level) -> None: 134 | logger = h2.config.OutputLogger(trace_level=trace_level) 135 | 136 | logger.debug("This is a debug message %d.", 123) 137 | logger.trace("This is a trace message %d.", 123) 138 | captured = capsys.readouterr() 139 | assert "h2 (debug): This is a debug message 123.\n" in captured.err 140 | if trace_level: 141 | assert "h2 (trace): This is a trace message 123.\n" in captured.err 142 | else: 143 | assert "h2 (trace): This is a trace message 123.\n" not in captured.err 144 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that verify logic local to exceptions. 3 | """ 4 | from __future__ import annotations 5 | 6 | import h2.exceptions 7 | 8 | 9 | class TestExceptions: 10 | def test_stream_id_too_low_prints_properly(self) -> None: 11 | x = h2.exceptions.StreamIDTooLowError(5, 10) 12 | 13 | assert str(x) == "StreamIDTooLowError: 5 is lower than 10" 14 | -------------------------------------------------------------------------------- /tests/test_head_request.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | import h2.connection 6 | 7 | EXAMPLE_REQUEST_HEADERS_BYTES = [ 8 | (b":authority", b"example.com"), 9 | (b":path", b"/"), 10 | (b":scheme", b"https"), 11 | (b":method", b"HEAD"), 12 | ] 13 | 14 | EXAMPLE_REQUEST_HEADERS = [ 15 | (":authority", "example.com"), 16 | (":path", "/"), 17 | (":scheme", "https"), 18 | (":method", "HEAD"), 19 | ] 20 | 21 | 22 | class TestHeadRequest: 23 | example_response_headers = [ 24 | (b":status", b"200"), 25 | (b"server", b"fake-serv/0.1.0"), 26 | (b"content_length", b"1"), 27 | ] 28 | 29 | @pytest.mark.parametrize("headers", [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) 30 | def test_non_zero_content_and_no_body(self, frame_factory, headers) -> None: 31 | c = h2.connection.H2Connection() 32 | c.initiate_connection() 33 | c.send_headers(1, headers, end_stream=True) 34 | 35 | f = frame_factory.build_headers_frame( 36 | self.example_response_headers, 37 | flags=["END_STREAM"], 38 | ) 39 | events = c.receive_data(f.serialize()) 40 | 41 | assert len(events) == 2 42 | event = events[0] 43 | 44 | assert isinstance(event, h2.events.ResponseReceived) 45 | assert event.stream_id == 1 46 | assert event.headers == self.example_response_headers 47 | 48 | @pytest.mark.parametrize("headers", [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) 49 | def test_reject_non_zero_content_and_body(self, frame_factory, headers) -> None: 50 | c = h2.connection.H2Connection() 51 | c.initiate_connection() 52 | c.send_headers(1, headers) 53 | 54 | headers = frame_factory.build_headers_frame( 55 | self.example_response_headers, 56 | ) 57 | data = frame_factory.build_data_frame(data=b"\x01") 58 | 59 | c.receive_data(headers.serialize()) 60 | with pytest.raises(h2.exceptions.InvalidBodyLengthError): 61 | c.receive_data(data.serialize()) 62 | -------------------------------------------------------------------------------- /tests/test_interacting_stacks.py: -------------------------------------------------------------------------------- 1 | """ 2 | These tests run two entities, a client and a server, in parallel threads. These 3 | two entities talk to each other, running what amounts to a number of carefully 4 | controlled simulations of real flows. 5 | 6 | This is to ensure that the stack as a whole behaves intelligently in both 7 | client and server cases. 8 | 9 | These tests are long, complex, and somewhat brittle, so they aren't in general 10 | recommended for writing the majority of test cases. Their purposes is primarily 11 | to validate that the top-level API of the library behaves as described. 12 | 13 | We should also consider writing helper functions to reduce the complexity of 14 | these tests, so that they can be written more easily, as they are remarkably 15 | useful. 16 | """ 17 | from __future__ import annotations 18 | 19 | import pytest 20 | 21 | import h2.config 22 | import h2.connection 23 | import h2.events 24 | import h2.settings 25 | 26 | from . import coroutine_tests 27 | 28 | 29 | class TestCommunication(coroutine_tests.CoroutineTestCase): 30 | """ 31 | Test that two communicating state machines can work together. 32 | """ 33 | 34 | server_config = h2.config.H2Configuration(client_side=False) 35 | 36 | request_headers = [ 37 | (":method", "GET"), 38 | (":path", "/"), 39 | (":authority", "example.com"), 40 | (":scheme", "https"), 41 | ("user-agent", "test-client/0.1.0"), 42 | ] 43 | 44 | request_headers_bytes = [ 45 | (b":method", b"GET"), 46 | (b":path", b"/"), 47 | (b":authority", b"example.com"), 48 | (b":scheme", b"https"), 49 | (b"user-agent", b"test-client/0.1.0"), 50 | ] 51 | 52 | response_headers = [ 53 | (b":status", b"204"), 54 | (b"server", b"test-server/0.1.0"), 55 | (b"content-length", b"0"), 56 | ] 57 | 58 | @pytest.mark.parametrize("request_headers", [request_headers, request_headers_bytes]) 59 | def test_basic_request_response(self, request_headers) -> None: 60 | """ 61 | A request issued by hyper-h2 can be responded to by hyper-h2. 62 | """ 63 | def client(): 64 | c = h2.connection.H2Connection() 65 | 66 | # Do the handshake. First send the preamble. 67 | c.initiate_connection() 68 | data = yield c.data_to_send() 69 | 70 | # Next, handle the remote preamble. 71 | events = c.receive_data(data) 72 | assert len(events) == 2 73 | assert isinstance(events[0], h2.events.SettingsAcknowledged) 74 | assert isinstance(events[1], h2.events.RemoteSettingsChanged) 75 | changed = events[1].changed_settings 76 | assert ( 77 | changed[ 78 | h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS 79 | ].new_value == 100 80 | ) 81 | 82 | # Send a request. 83 | events = c.send_headers(1, request_headers, end_stream=True) 84 | assert not events 85 | data = yield c.data_to_send() 86 | 87 | # Validate the response. 88 | events = c.receive_data(data) 89 | assert len(events) == 2 90 | assert isinstance(events[0], h2.events.ResponseReceived) 91 | assert events[0].stream_id == 1 92 | assert events[0].headers == self.response_headers 93 | assert isinstance(events[1], h2.events.StreamEnded) 94 | assert events[1].stream_id == 1 95 | 96 | @self.server 97 | def server(): 98 | c = h2.connection.H2Connection(config=self.server_config) 99 | 100 | # First, read for the preamble. 101 | data = yield 102 | events = c.receive_data(data) 103 | assert len(events) == 1 104 | assert isinstance(events[0], h2.events.RemoteSettingsChanged) 105 | changed = events[0].changed_settings 106 | assert ( 107 | changed[ 108 | h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS 109 | ].new_value == 100 110 | ) 111 | 112 | # Send our preamble back. 113 | c.initiate_connection() 114 | data = yield c.data_to_send() 115 | 116 | # Listen for the request. 117 | events = c.receive_data(data) 118 | assert len(events) == 3 119 | assert isinstance(events[0], h2.events.SettingsAcknowledged) 120 | assert isinstance(events[1], h2.events.RequestReceived) 121 | assert events[1].stream_id == 1 122 | assert events[1].headers == self.request_headers_bytes 123 | assert isinstance(events[2], h2.events.StreamEnded) 124 | assert events[2].stream_id == 1 125 | 126 | # Send our response. 127 | events = c.send_headers(1, self.response_headers, end_stream=True) 128 | assert not events 129 | yield c.data_to_send() 130 | 131 | self.run_until_complete(client(), server()) 132 | -------------------------------------------------------------------------------- /tests/test_invalid_content_lengths.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains tests that use invalid content lengths, and validates that 3 | they fail appropriately. 4 | """ 5 | from __future__ import annotations 6 | 7 | import pytest 8 | 9 | import h2.config 10 | import h2.connection 11 | import h2.errors 12 | import h2.events 13 | import h2.exceptions 14 | 15 | 16 | class TestInvalidContentLengths: 17 | """ 18 | Hyper-h2 raises Protocol Errors when the content-length sent by a remote 19 | peer is not valid. 20 | """ 21 | 22 | example_request_headers = [ 23 | (":authority", "example.com"), 24 | (":path", "/"), 25 | (":scheme", "https"), 26 | (":method", "POST"), 27 | ("content-length", "15"), 28 | ] 29 | example_request_headers_bytes = [ 30 | (b":authority", b"example.com"), 31 | (b":path", b"/"), 32 | (b":scheme", b"https"), 33 | (b":method", b"POST"), 34 | (b"content-length", b"15"), 35 | ] 36 | example_response_headers = [ 37 | (":status", "200"), 38 | ("server", "fake-serv/0.1.0"), 39 | ] 40 | server_config = h2.config.H2Configuration(client_side=False) 41 | 42 | @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) 43 | def test_too_much_data(self, frame_factory, request_headers) -> None: 44 | """ 45 | Remote peers sending data in excess of content-length causes Protocol 46 | Errors. 47 | """ 48 | c = h2.connection.H2Connection(config=self.server_config) 49 | c.initiate_connection() 50 | c.receive_data(frame_factory.preamble()) 51 | 52 | headers = frame_factory.build_headers_frame( 53 | headers=request_headers, 54 | ) 55 | first_data = frame_factory.build_data_frame(data=b"\x01"*15) 56 | c.receive_data(headers.serialize() + first_data.serialize()) 57 | c.clear_outbound_data_buffer() 58 | 59 | second_data = frame_factory.build_data_frame(data=b"\x01") 60 | with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp: 61 | c.receive_data(second_data.serialize()) 62 | 63 | assert exp.value.expected_length == 15 64 | assert exp.value.actual_length == 16 65 | assert str(exp.value) == ( 66 | "InvalidBodyLengthError: Expected 15 bytes, received 16" 67 | ) 68 | 69 | expected_frame = frame_factory.build_goaway_frame( 70 | last_stream_id=1, 71 | error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR, 72 | ) 73 | assert c.data_to_send() == expected_frame.serialize() 74 | 75 | @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) 76 | def test_insufficient_data(self, frame_factory, request_headers) -> None: 77 | """ 78 | Remote peers sending less data than content-length causes Protocol 79 | Errors. 80 | """ 81 | c = h2.connection.H2Connection(config=self.server_config) 82 | c.initiate_connection() 83 | c.receive_data(frame_factory.preamble()) 84 | 85 | headers = frame_factory.build_headers_frame( 86 | headers=request_headers, 87 | ) 88 | first_data = frame_factory.build_data_frame(data=b"\x01"*13) 89 | c.receive_data(headers.serialize() + first_data.serialize()) 90 | c.clear_outbound_data_buffer() 91 | 92 | second_data = frame_factory.build_data_frame( 93 | data=b"\x01", 94 | flags=["END_STREAM"], 95 | ) 96 | with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp: 97 | c.receive_data(second_data.serialize()) 98 | 99 | assert exp.value.expected_length == 15 100 | assert exp.value.actual_length == 14 101 | assert str(exp.value) == ( 102 | "InvalidBodyLengthError: Expected 15 bytes, received 14" 103 | ) 104 | 105 | expected_frame = frame_factory.build_goaway_frame( 106 | last_stream_id=1, 107 | error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR, 108 | ) 109 | assert c.data_to_send() == expected_frame.serialize() 110 | 111 | @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) 112 | def test_insufficient_data_empty_frame(self, frame_factory, request_headers) -> None: 113 | """ 114 | Remote peers sending less data than content-length where the last data 115 | frame is empty causes Protocol Errors. 116 | """ 117 | c = h2.connection.H2Connection(config=self.server_config) 118 | c.initiate_connection() 119 | c.receive_data(frame_factory.preamble()) 120 | 121 | headers = frame_factory.build_headers_frame( 122 | headers=request_headers, 123 | ) 124 | first_data = frame_factory.build_data_frame(data=b"\x01"*14) 125 | c.receive_data(headers.serialize() + first_data.serialize()) 126 | c.clear_outbound_data_buffer() 127 | 128 | second_data = frame_factory.build_data_frame( 129 | data=b"", 130 | flags=["END_STREAM"], 131 | ) 132 | with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp: 133 | c.receive_data(second_data.serialize()) 134 | 135 | assert exp.value.expected_length == 15 136 | assert exp.value.actual_length == 14 137 | assert str(exp.value) == ( 138 | "InvalidBodyLengthError: Expected 15 bytes, received 14" 139 | ) 140 | 141 | expected_frame = frame_factory.build_goaway_frame( 142 | last_stream_id=1, 143 | error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR, 144 | ) 145 | assert c.data_to_send() == expected_frame.serialize() 146 | -------------------------------------------------------------------------------- /tests/test_rfc8441.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the RFC 8441 extended connect request support. 3 | """ 4 | from __future__ import annotations 5 | 6 | import pytest 7 | 8 | import h2.config 9 | import h2.connection 10 | import h2.events 11 | from h2.utilities import utf8_encode_headers 12 | 13 | 14 | class TestRFC8441: 15 | """ 16 | Tests that the client supports sending an extended connect request 17 | and the server supports receiving it. 18 | """ 19 | 20 | headers = [ 21 | (":authority", "example.com"), 22 | (":path", "/"), 23 | (":scheme", "https"), 24 | (":method", "CONNECT"), 25 | (":protocol", "websocket"), 26 | ("user-agent", "someua/0.0.1"), 27 | ] 28 | 29 | headers_bytes = [ 30 | (b":authority", b"example.com"), 31 | (b":path", b"/"), 32 | (b":scheme", b"https"), 33 | (b":method", b"CONNECT"), 34 | (b":protocol", b"websocket"), 35 | (b"user-agent", b"someua/0.0.1"), 36 | ] 37 | 38 | @pytest.mark.parametrize("headers", [headers, headers_bytes]) 39 | def test_can_send_headers(self, frame_factory, headers) -> None: 40 | client = h2.connection.H2Connection() 41 | client.initiate_connection() 42 | client.send_headers(stream_id=1, headers=headers) 43 | 44 | server = h2.connection.H2Connection( 45 | config=h2.config.H2Configuration(client_side=False), 46 | ) 47 | events = server.receive_data(client.data_to_send()) 48 | event = events[1] 49 | assert isinstance(event, h2.events.RequestReceived) 50 | assert event.stream_id == 1 51 | assert event.headers == utf8_encode_headers(headers) 52 | -------------------------------------------------------------------------------- /tests/test_state_machines.py: -------------------------------------------------------------------------------- 1 | """ 2 | These tests validate the state machines directly. Writing meaningful tests for 3 | this case can be tricky, so the majority of these tests use Hypothesis to try 4 | to talk about general behaviours rather than specific cases. 5 | """ 6 | from __future__ import annotations 7 | 8 | import pytest 9 | from hypothesis import given 10 | from hypothesis.strategies import sampled_from 11 | 12 | import h2.connection 13 | import h2.exceptions 14 | import h2.stream 15 | 16 | 17 | class TestConnectionStateMachine: 18 | """ 19 | Tests of the connection state machine. 20 | """ 21 | 22 | @given(state=sampled_from(h2.connection.ConnectionState), 23 | input_=sampled_from(h2.connection.ConnectionInputs)) 24 | def test_state_transitions(self, state, input_) -> None: 25 | c = h2.connection.H2ConnectionStateMachine() 26 | c.state = state 27 | 28 | try: 29 | c.process_input(input_) 30 | except h2.exceptions.ProtocolError: 31 | assert c.state == h2.connection.ConnectionState.CLOSED 32 | else: 33 | assert c.state in h2.connection.ConnectionState 34 | 35 | def test_state_machine_only_allows_connection_states(self) -> None: 36 | """ 37 | The Connection state machine only allows ConnectionState inputs. 38 | """ 39 | c = h2.connection.H2ConnectionStateMachine() 40 | 41 | with pytest.raises(ValueError): 42 | c.process_input(1) 43 | 44 | @pytest.mark.parametrize( 45 | "state", 46 | ( 47 | s for s in h2.connection.ConnectionState 48 | if s != h2.connection.ConnectionState.CLOSED 49 | ), 50 | ) 51 | @pytest.mark.parametrize( 52 | "input_", 53 | [ 54 | h2.connection.ConnectionInputs.RECV_PRIORITY, 55 | h2.connection.ConnectionInputs.SEND_PRIORITY, 56 | ], 57 | ) 58 | def test_priority_frames_allowed_in_all_states(self, state, input_) -> None: 59 | """ 60 | Priority frames can be sent/received in all connection states except 61 | closed. 62 | """ 63 | c = h2.connection.H2ConnectionStateMachine() 64 | c.state = state 65 | 66 | c.process_input(input_) 67 | 68 | 69 | class TestStreamStateMachine: 70 | """ 71 | Tests of the stream state machine. 72 | """ 73 | 74 | @given(state=sampled_from(h2.stream.StreamState), 75 | input_=sampled_from(h2.stream.StreamInputs)) 76 | def test_state_transitions(self, state, input_) -> None: 77 | s = h2.stream.H2StreamStateMachine(stream_id=1) 78 | s.state = state 79 | 80 | try: 81 | s.process_input(input_) 82 | except h2.exceptions.StreamClosedError: 83 | # This can only happen for streams that started in the closed 84 | # state OR where the input was RECV_DATA and the state was not 85 | # OPEN or HALF_CLOSED_LOCAL OR where the state was 86 | # HALF_CLOSED_REMOTE and a frame was received. 87 | if state == h2.stream.StreamState.CLOSED: 88 | assert s.state == h2.stream.StreamState.CLOSED 89 | elif input_ == h2.stream.StreamInputs.RECV_DATA: 90 | assert s.state == h2.stream.StreamState.CLOSED 91 | assert state not in ( 92 | h2.stream.StreamState.OPEN, 93 | h2.stream.StreamState.HALF_CLOSED_LOCAL, 94 | ) 95 | elif state == h2.stream.StreamState.HALF_CLOSED_REMOTE: 96 | assert input_ in ( 97 | h2.stream.StreamInputs.RECV_HEADERS, 98 | h2.stream.StreamInputs.RECV_PUSH_PROMISE, 99 | h2.stream.StreamInputs.RECV_DATA, 100 | h2.stream.StreamInputs.RECV_CONTINUATION, 101 | ) 102 | except h2.exceptions.ProtocolError: 103 | assert s.state == h2.stream.StreamState.CLOSED 104 | else: 105 | assert s.state in h2.stream.StreamState 106 | 107 | def test_state_machine_only_allows_stream_states(self) -> None: 108 | """ 109 | The Stream state machine only allows StreamState inputs. 110 | """ 111 | s = h2.stream.H2StreamStateMachine(stream_id=1) 112 | 113 | with pytest.raises(ValueError): 114 | s.process_input(1) 115 | 116 | def test_stream_state_machine_forbids_pushes_on_server_streams(self) -> None: 117 | """ 118 | Streams where this peer is a server do not allow receiving pushed 119 | frames. 120 | """ 121 | s = h2.stream.H2StreamStateMachine(stream_id=1) 122 | s.process_input(h2.stream.StreamInputs.RECV_HEADERS) 123 | 124 | with pytest.raises(h2.exceptions.ProtocolError): 125 | s.process_input(h2.stream.StreamInputs.RECV_PUSH_PROMISE) 126 | 127 | def test_stream_state_machine_forbids_sending_pushes_from_clients(self) -> None: 128 | """ 129 | Streams where this peer is a client do not allow sending pushed frames. 130 | """ 131 | s = h2.stream.H2StreamStateMachine(stream_id=1) 132 | s.process_input(h2.stream.StreamInputs.SEND_HEADERS) 133 | 134 | with pytest.raises(h2.exceptions.ProtocolError): 135 | s.process_input(h2.stream.StreamInputs.SEND_PUSH_PROMISE) 136 | 137 | @pytest.mark.parametrize( 138 | "input_", 139 | [ 140 | h2.stream.StreamInputs.SEND_HEADERS, 141 | h2.stream.StreamInputs.SEND_PUSH_PROMISE, 142 | h2.stream.StreamInputs.SEND_RST_STREAM, 143 | h2.stream.StreamInputs.SEND_DATA, 144 | h2.stream.StreamInputs.SEND_WINDOW_UPDATE, 145 | h2.stream.StreamInputs.SEND_END_STREAM, 146 | ], 147 | ) 148 | def test_cannot_send_on_closed_streams(self, input_) -> None: 149 | """ 150 | Sending anything but a PRIORITY frame is forbidden on closed streams. 151 | """ 152 | c = h2.stream.H2StreamStateMachine(stream_id=1) 153 | c.state = h2.stream.StreamState.CLOSED 154 | 155 | expected_error = ( 156 | h2.exceptions.ProtocolError 157 | if input_ == h2.stream.StreamInputs.SEND_PUSH_PROMISE 158 | else h2.exceptions.StreamClosedError 159 | ) 160 | 161 | with pytest.raises(expected_error): 162 | c.process_input(input_) 163 | -------------------------------------------------------------------------------- /tests/test_stream_reset.py: -------------------------------------------------------------------------------- 1 | """ 2 | More complex tests that exercise stream resetting functionality to validate 3 | that connection state is appropriately maintained. 4 | 5 | Specifically, these tests validate that streams that have been reset accurately 6 | keep track of connection-level state. 7 | """ 8 | from __future__ import annotations 9 | 10 | import pytest 11 | 12 | import h2.connection 13 | import h2.errors 14 | import h2.events 15 | 16 | 17 | class TestStreamReset: 18 | """ 19 | Tests for resetting streams. 20 | """ 21 | 22 | example_request_headers = [ 23 | (b":authority", b"example.com"), 24 | (b":path", b"/"), 25 | (b":scheme", b"https"), 26 | (b":method", b"GET"), 27 | ] 28 | example_response_headers = [ 29 | (b":status", b"200"), 30 | (b"server", b"fake-serv/0.1.0"), 31 | (b"content-length", b"0"), 32 | ] 33 | 34 | def test_reset_stream_keeps_header_state_correct(self, frame_factory) -> None: 35 | """ 36 | A stream that has been reset still affects the header decoder. 37 | """ 38 | c = h2.connection.H2Connection() 39 | c.initiate_connection() 40 | c.send_headers(stream_id=1, headers=self.example_request_headers) 41 | c.reset_stream(stream_id=1) 42 | c.send_headers(stream_id=3, headers=self.example_request_headers) 43 | c.clear_outbound_data_buffer() 44 | 45 | f = frame_factory.build_headers_frame( 46 | headers=self.example_response_headers, stream_id=1, 47 | ) 48 | rst_frame = frame_factory.build_rst_stream_frame( 49 | 1, h2.errors.ErrorCodes.STREAM_CLOSED, 50 | ) 51 | events = c.receive_data(f.serialize()) 52 | assert not events 53 | assert c.data_to_send() == rst_frame.serialize() 54 | 55 | # This works because the header state should be intact from the headers 56 | # frame that was send on stream 1, so they should decode cleanly. 57 | f = frame_factory.build_headers_frame( 58 | headers=self.example_response_headers, stream_id=3, 59 | ) 60 | event = c.receive_data(f.serialize())[0] 61 | 62 | assert isinstance(event, h2.events.ResponseReceived) 63 | assert event.stream_id == 3 64 | assert event.headers == self.example_response_headers 65 | 66 | @pytest.mark.parametrize(("close_id", "other_id"), [(1, 3), (3, 1)]) 67 | def test_reset_stream_keeps_flow_control_correct(self, 68 | close_id, 69 | other_id, 70 | frame_factory) -> None: 71 | """ 72 | A stream that has been reset does not affect the connection flow 73 | control window. 74 | """ 75 | c = h2.connection.H2Connection() 76 | c.initiate_connection() 77 | c.send_headers(stream_id=1, headers=self.example_request_headers) 78 | c.send_headers(stream_id=3, headers=self.example_request_headers) 79 | 80 | # Record the initial window size. 81 | initial_window = c.remote_flow_control_window(stream_id=other_id) 82 | 83 | f = frame_factory.build_headers_frame( 84 | headers=self.example_response_headers, stream_id=close_id, 85 | ) 86 | c.receive_data(f.serialize()) 87 | c.reset_stream(stream_id=close_id) 88 | c.clear_outbound_data_buffer() 89 | 90 | f = frame_factory.build_data_frame( 91 | data=b"some data", 92 | stream_id=close_id, 93 | ) 94 | c.receive_data(f.serialize()) 95 | 96 | expected = frame_factory.build_rst_stream_frame( 97 | stream_id=close_id, 98 | error_code=h2.errors.ErrorCodes.STREAM_CLOSED, 99 | ).serialize() 100 | assert c.data_to_send() == expected 101 | 102 | new_window = c.remote_flow_control_window(stream_id=other_id) 103 | assert initial_window - len(b"some data") == new_window 104 | 105 | @pytest.mark.parametrize("clear_streams", [True, False]) 106 | def test_reset_stream_automatically_resets_pushed_streams(self, 107 | frame_factory, 108 | clear_streams) -> None: 109 | """ 110 | Resetting a stream causes RST_STREAM frames to be automatically emitted 111 | to close any streams pushed after the reset. 112 | """ 113 | c = h2.connection.H2Connection() 114 | c.initiate_connection() 115 | c.send_headers(stream_id=1, headers=self.example_request_headers) 116 | c.reset_stream(stream_id=1) 117 | c.clear_outbound_data_buffer() 118 | 119 | if clear_streams: 120 | # Call open_outbound_streams to force the connection to clean 121 | # closed streams. 122 | c.open_outbound_streams 123 | 124 | f = frame_factory.build_push_promise_frame( 125 | stream_id=1, 126 | promised_stream_id=2, 127 | headers=self.example_request_headers, 128 | ) 129 | events = c.receive_data(f.serialize()) 130 | assert not events 131 | 132 | f = frame_factory.build_rst_stream_frame( 133 | stream_id=2, 134 | error_code=h2.errors.ErrorCodes.REFUSED_STREAM, 135 | ) 136 | assert c.data_to_send() == f.serialize() 137 | -------------------------------------------------------------------------------- /tests/test_utility_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the various utility functions provided by hyper-h2. 3 | """ 4 | from __future__ import annotations 5 | 6 | import pytest 7 | 8 | import h2.config 9 | import h2.connection 10 | import h2.errors 11 | import h2.events 12 | import h2.exceptions 13 | from h2.utilities import SizeLimitDict, extract_method_header 14 | 15 | 16 | class TestGetNextAvailableStreamID: 17 | """ 18 | Tests for the ``H2Connection.get_next_available_stream_id`` method. 19 | """ 20 | 21 | example_request_headers = [ 22 | (":authority", "example.com"), 23 | (":path", "/"), 24 | (":scheme", "https"), 25 | (":method", "GET"), 26 | ] 27 | example_response_headers = [ 28 | (":status", "200"), 29 | ("server", "fake-serv/0.1.0"), 30 | ] 31 | server_config = h2.config.H2Configuration(client_side=False) 32 | 33 | def test_returns_correct_sequence_for_clients(self, frame_factory) -> None: 34 | """ 35 | For a client connection, the correct sequence of stream IDs is 36 | returned. 37 | """ 38 | # Running the exhaustive version of this test (all 1 billion available 39 | # stream IDs) is too painful. For that reason, we validate that the 40 | # original sequence is right for the first few thousand, and then just 41 | # check that it terminates properly. 42 | # 43 | # Make sure that the streams get cleaned up: 8k streams floating 44 | # around would make this test memory-hard, and it's not supposed to be 45 | # a test of how much RAM your machine has. 46 | c = h2.connection.H2Connection() 47 | c.initiate_connection() 48 | initial_sequence = range(1, 2**13, 2) 49 | 50 | for expected_stream_id in initial_sequence: 51 | stream_id = c.get_next_available_stream_id() 52 | assert stream_id == expected_stream_id 53 | 54 | c.send_headers( 55 | stream_id=stream_id, 56 | headers=self.example_request_headers, 57 | end_stream=True, 58 | ) 59 | f = frame_factory.build_headers_frame( 60 | headers=self.example_response_headers, 61 | stream_id=stream_id, 62 | flags=["END_STREAM"], 63 | ) 64 | c.receive_data(f.serialize()) 65 | c.clear_outbound_data_buffer() 66 | 67 | # Jump up to the last available stream ID. Don't clean up the stream 68 | # here because who cares about one stream. 69 | last_client_id = 2**31 - 1 70 | c.send_headers( 71 | stream_id=last_client_id, 72 | headers=self.example_request_headers, 73 | end_stream=True, 74 | ) 75 | 76 | with pytest.raises(h2.exceptions.NoAvailableStreamIDError): 77 | c.get_next_available_stream_id() 78 | 79 | def test_returns_correct_sequence_for_servers(self, frame_factory) -> None: 80 | """ 81 | For a server connection, the correct sequence of stream IDs is 82 | returned. 83 | """ 84 | # Running the exhaustive version of this test (all 1 billion available 85 | # stream IDs) is too painful. For that reason, we validate that the 86 | # original sequence is right for the first few thousand, and then just 87 | # check that it terminates properly. 88 | # 89 | # Make sure that the streams get cleaned up: 8k streams floating 90 | # around would make this test memory-hard, and it's not supposed to be 91 | # a test of how much RAM your machine has. 92 | c = h2.connection.H2Connection(config=self.server_config) 93 | c.initiate_connection() 94 | c.receive_data(frame_factory.preamble()) 95 | f = frame_factory.build_headers_frame( 96 | headers=self.example_request_headers, 97 | ) 98 | c.receive_data(f.serialize()) 99 | 100 | initial_sequence = range(2, 2**13, 2) 101 | 102 | for expected_stream_id in initial_sequence: 103 | stream_id = c.get_next_available_stream_id() 104 | assert stream_id == expected_stream_id 105 | 106 | c.push_stream( 107 | stream_id=1, 108 | promised_stream_id=stream_id, 109 | request_headers=self.example_request_headers, 110 | ) 111 | c.send_headers( 112 | stream_id=stream_id, 113 | headers=self.example_response_headers, 114 | end_stream=True, 115 | ) 116 | c.clear_outbound_data_buffer() 117 | 118 | # Jump up to the last available stream ID. Don't clean up the stream 119 | # here because who cares about one stream. 120 | last_server_id = 2**31 - 2 121 | c.push_stream( 122 | stream_id=1, 123 | promised_stream_id=last_server_id, 124 | request_headers=self.example_request_headers, 125 | ) 126 | 127 | with pytest.raises(h2.exceptions.NoAvailableStreamIDError): 128 | c.get_next_available_stream_id() 129 | 130 | def test_does_not_increment_without_stream_send(self) -> None: 131 | """ 132 | If a new stream isn't actually created, the next stream ID doesn't 133 | change. 134 | """ 135 | c = h2.connection.H2Connection() 136 | c.initiate_connection() 137 | 138 | first_stream_id = c.get_next_available_stream_id() 139 | second_stream_id = c.get_next_available_stream_id() 140 | 141 | assert first_stream_id == second_stream_id 142 | 143 | c.send_headers( 144 | stream_id=first_stream_id, 145 | headers=self.example_request_headers, 146 | ) 147 | 148 | third_stream_id = c.get_next_available_stream_id() 149 | assert third_stream_id == (first_stream_id + 2) 150 | 151 | 152 | class TestExtractHeader: 153 | 154 | example_headers_with_bytes = [ 155 | (b":authority", b"example.com"), 156 | (b":path", b"/"), 157 | (b":scheme", b"https"), 158 | (b":method", b"GET"), 159 | ] 160 | 161 | def test_extract_header_method(self) -> None: 162 | assert extract_method_header( 163 | self.example_headers_with_bytes, 164 | ) == b"GET" 165 | 166 | 167 | def test_size_limit_dict_limit() -> None: 168 | dct = SizeLimitDict(size_limit=2) 169 | 170 | dct[1] = 1 171 | dct[2] = 2 172 | 173 | assert len(dct) == 2 174 | assert dct[1] == 1 175 | assert dct[2] == 2 176 | 177 | dct[3] = 3 178 | 179 | assert len(dct) == 2 180 | assert dct[2] == 2 181 | assert dct[3] == 3 182 | assert 1 not in dct 183 | 184 | 185 | def test_size_limit_dict_limit_init() -> None: 186 | initial_dct = { 187 | 1: 1, 188 | 2: 2, 189 | 3: 3, 190 | } 191 | 192 | dct = SizeLimitDict(initial_dct, size_limit=2) 193 | 194 | assert len(dct) == 2 195 | 196 | 197 | def test_size_limit_dict_no_limit() -> None: 198 | dct = SizeLimitDict(size_limit=None) 199 | 200 | dct[1] = 1 201 | dct[2] = 2 202 | 203 | assert len(dct) == 2 204 | assert dct[1] == 1 205 | assert dct[2] == 2 206 | 207 | dct[3] = 3 208 | 209 | assert len(dct) == 3 210 | assert dct[1] == 1 211 | assert dct[2] == 2 212 | assert dct[3] == 3 213 | -------------------------------------------------------------------------------- /visualizer/NOTICES.visualizer: -------------------------------------------------------------------------------- 1 | This module contains code inspired by and borrowed from Automat. That code was 2 | made available under the following license: 3 | 4 | Copyright (c) 2014 5 | Rackspace 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /visualizer/visualize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | State Machine Visualizer 4 | ~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | This code provides a module that can use graphviz to visualise the state 7 | machines included in h2. These visualisations can be used as part of the 8 | documentation of h2, and as a reference material to understand how the 9 | state machines function. 10 | 11 | The code in this module is heavily inspired by code in Automat, which can be 12 | found here: https://github.com/glyph/automat. For details on the licensing of 13 | Automat, please see the NOTICES.visualizer file in this folder. 14 | 15 | This module is very deliberately not shipped with the rest of h2. This is 16 | because it is of minimal value to users who are installing h2: its use 17 | is only really for the developers of h2. 18 | """ 19 | import argparse 20 | import collections 21 | import sys 22 | 23 | import graphviz 24 | import graphviz.files 25 | 26 | import h2.connection 27 | import h2.stream 28 | 29 | 30 | StateMachine = collections.namedtuple( 31 | 'StateMachine', ['fqdn', 'machine', 'states', 'inputs', 'transitions'] 32 | ) 33 | 34 | 35 | # This is all the state machines we currently know about and will render. 36 | # If any new state machines are added, they should be inserted here. 37 | STATE_MACHINES = [ 38 | StateMachine( 39 | fqdn='h2.connection.H2ConnectionStateMachine', 40 | machine=h2.connection.H2ConnectionStateMachine, 41 | states=h2.connection.ConnectionState, 42 | inputs=h2.connection.ConnectionInputs, 43 | transitions=h2.connection.H2ConnectionStateMachine._transitions, 44 | ), 45 | StateMachine( 46 | fqdn='h2.stream.H2StreamStateMachine', 47 | machine=h2.stream.H2StreamStateMachine, 48 | states=h2.stream.StreamState, 49 | inputs=h2.stream.StreamInputs, 50 | transitions=h2.stream._transitions, 51 | ), 52 | ] 53 | 54 | 55 | def quote(s): 56 | return '"{}"'.format(s.replace('"', r'\"')) 57 | 58 | 59 | def html(s): 60 | return '<{}>'.format(s) 61 | 62 | 63 | def element(name, *children, **attrs): 64 | """ 65 | Construct a string from the HTML element description. 66 | """ 67 | formatted_attributes = ' '.join( 68 | '{}={}'.format(key, quote(str(value))) 69 | for key, value in sorted(attrs.items()) 70 | ) 71 | formatted_children = ''.join(children) 72 | return u'<{name} {attrs}>{children}'.format( 73 | name=name, 74 | attrs=formatted_attributes, 75 | children=formatted_children 76 | ) 77 | 78 | 79 | def row_for_output(event, side_effect): 80 | """ 81 | Given an output tuple (an event and its side effect), generates a table row 82 | from it. 83 | """ 84 | point_size = {'point-size': '9'} 85 | event_cell = element( 86 | "td", 87 | element("font", enum_member_name(event), **point_size) 88 | ) 89 | side_effect_name = ( 90 | function_name(side_effect) if side_effect is not None else "None" 91 | ) 92 | side_effect_cell = element( 93 | "td", 94 | element("font", side_effect_name, **point_size) 95 | ) 96 | return element("tr", event_cell, side_effect_cell) 97 | 98 | 99 | def table_maker(initial_state, final_state, outputs, port): 100 | """ 101 | Construct an HTML table to label a state transition. 102 | """ 103 | header = "{} -> {}".format( 104 | enum_member_name(initial_state), enum_member_name(final_state) 105 | ) 106 | header_row = element( 107 | "tr", 108 | element( 109 | "td", 110 | element( 111 | "font", 112 | header, 113 | face="menlo-italic" 114 | ), 115 | port=port, 116 | colspan="2", 117 | ) 118 | ) 119 | rows = [header_row] 120 | rows.extend(row_for_output(*output) for output in outputs) 121 | return element("table", *rows) 122 | 123 | 124 | def enum_member_name(state): 125 | """ 126 | All enum member names have the form .. For 127 | our rendering we only want the member name, so we take their representation 128 | and split it. 129 | """ 130 | return str(state).split('.', 1)[1] 131 | 132 | 133 | def function_name(func): 134 | """ 135 | Given a side-effect function, return its string name. 136 | """ 137 | return func.__name__ 138 | 139 | 140 | def build_digraph(state_machine): 141 | """ 142 | Produce a L{graphviz.Digraph} object from a state machine. 143 | """ 144 | digraph = graphviz.Digraph(node_attr={'fontname': 'Menlo'}, 145 | edge_attr={'fontname': 'Menlo'}, 146 | graph_attr={'dpi': '200'}) 147 | 148 | # First, add the states as nodes. 149 | seen_first_state = False 150 | for state in state_machine.states: 151 | if not seen_first_state: 152 | state_shape = "bold" 153 | font_name = "Menlo-Bold" 154 | else: 155 | state_shape = "" 156 | font_name = "Menlo" 157 | digraph.node(enum_member_name(state), 158 | fontame=font_name, 159 | shape="ellipse", 160 | style=state_shape, 161 | color="blue") 162 | seen_first_state = True 163 | 164 | # We frequently have vary many inputs that all trigger the same state 165 | # transition, and only differ in terms of their input and side-effect. It 166 | # would be polite to say that graphviz does not handle this very well. So 167 | # instead we *collapse* the state transitions all into the one edge, and 168 | # then provide a label that displays a table of all the inputs and their 169 | # associated side effects. 170 | transitions = collections.defaultdict(list) 171 | for transition in state_machine.transitions.items(): 172 | initial_state, event = transition[0] 173 | side_effect, final_state = transition[1] 174 | transition_key = (initial_state, final_state) 175 | transitions[transition_key].append((event, side_effect)) 176 | 177 | for n, (transition_key, outputs) in enumerate(transitions.items()): 178 | this_transition = "t{}".format(n) 179 | initial_state, final_state = transition_key 180 | 181 | port = "tableport" 182 | table = table_maker( 183 | initial_state=initial_state, 184 | final_state=final_state, 185 | outputs=outputs, 186 | port=port 187 | ) 188 | 189 | digraph.node(this_transition, 190 | label=html(table), margin="0.2", shape="none") 191 | 192 | digraph.edge(enum_member_name(initial_state), 193 | '{}:{}:w'.format(this_transition, port), 194 | arrowhead="none") 195 | digraph.edge('{}:{}:e'.format(this_transition, port), 196 | enum_member_name(final_state)) 197 | 198 | return digraph 199 | 200 | 201 | def main(): 202 | """ 203 | Renders all the state machines in h2 into images. 204 | """ 205 | program_name = sys.argv[0] 206 | argv = sys.argv[1:] 207 | 208 | description = """ 209 | Visualize h2 state machines as graphs. 210 | """ 211 | epilog = """ 212 | You must have the graphviz tool suite installed. Please visit 213 | http://www.graphviz.org for more information. 214 | """ 215 | 216 | argument_parser = argparse.ArgumentParser( 217 | prog=program_name, 218 | description=description, 219 | epilog=epilog 220 | ) 221 | argument_parser.add_argument( 222 | '--image-directory', 223 | '-i', 224 | help="Where to write out image files.", 225 | default=".h2_visualize" 226 | ) 227 | argument_parser.add_argument( 228 | '--view', 229 | '-v', 230 | help="View rendered graphs with default image viewer", 231 | default=False, 232 | action="store_true" 233 | ) 234 | args = argument_parser.parse_args(argv) 235 | 236 | for state_machine in STATE_MACHINES: 237 | print(state_machine.fqdn, '...discovered') 238 | 239 | digraph = build_digraph(state_machine) 240 | 241 | if args.image_directory: 242 | digraph.format = "png" 243 | digraph.render(filename="{}.dot".format(state_machine.fqdn), 244 | directory=args.image_directory, 245 | view=args.view, 246 | cleanup=True) 247 | print(state_machine.fqdn, "...wrote image into", args.image_directory) 248 | 249 | 250 | if __name__ == '__main__': 251 | main() 252 | --------------------------------------------------------------------------------