pax_global_header00006660000000000000000000000064144523171450014520gustar00rootroot0000000000000052 comment=f79df2c0be6e2d38993cd49f6799705471255dca hypercorn-0.14.4/000077500000000000000000000000001445231714500136175ustar00rootroot00000000000000hypercorn-0.14.4/.github/000077500000000000000000000000001445231714500151575ustar00rootroot00000000000000hypercorn-0.14.4/.github/workflows/000077500000000000000000000000001445231714500172145ustar00rootroot00000000000000hypercorn-0.14.4/.github/workflows/ci.yml000066400000000000000000000060761445231714500203430ustar00rootroot00000000000000name: CI on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: tox: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {name: '3.12-dev', python: '3.12-dev', tox: py312} - {name: '3.11', python: '3.11', tox: py311} - {name: '3.10', python: '3.10', tox: py310} - {name: '3.9', python: '3.9', tox: py39} - {name: '3.8', python: '3.8', tox: py38} - {name: '3.7', python: '3.7', tox: py37} - {name: 'format', python: '3.11', tox: format} - {name: 'mypy', python: '3.11', tox: mypy} - {name: 'pep8', python: '3.11', tox: pep8} - {name: 'package', python: '3.11', tox: package} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: update pip run: | pip install -U wheel pip install -U setuptools python -m pip install -U pip - run: pip install tox - run: tox -e ${{ matrix.tox }} h2spec: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {name: 'asyncio', worker: 'asyncio'} - {name: 'trio', worker: 'trio'} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: python-version: "3.11" - name: update pip run: | pip install -U wheel pip install -U setuptools python -m pip install -U pip - run: pip install trio . - name: Run server working-directory: compliance/h2spec run: nohup hypercorn --keyfile key.pem --certfile cert.pem -k ${{ matrix.worker }} server:app & - name: Download h2spec run: | wget https://github.com/summerwind/h2spec/releases/download/v2.6.0/h2spec_linux_amd64.tar.gz tar -xvf h2spec_linux_amd64.tar.gz - name: Run h2spec run: ./h2spec -tk -h 127.0.0.1 -p 8000 -o 10 autobahn: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {name: 'asyncio', worker: 'asyncio'} - {name: 'trio', worker: 'trio'} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: python-version: "3.10" - name: update pip run: | pip install -U wheel pip install -U setuptools python -m pip install -U pip - run: python3 -m pip install trio . - name: Run server working-directory: compliance/autobahn run: nohup hypercorn -k ${{ matrix.worker }} server:app & - name: Run Unit Tests working-directory: compliance/autobahn run: docker run --rm --network=host -v "${PWD}/:/config" -v "${PWD}/reports:/reports" --name fuzzingclient crossbario/autobahn-testsuite wstest -m fuzzingclient -s /config/fuzzingclient.json && python3 summarise.py hypercorn-0.14.4/.github/workflows/publish.yml000066400000000000000000000014511445231714500214060ustar00rootroot00000000000000name: Publish on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: python-version: 3.11 - run: | pip install poetry poetry build - uses: actions/upload-artifact@v3 with: path: ./dist pypi-publish: needs: ['build'] environment: 'publish' name: upload release to PyPI runs-on: ubuntu-latest permissions: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - uses: actions/download-artifact@v3 - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages_dir: artifact/ hypercorn-0.14.4/.gitignore000066400000000000000000000002461445231714500156110ustar00rootroot00000000000000*~ venv/ __pycache__/ Hypercorn.egg-info/ .cache/ .tox/ TODO .mypy_cache/ .pytest_cache/ .hypothesis/ docs/_build/ docs/reference/source/ dist/ .coverage poetry.lock hypercorn-0.14.4/.readthedocs.yaml000066400000000000000000000003071445231714500170460ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" python: install: - method: pip path: . extra_requirements: - docs sphinx: configuration: docs/conf.py hypercorn-0.14.4/CHANGELOG.rst000066400000000000000000000456671445231714500156620ustar00rootroot000000000000000.14.4 2023-07-08 ----------------- * Bugfix Use tomllib/tomli for .toml support replacing the unmaintained toml library. * Bugfix server hanging on startup failure. * Bugfix close websocket with 1011 on internal error (1006 is a client-only code). * Bugfix support trio > 0.22 utilising exception groups (note trio <= 0.22 is not supported). * Bugfix except ConnectionAbortedError which can be raised on Windows machines. * Bugfix ensure that closed is sent on reading end. * Bugfix handle read_timeout exception on trio. * Support and test against Python 3.11. * Add explanation of PicklingErrors. * Add config option to pass raw h11 headers. 0.14.3 2022-09-04 ----------------- * Revert Preserve response headers casing for HTTP/1 as this breaks ASGI frameworks. * Bugfix stream WSGI responses 0.14.2 2022-09-03 ----------------- * Bugfix add missing ASGI version to lifespan scope. * Bugfix preserve the HTTP/1 request header casing through to the ASGI app. * Bugifx ensure the config loglevel is respected. * Bugfix ensure new processes are spawned not forked. * Bugfix ignore dunder vars in config objects. * Bugfix clarify the subprotocol exception. 0.14.1 2022-08-29 ----------------- * Fix Python3.7 compatibility. 0.14.0 2022-08-29 ----------------- * Bugfix only recycle a HTTP/1.1 connection if client is DONE. * Bugfix uvloop may raise a RuntimeError. * Bugfix ensure 100ms sleep between Windows workers starting. * Bugfix ensure lifespan shutdowns occur. * Bugfix close idle Keep-Alive connections on graceful exit. * Bugfix don't suppress 412 bodies. * Bugfix don't idle close upgrade requests. * Allow control over date header addition. * Allow for logging configuration to be loaded from JSON or TOML files. * Preserve response headers casing for HTTP/1. * Support the early hint ASGI-extension. * Alter the process and reloading system such that it should work correctly in all configurations. * Directly support serving WSGI applications (and drop support for ASGI-2, now ASGI-3 only). 0.13.2 2021-12-23 ----------------- * Bugfix re-enable HTTP/3. 0.13.1 2021-12-16 ----------------- * Bugfix trio tcp server read completion. 0.13.0 2021-12-14 ----------------- * Bugfix eof and keep alive handling. * Bugfix Handle SSLErrors when reading. * Support websocket close reasons. * Improve the graceful shutdown, such that it works as expected. * Support a keyfile password argument. * Change the logging level to warning for lifespan not supported. * Shutdown the default executor. * Support additional headers for WS accept response. 0.12.0 2021-11-08 ----------------- * Correctly utilise SCRIPT_NAME in the wsgi middleware. * Support Python 3.10. * Support badly behaved HTTP/2 clients that omit a :authority header but provide a host header. * Use environment marker for uvloop (on windows). * Use StringIO and BytesIO for more performant websocket buffers. * Add optional read timeout. * Rename errors to add a ``Error`` suffix, most notably ``LifespanFailure`` to ``LifespanFailureError``. * Bugfix ensure keep alive timeout is cancelled on closure. * Bugfix statsd type error. * Bugfix prevent spawning whilst a task group is exit(ing). 0.11.2 2021-01-10 ----------------- * Bugfix catch the base class ConnectionError. * Bugfix catch KeyboardInterrupt if raised here e.g. on Windows. * Bugfix support non-standard HTTP status codes in the access logger. * Docs add typing for ASGI scopes and messages. 0.11.1 2020-10-07 ----------------- * Bugfix logging setup. This should work by default as expected from pre 0.11 whilst being more configurable. 0.11.0 2020-09-27 ----------------- * Bugfix race condition in H11 handling. * Bugfix HTTP/1 recycling. * Bugfix wait for tasks to complete when cancelled. * Bugfix ensure signals are always handled (asyncio). This may allow manual signal handling to be removed if you use Hypercorn via the API. * Bugfix wait on the serving when running. * Bugfix logger configuration via ``-log-config`` option. * Bugfix allow lifespan completion if app just returns. * Bugfix handle lifespan in WSGI middleware. * Bugfix handle sockets given as file descriptors properly. * Improve the logging configuration. * Allow HTTP -> HTTPS redirects to host from headers. * Introduce new access log atoms, ``R`` path with query string, ``st`` status phrase, and ``Uq`` url with query string. 0.10.2 2020-07-22 ----------------- * Bugfix add missing h2c Connection header field. * Bugfix raise an exception for unknown scopes to WSGI middleware. * Bugfix ensure HTTP/2 sending is active after upgrades. * Bugfix WSGI PATH_INFO and SCRIPT_NAME encoding. * Bugfix dispatcher middleware with non http/websocket scopes. * Bugfix dispatcher lifespan handling, 0.10.1 2020-06-10 ----------------- * Bugfix close streams on server name rejection. * Bugfix handle receiving data after stream closure. 0.10.0 2020-06-06 ----------------- * Bugfix spawn_app usage for asyncio UDP servers. * Update HTTP/3 code for aioquic >= 0.9.0, this supports draft 28. * Bugfix don't error if send to a h11 errored client. * Bugfix handle SIGINT/SIGTERM on Windows. * Improve the reloader efficiency. * Bugfix ignore BufferCompleteErrors when trying to send. * Add support for server names to ensure Hypercorn only responds to valid requests (by host header). * Add WSGI middleware. * Add the ability to send websocket pings to keep a WebSocket connection alive. * Add a graceful timeout on shutdown. 0.9.5 2020-04-19 ---------------- * Bugfix also catch RuntimeError for uvloop workers. * Bugfix correct handling of verify-flag argument and improved error message on bad values. * Bugfix correctly cope with TCP half closes via asyncio. * Bugfix handle MissingStreamError and KeyError (HTTP/2). 0.9.4 2020-03-31 ---------------- * Bugfix AssertionError when draining. * Bugfix catch the correct timeout error. 0.9.3 2020-03-23 ---------------- * Bugfix trio worker with multiple workers. * Bugfix unblock sending when the connection closes. * Bugfix Trio HTTP/1 keep alive handling. * Bugfix catch TimeoutError. * Bugfix cope with quick disconnection. * Bugfix HTTP->HTTPS redirect middleware path encoding. * Bugfix catch ConnectionRefusedError and OSError when reading. * Bugfix Ensure there is only a single timeout. * Bugfix ensure the send_task completes on timeout. * Bugfix trio has deprecated event.clear. 0.9.2 2020-02-29 ---------------- * Bugfix HTTP/1 connection recycling. This should also result in better performance under high load. * Bugfix trio syntax error, (MultiError filter usage). * Bugfix catch NotADirectoryError alongside FileNotFoundError. * Bugfix support multiple workers on Windows for Python 3.8. 0.9.1 2020-02-24 ---------------- * Bugfix catch NotImplementedError alongside AttributeError for Windows support. * Allow the access log atoms to be customised (follows the Gunicorn API expectations). * Support Python 3.8 (formally, already worked with Python 3.8). * Bugfix add scope check in DispatcherMiddleware. * Utilise the H3_ALPN constant to ensure the correct h3 draft versions are advertised. 0.9.0 2019-10-09 ---------------- * Update development status classifier to Beta. * Allow the Alt-Svc headers to be configured. * Add dispatcher middleware, allowing multiple apps to be mounted and served depending on the root path. * Support logging configuration setup. * Switch the access log format to be the same as Gunicorn's. The previous format was ``%(h)s %(S)s %(r)s %(s)s %(b)s %(D)s``. 0.8.4 2019-09-26 ---------------- * Bugfix server push pseudo headers - the bug would result in HTTP/2 connections failing if server push was attempted. 0.8.3 2019-09-26 ---------------- * Bugfix ``--error-logfile`` to work when used. * Bugfix Update keep alive after handling data (to ensure the connection isn't mistakenly considered idle). * Bugfix follow the ASGI specification by filtering and rejecting Pseudo headers sent to and received from any ASGI application. * Bugfix ensure keep alive timeout is not active when pipelining. * Bugfix clarify lifespan error messages. * Bugfix remove signal handling from worker_serve - this allows the ``serve`` functions to be used as advertised i.e. on the non-main thread. * Support HTTP/3 draft 23 and server push (HTTP/3 support is an experimental optional extra). 0.8.2 2019-08-29 ---------------- * Bugfix correctly handle HTTP/3 request with no body. * Bugfix correct the alt-svc for HTTP/3. 0.8.1 2019-08-26 ---------------- * Bugfix make unix socket ownership and mask optional, fixing a Windows bug. 0.8.0 2019-08-26 ---------------- * Support HTTP/2 prioritisation, thereby ensuring Hypercorn sends data according to the client's priorisation. * Support HTTP/3 as an optional extra (``pip install hypercorn[h3]``). * Support WebSockets over HTTP/3. * Remove worker class warnings when using serve. * Add a shutdown_trigger argument to serve functions. * Add the ability to change permissions and ownerships of unix sockets. * Bugfix ensure ASGI http response headers is an optional field. * Bugfix set the version to ``2`` rather than ``2.0`` in the scope. * Bugfix Catch ClosedResourceError as well and close. * Bugfix fix KeyError in close_stream. * Bugfix catch and ignore OSErrors when setting up a connection. * Bugfix ensure a closure code is sent with the WebSocket ASGI disconnect message. * Bugfix WinError 10022 Invalid argument to allow multiple workers on Windows. * Bugfix handle logger targets equal to None. * Bugfix don't send empty bytes (eof) to protocols. 0.7.2 2019-07-28 ---------------- * Bugfix only delete the H2 stream if present. * Bugfix change the h2 closed routine to avoid a dictionary changed size during iteration error. * Bugfix move the trio socket address parsing within the try-finally (as the socket can immediately close after/during the ssl handshake). * Bugfix handle ASGI apps ending prematurely. * Bugfix shield data sending in Trio worker. 0.7.1 2019-07-21 ---------------- * Bugfix correct the request duration units. * Bugfix ensure disconnect messages are only sent once. * Bugfix correctly handle client disconnection. * Bugfix ensure the keep alive timeout is updated. * Bugfix don't pass None to the wsproto connection. * Bugfix correctly handle server disconnections. * Bugfix specify header encoding. * Bugfix HTTP/2 stream closing issues. * Bugfix send HTTP/2 push promise frame sooner. * Bugfix HTTP/2 stream closing issues. 0.7.0 2019-07-08 ---------------- * Switch from pytoml to toml as the TOML dependency. * Bump minimum supported Trio version to 0.11. * Structually refactor the codebase. This is a large change that aims to simplify the codebase and hence make Hypercorn much more robust. It may result in lower performance (please open an issue if so), it should result in less runtime errors. * Support raw_path in the scope. * Remove support for the older NPN protocol negotiation. * Remove the `--uvloop` argument, use `-k uvloop` instead. * Rationalise the logging settings based on Gunicorn. This makes Hypercorn match the Gunicorn logging settings, at the cost of deprecating `--access-log` and `--error-log` replacing with `--access-logfile` and `--error-logfile`. * Set the default error log (target) to `-` i.e. stderr. This means that by default Hypercorn logs messages. * Log the bindings after binding. This ensures that when binding to port 0 (random port) the logged message is the port Hypercorn bound to. * Support literal IPv6 addresses (square brackets). * Allow the addtion server header to be prevented. * Add the ability to log metrics to statsd. This follows Gunicorn with the naming and which metrics are logged. * Timeout the close handshake in WebSocket connections. * Report the list of binds on trio worker startup. * Allow a subclass to decide how and where to load certificates for a SSL context. * Bugfix HTTP/2 flow control handling. 0.6.0 2019-04-06 ---------------- * Remove deprecated features, this renders this version incompatible with Quart 0.6.X releases - please use the 0.5.X Hypercorn releases. * Bugfix accept bind definitions as a single string (alongside a list of strings). * Add a LifespanTimeout Exception to better communicate the failure. * Stop supporting Python 3.6, support only 3.7 or better. * Add an SSL handshake timeout, fixing a potential DOS weakness. * Pause reading during h11 pipelining, fixing a potential DOS weakness. * Add the spec_version to the scope. * Added check for supported ssl versions. * Support ASGI 3.0, with ASGI 2.0 also supported for the time being. * Support serving on insecure binds alongside secure binds, thereby allowing responses that redirect HTTP to HTTPS. * Don't propagate access logs. 0.5.4 2019-04-06 ---------------- * Bugfix correctly support the ASGI specification; headers an subprotocol support on WebSocket acceptance. * Bugfix ensure the response headers are correctly built, ensuring they have lowercase names. * Bugfix reloading when invocated as python -m hypercorn. * Bugfix RESUSE -> REUSE typo. 0.5.3 2019-02-24 ---------------- * Bugfix reloading on both Windows and Linux. * Bugfix WebSocket unbounded memory usage. * Fixed import from deprecated trio.ssl. 0.5.2 2019-02-03 ---------------- * Bugfix ensure stream is not closed when reseting. 0.5.1 2019-01-29 ---------------- * Bugfix mark the task started after the server starts. * Bugfix ensure h11 connections are closed. * Bugfix ensure h2 streams are closed/reset. 0.5.0 2019-01-24 ---------------- * Add flag to control SSL verify mode (--verify-mode). * Allow the SSL Verify Flags to be specified in the config. * Add an official API for using Hypercorn programmatically:: async def serve(app: Type[ASGIFramework], config: Config) -> None: asyncio.run(serve(app, config)) trio.run(serve, app, config) * Add the ability to bind to multiple sockets:: hypercorn --bind '0.0.0.0:5000' --bind '[::]:5000' ... * Bugfix default port is now 8000 not 5000, * Bugfix ensure that h2c upgrade requests work. * Support requests that assume HTTP/2. * Allow the ALPN protocols to be configured. * Allow the access logger class to be customised. * Change websocket access logging to be after the handshake. * Bugfix ensure there is no race condition in lifespan startup. * Bugfix don't crash or log on SSL handshake failures. * Initial working h2 Websocket support RFC 8441. * Bugfix support reloading on Windows machines. 0.4.6 2019-01-01 ---------------- * Bugfix EOF handling for websocket connections. * Bugfix Introduce a random delay between worker starts on Windows. 0.4.5 (Not Released) -------------------- An issue with incorrect tags lead to this being pulled from PyPI. 0.4.4 2018-12-28 ---------------- * Bugfix ensure on timeout the connection is closed. * Bugfix ensure Trio h2 connections timeout when idle. * Bugfix flow window updates to connection window. * Bugfix ensure ASGI framework errors are logged. 0.4.3 2018-12-16 ---------------- * Bugfix ensure task cancellation works on Python 3.6 * Bugfix task cancellation warnings 0.4.2 2018-11-13 ---------------- * Bugfix allow SSL setting to be configured in a file 0.4.1 2018-11-12 ---------------- * Bugfix uvloop argument usage * Bugfix lifespan not supported error * Bugfix downgrade logging to warning for no lifespan support 0.4.0 2018-11-11 ---------------- * Introduce a worker-class configuration option. Note that the ``-k`` cli option is now mapped to ``-w`` to match Gunicorn. ``-k`` for the worker class and ``-w`` for the number of workers. Note also that ``--uvloop`` is deprecated and replaced with ``-k uvloop``. * Add a trio worker, ``-k trio`` to run trio or neutral ASGI applications. This worker supports HTTP/1, HTTP/2 and websockets. Note trio must be installed, ideally via the Hypercorn ``trio`` extra requires. * Handle application failures with a 500 response if no (partial) response has been sent. * Handle application failures with a 500 HTTP or 1006 websocket response depending on upgrade acceptance. * Bugfix a race condition establishing the client/server address. * Bugfix don't create an unpickleable (on windows) ssl context in the master worker, rather do so in each worker. This should support multiple workers on windows. * Support the ASGI lifespan protocol (with backwards compatibility to the provisional protocol for asyncio & uvloop workers). * Bugfix cleanup all tasks on asyncio & uvloop workers. * Adopt Black for code formatting. * Bugfix h2 don't try to send negative or zero bytes. * Bugfix h2 don't send nothing. * Bugfix restore the single worker behaviour of being a single process. * Bugfix Ensure sending doesn't error when the connection is closed. * Allow configuration of h2 max concurrent streams and max header list size. * Introduce a backlog configuration option. 0.3.2 2018-10-04 ---------------- * Bugfix cope with a None loop argument to run_single. * Add a new logo. 0.3.1 2018-09-25 ---------------- * Bugfix ensure the event-loop is configured before the app is created. * Bugfix import error on windows systems. 0.3.0 2018-09-23 ---------------- * Add ability to specify a file logging target. * Support serving on a unix domain socket or a file descriptor. * Alter keep alive timeout to require a request to be considered active (rather than just data). This mitigates a HTTP/2 DOS attack. * Improve the SSL configuration, including NPN protocols, compression suppression, and disallowed SSL versions for HTTP/2. * Allow the h2 max inbound frame size to be configured. * Add a PID file to be specified and used. * Upgrade to the latest wsproto and h11 libraries. * Bugfix propagate TERM signal to workers. * Bugfix ensure hosting information is printed when running from the command line. 0.2.4 2018-08-05 ---------------- * Bugfix don't force the ALPN protocols * Bugfix shutdown on reload * Bugfix set the default log level if std(out/err) is used * Bugfix HTTP/1.1 -> HTTP/2 Upgrade requests * Bugfix correctly handle TERM and INT signals * Bugix loop usage and creation for multiple workers 0.2.3 2018-07-08 ---------------- * Bugfix setting ssl from config files * Bugfix ensure modules aren't set as config values * Bugfix use the wsgiref datetime formatter (accurate Date headers). * Bugfix query_string value ASGI conformance 0.2.2 2018-06-27 ---------------- * Bugfix ensure that hypercorn as a command line (entry point) works. 0.2.1 2018-06-26 ---------------- * Bugfix ensure CLI defaults don't override configuration settings. 0.2.0 2018-06-24 ---------------- * Bugfix correct ASGI extension names & definitions * Bugfix don't log without a target to log to. * Bugfix allow SSL values to be loaded from command line args. * Bugfix avoid error when logging with IPv6 bind. * Don't send b'', rather no-op for performance. * Support IPv6 binding. * Add the ability to load configuration from python or TOML files. * Unblock on connection close (send becomes a no-op). * Bugfix send the close message only once. * Bugfix correct scope client and server values. * Implement root_path scope via config variable. * Stop creating event-loops, rather use the default/existing. 0.1.0 2018-06-02 ---------------- * Released initial alpha version. hypercorn-0.14.4/LICENSE000066400000000000000000000020321445231714500146210ustar00rootroot00000000000000Copyright P G Jones 2018. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hypercorn-0.14.4/README.rst000066400000000000000000000067731445231714500153230ustar00rootroot00000000000000Hypercorn ========= .. image:: https://github.com/pgjones/hypercorn/raw/main/artwork/logo.png :alt: Hypercorn logo |Build Status| |docs| |pypi| |http| |python| |license| Hypercorn is an `ASGI `_ and WSGI web server based on the sans-io hyper, `h11 `_, `h2 `_, and `wsproto `_ libraries and inspired by Gunicorn. Hypercorn supports HTTP/1, HTTP/2, WebSockets (over HTTP/1 and HTTP/2), ASGI, and WSGI specifications. Hypercorn can utilise asyncio, uvloop, or trio worker types. Hypercorn can optionally serve the current draft of the HTTP/3 specification using the `aioquic `_ library. To enable this install the ``h3`` optional extra, ``pip install hypercorn[h3]`` and then choose a quic binding e.g. ``hypercorn --quic-bind localhost:4433 ...``. Hypercorn was initially part of `Quart `_ before being separated out into a standalone server. Hypercorn forked from version 0.5.0 of Quart. Quickstart ---------- Hypercorn can be installed via `pip `_, .. code-block:: console $ pip install hypercorn and requires Python 3.7.0 or higher. With hypercorn installed ASGI frameworks (or apps) can be served via Hypercorn via the command line, .. code-block:: console $ hypercorn module:app Alternatively Hypercorn can be used programatically, .. code-block:: python import asyncio from hypercorn.config import Config from hypercorn.asyncio import serve from module import app asyncio.run(serve(app, Config())) learn more (including a Trio example of the above) in the `API usage `_ docs. Contributing ------------ Hypercorn is developed on `Github `_. If you come across an issue, or have a feature request please open an `issue `_. If you want to contribute a fix or the feature-implementation please do (typo fixes welcome), by proposing a `pull request `_. Testing ~~~~~~~ The best way to test Hypercorn is with `Tox `_, .. code-block:: console $ pipenv install tox $ tox this will check the code style and run the tests. Help ---- The Hypercorn `documentation `_ is the best place to start, after that try searching stack overflow, if you still can't find an answer please `open an issue `_. .. |Build Status| image:: https://github.com/pgjones/hypercorn/actions/workflows/ci.yml/badge.svg :target: https://github.com/pgjones/hypercorn/commits/main .. |docs| image:: https://img.shields.io/badge/docs-passing-brightgreen.svg :target: https://hypercorn.readthedocs.io .. |pypi| image:: https://img.shields.io/pypi/v/hypercorn.svg :target: https://pypi.python.org/pypi/Hypercorn/ .. |http| image:: https://img.shields.io/badge/http-1.0,1.1,2-orange.svg :target: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol .. |python| image:: https://img.shields.io/pypi/pyversions/hypercorn.svg :target: https://pypi.python.org/pypi/Hypercorn/ .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://github.com/pgjones/hypercorn/blob/main/LICENSE hypercorn-0.14.4/artwork/000077500000000000000000000000001445231714500153105ustar00rootroot00000000000000hypercorn-0.14.4/artwork/LICENSE000066400000000000000000000010441445231714500163140ustar00rootroot00000000000000CC0 1.0 Universal (CC0 1.0) The Quart logo is Copyright © 2018 Phil Jones CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. hypercorn-0.14.4/artwork/logo.png000066400000000000000000000545651445231714500167750ustar00rootroot00000000000000PNG  IHDRZoD IDATx]G}s B"'UsWmB M7J)!RVBIh4nK*IHwj8J(8U =Z!狨8j",Bmiuw3g}=_`ses3[cNItEΉ5JXN4~WnMJ7"k+Nc#⯹"tV'S :_\4x~GrEGb,+?2%80AmE#v`H~|͈ WI7+6 \>/^R/ 8>*vzx+SAK+4"},p_wEwb :+\'j;{`pHT\#ONǞ VZϟW3sSW(xi낀Ɩss]'9}xE72\0\QE~O|="Z9` "7~9;Y90P\oG:'Ğ? 1b+'P{~$/佃;c%f:/K q'?{N%7+6"=_RunvHƞWj9^I.G_ė_v55P[NNz6"7SDŞOjb+ +9:-:\_Hcu[\'_ι3|0܌r']\bcnVl=OHv{KH~9&YɱжX~vb}y`pC\'9]\ojbCVwEz8tƝL7:7"=dG*~/ˮH1{X诸"z!N]/X7r cKsb+ҽW: B* \'p=W%/NrG񥷸")>mOÂH~SrLsj{V4Ή5H?UEw'uKΈ48 pus ƶɩ.c8_ mfB=7w]HWsgD:kB>B"y2z<͈!IfP-\Ѹu8=5e _p]Ήኴ^kop9W$UZPV\}pWJ++[>JzmK߾ϗ+{l_(3v_!pE_* ħ\%v @@tHT]܎\1 ֽbaͮ~_ȧcBm!H\'}O]YWBl !*U pɷ"y:zPuˮHrN"m_ոx$Y)xE7bgBu\B +;\'y"R{Cut35XXd;#Ҡϝ 㖁܌rEɕ ˧\%ɝz't_td+s7:mu`Upb+cӡ]0xi+d9I.__Dg]n<jmst_vEzsbM.H 9VY cnVl >EcŸbhk1PW4v"CsE#|ie?` fIgj#+}nF \|d_'\)X͈!W$ޱ7lq'H\$8c2Hvmů]\7q͊H_wnV!6"V`IaA}'ָ":ɥ,o?fZpZ١m1dsbM9 Wr!fzW/Vr͊! W mvEc}79W$;B_7;`@N aw2:L +H1zf&W]~=&n=@͹Ů|o>5M//"u}__IN١m!&htEzy7t?rEP:fŦ.nV:3"uur!t_r[-]]fIg]$]ilqEr*\)tqE뫓wgD1d H'q'Aۉ5:ňP5N$o|Νh"nw>7#>^[.+n6}e^5}z~ $wD7?sC vPC}_ͯӮbQ܌r'kt³shWW+֍ǞrM^劤>Ccҧ ˮHn,3z*%IX{5u N aՍi7;-j]+b|X7>wBZݬX{5VwaXtKb@&rNqEuK*w7+6"=}M/Ԙ:KHT}؝+;]J7((j͊H_v]sbc%P$[ ~۫:FW[Pմٖ=Ĥ]v+ch-+?nvEr^[ݬ:ɍtn뿶=ĔYV}WTĝL7N:SJݡû3"uEHxsbm~!eq3b>W$W/oXWs0^Գ:YWc]!W$;\𿡪E^$ӮBVg~ʝ b+c]받ZW$|WD'N$?AAX\'9QΉ5Hݶ/!$]O!B`r'ŋ]~++BNvg]nY,л"|Yay~-y\ߋ;ޫ__%"fNr>[{!B`pEzK_ī:=c^?:sb+=sx,;b B? :7T|W sjXr~fFW$}nɘ=!BN Õ:nI._߰`>[ŭIpA.Hr!V\*9=u[\q|ŝHv3b(!CH1H?ϸMU!Bu|7#u߉e!{Bw2:L~fd^|!VxYp5绢,ꒈɅJzv++G_ϝ|f&W#Yሳb+=!T|(|ZW{.+\$Wt١m}&to+@CX.֍=֊ 7ָ":ɥ8[sbM}!VW$D_w{\YRW$"?__r/ ݫmէ|ɬCˎ/QiQ|ĢWFzڪ}AF1ik#Or#h+}]F~YHnjmFe񛵑(7s>y_j+OVmG =FYCꭽyB[x-/|jImlU_V>[nċcc?Uߢ's+V>v_iTnԯOLny={yC's#enܨȯ3Ujngoon|zhV/:y[|nVV}״QOi#j:'޾'-ު:nI.;cϯaCKUT?۪C!†?o_[Vwܫv{+;jj9w[\*aA|ɾ n}8dEzn:%I^E.!y}92?yVo,a ABE^ؑg(eD5sʟ=!B}6D?ow-ٖ?6H8/ulo(n= J~2v*ȭk3΅BmwF>=6lӨ/ψu:Mc_GZ[Ŵ]~*yX{[hѭ!6{[gBmƴb_f{|UNW9JĻɅa7+jumf/ .;B./%TE[;~=˽匣MïB< 6ꋱ2wM굵Ozync]m'fUs&r+?'WwǘwF~ \v+m?:c&竄]a{b+=+=ĻY!W4v3$vNaZŎQxċsejWN[5Y)Xk+/c)gcEW_#Ç E]5o|B =Njz&s̝y[t9\ɱsXN}>.a{|i[ĊĻNc$/;nkHSB[NH#?PxBʭ9^5v8(lE |缤ڪ`>#Zo=(n WJl6Qܗ oooƞ%/+^d=l:Js>}J,W0=u^&V%>W:C{ ~j@̝|[ 1==~e)'[- 0w˱/[erh:T-y/a'n7ƞ^+n #բPsܨ_{mJ~^/j pu箧j=em5eW{?j;4-59|V~@!rI=5N2SNmc)Ŵnz{G/# ߝۏnIYꮢ,n.|8vu)}跅snzKU_Nm{brid?g-[ڪ=/_`-=>nwbS 砒jqJJ~#ox}0Zel'}odW/Đ4eRr#1vϵ*# }ngyl7Nzosڕ*B;1[>hV]T,J̥&~n~\ Y=]wV~v9|m 1207\nL!@0di+90RS3{SiSUs]1xϝ6ry4SiX/:V=zV5y5m2_V^>yˑ.Ğs^.5ٮBv;#Қ@-.茅X ৎ;B~C VRc rN2 ]!~-fvj[Uu@{%ǁ$G({νn]@[9}(ݖ^Z5<^FV}B຅*9:-7@=^ 5Bm}6n_֯;"m>}m?%e'ȭ_TE{Դj{~Xfo+k3vu-mէxJNƞWceT}Z,hWz'[ ڪ?y/m`/XiU_@Ej.1ɍ{akUjq~y(\``Ys[ؽֹ*yn8U95U|wU~Nvw"yߠrȶ[mR`י'ڲ5j[yc=NjϿ<{@<_Ŝ_˼}n_GbN} {l1F}9^Lnx|sT5џ#/ 0ˑ?G{|N?V;SU6h+g6<^bFǵJi[Br+kGӲơ.gG>!]nU1FmoW1FgNTqȨN\x|N?^8n5l5дS!Dˎ,pҐzQowPuw2-`UBc'wlɍ<,,~TrRoO$w7 ~Zȁ|W!h1.كʫ#OBOaG!@T;x+ڨ}#Fn6갛Avb+c;u!bz("N9-W[y5-eM|zC> <E!@|NP1 y/2zץ:i7;-hNqEs\䘛C%[u/ЏBUI oִg7'eW!OBOaG!@T'@X/ =\rEV!6IN肋=t/Çk#M/"[Uߜ[t4kʪ|Q}uD& z!@XG|+rۋ|};#RIG_G=,9XjBܖyf7v%Z.BUU ڨ7f;<㕌> <E!@|NP1<:ݜhtEz>:犤CK?‰5/Oqvʇr#sq,'!'B# *Vƙ.斗Q{BjJ!ܬx w\qE͈&5w:;:17=|y}=XA >3*ZS&V> <E!@|NP1e <E!@|NP.XSFmVu㮓Kfȁ!mo/Wzq nU! }gBB}yi#'CZ=I ,B#>vBp9}|^F90v{|!6"ZJ,ݜy7w ĭf[5n~=s׼cnI ,B#>vB=Q3SNu5X~_ѽw/B97NVǸ\q+F0~'mF1j+m2#OBOaG!@T+;:gݖw[\1d }hwK=c^4ݖ?rV}>g&Ǿ3.kY1̘W`-8f=K#OT4{~.Pc엶_g?wCm꧔U//kQu0n_qH6\}BU}c>߱o!}k'k:fPc[;zꓝ :;;z+'wL e =htEz>X/V}׆#v[ĭDV[,{Ll6Bc!@O}x"X"X!@o%&&Du Q{Db+5XPI!ۘ_6{[#[1C oo΍u1ZD'!'BՁ .Bŋ`0Vo%xsb+ҽ\WtƝLXf;r#Ж?V;F.0e~fokh+\5eMeg9z!@!E0z+ѲلD-YFm|{+֍Nz6]\~h#Cڪ{Bt[r>#[C4|X"ESVp B/B@[f[}O/`yEoH\L_wnV=4Ss#O ؽ{ɍB@Yu6k}`9a2Ƣ|y:r'!@"q,^y[~_u_rfbj4xIvNr y$PC{ǹ_o/b;!AB^v1՘/c.ES`pEx BJ4Ί/ =u[\1d |hbf/̍yV߅Y !垷 r#f[}Os1~B$X .Bŋ`0VBU!VÇnwgD: s"衉L6lϹ|_ս.J"7}hѭ>ѱǭ89 OBE08"X!@o%&Ѫ+N7'ehtEz>:$B!Fb)mV2Y)!(Ff~̹!@O}D,BA!E0z+'U!_!/iݬx yW4v[!Zvec>ϹC\~VJ G "! `" ȭ/X9^I. 8fŦ !6Qc?U6tEgEm^ϖVcMB$X .Bŋ`0V"o˟ ypQ;bݸg,Εskh9}rYI!mg|&mES`pEx BJ4zk_KȻCO+W$YW۸}F<`:"B!mcV#!b9z b B/B@[o L˪[CO$]'>H]!6'O֡н/!(^|DXfL OBE08"X!@o%"og?T_=ݖBO4"9hEIv!`ܨ "! `" uP`+F/ʨI-dvwgD_Wp%gƦ^O~Vb O?~kn hWp[k'!@"q,^6'^F^FY)f&W λ}YnImo>Zz|Y!rͶs;o3ϫ^'!@"q,^ZmMeIaJ[ulƐXt[wbSq !6Qc?7덣>ok2{I[97cV~:пc9/rz:gyrm{<>OT4{~.D[VdMnuEg?!ܨ_5L'hZ[ǿ|rl;xzkٛ#!|Z'sيM׉١sv\Wߢ[/"Xr,>xkzoRڪcۏnIN?+u'B4|Wo&{SO̻/\,=YF}1dN x`0Fk7r`d(Bl_ =u!_!&&:T9ȟ ~Vr0vtlcuֆ|pB$XEx BުiMoݠzgRÕpBNuל? ~k?#Xuy$(~Vr {WLܪ=櫈9!OBU!@\!` 譖`y;ܨ|&7P]m+^ǝh::Џ}0{ant ۇ|tBAHڪ?񙃺2[mIB=[ >p&@cLBrICf;۩O6U'ȍUчoPřy ѴٖܪG}&8XyxrGd<}>當ɼ~(~VN!ȭ*ȯ)nNz/yNذ`'@O}` wV*NŋU:J}NZFnNm%qu\=rya^*. !4M>=6=wU;cbz` G !@os'|&;Di#/i'֔0ǕhїV=w7̥Ww>j~VC sw"ONF<)%,> VB ? @:V 5m_P)mձcKi4 miǞ/Wq+`zH_F^BS!?Bޟe XzFaڦ~U)#u!{냷&}'!@!.ݱ_ !@O}z`u JLqm>涴}u>1չUbM6 ҥ ZB!ȍ<\g9v߽ OBU!BOB2UnV=+9St[<3*.!LLnߋw/z` G !@ot`y;ܨ|@u!}f/̍tf%^W~VS ڪ?hAىCS!?BJ@:V״ٖܪG}`e*Gdupum?jj  'b+B$X,$(!@[U2V:+NWuD&z]'&^:"BaZ^OWFYz` G !@o%"7oh̭| QKڨ=n'oMB<69ܨ_ "B!Y;\B$X,$(!@[ !涹Vuz~k* SڪcoW3Q^O!m+6~Vc0V=:^ OBU!BOB2Bl] zks~Mϓ^@!r#}'c\=I #XIPBՁBѺO2ڪ}Uծέ:UQ_s~E/Sf[rN_M+1{{k2{I[yoƾ3vyQoB#F}* m35 5V_xJد*{ss`|9s@P yu[o緦n ZwE୉6߆CyvB!M#ww{1NXZw!6|<6(OzJ>Xs֯UB_?RF[6*B譞G*M:mot!ޚ޺asZvtk!4M߫=v_ sX~F﵆E08&c^XM#]7|w^[U<zܪG?NolbZU슰/˭<^xF9^!^ұZ[7G?jYK~d)=T[BED﵆UᴄƏRV*柝>Ǫx\@[P/oЧRmՁoUŮQ[ucNLnuqZ>KS>t g5B-`隰Hυꦥ?W_`h5S }S#-v-U< [=yoOQE7/QXU튘8"~~iFu[k*۴ B?=漤j!+}>=׭Ks^lOV͍5m3?nvEFET) !߷ΊN;B:^9ҴN>|,O><>!{ NN_7u(mPVT+n⒌yՖWc'@o?ҚdH%7V=`^U+bbrsN]F>|bmǾz`'{Gȱ`'vɭ\`E+1sl{gܾcnPͶ.xsOP|[7xQ8=zm?xq냷&sC!O!Fy5=N?bbjt?݃/BOE{^<}jRL-9k'joǣAVUUg<7 []yWBm[rХ*l[B>Fkz#XZvtkq C 6/zg]_ە8&EJ*BUm<El?%}."V=zoNϿE]]!5'嶒~~V|jCn0~d/Ǟku_CVc^!t[i#V9{9!@)|Uh>Rڛ[[isvEʉ5]yy--~VN-A-{ME­KU=7v |دJ^F>ҺO{ί6J[EL$IDATyiGoBo譮|OoNoޗ2 hWDƎmV+}-cvKnu꣱_ޫؑ~>mE2FYc$XZnV~%)T5|w/ksª:^sv{ x`BׯMRojNoMe |Wf;~Y!@n~hUV{zi-^˴Q{*V;򢦕[u!몼ק4u!-#,sH=B*C ~zbmnԯi#rCU+b9F ii+Vwq1~Vu`۷6/Ӫ 6?g/=fVo:>R X+Sn˭:Bkqv*oL|hͶkG]sWڪ/h>|="(kdUÇV]uBjWD/nm&gB[jMe3yGs&r*Vܪ? ڪb/Xݖ ž Aywߋ^#Vk>A0Qd!\ڨF=CU+ZÇF?mA8Xm %i#C\wXJNolKnգ5XXBznʞ[+4tO@NecP6BK!VZYW+inI.oS;V>>=\zMp57'/oX4W+T~FP5mƴ{bN]BA x-/XT)7ӱ砢]V/dyXyrlv[k] 㭩lS&Xkgb/o wEhΗ=~mVz@UƎmV=BhFo{m9zb#X "X\^==v-#^M87jg-M#P%mz&C#X!}u!'>4␽R?l9)_!t;{6R{oMBBˎny6Ch+Hʋp4l\7'bۥKڨ=‰5ArN/B Rn!j{U Xan?{ۥ.붼KX6dlSnko?ձ^yx{Bć&^[ߟm=T`jlKoMeNkOx2\nABܕ[ua4=X*h+cqe]`vSu>wFo7r\[x~!}EoP'F7V,6P{+"X Xܪ7ԩͶm[rAn]mc/{C?.PmC}/l`W`i9<[?,/6Co2!;:Q[ulZTAOyMLn=T`iMoݐ5}u![[7`+#8pr[kt;{6R>yK*:}:FJ5ܭ};L#6;ۙ[D~IG8&d-;5Ta񛵕O~k7jXf[rN^v˺- }ˮT)75j: Gn>3j-VCs#^v/y5m :VFKL~o\6`q㏁B!t[ʭ};N77V=.t_F902zAҴ{M8v19Ss+gb/|r`+ kzm{^rfbjtsA[} !3r`dHO[y%:մٖsдꭹQOՠuAu7153uD&vf;IGŦCߴٖܪGc۵<<|hx}9J[uOߟ!FqDۨ:}u![[7uI-d@)nF~NdCLXkQ{/|Aj٦㡧vV>._FN =@]VF>/IsRnˍ:{ۥ.붼+[S٦Oנ.ccG6 ϡ㏍=>4>7pEo[S٦`Zm仴w^y.َix<=~ͱ{˦rWnՅ uvi丶v6jȁsĐ5C=: @_&F7V^vV ڪbڽ1VS{UAn;=~D4r`dH~Tf[BAӪV=]~ݖB|Smc J[yi7m%v-#^z5д*:cUǢ/z.tkz놐RmՁvWPsWpkQ{/|6l !lgn_-{ԀꞫJoNm^Wu[%XT)75wt~5Ҳ[- _'>|hx}n %M!VFKL~Qo +^u9)~\ݖr.^wYoo9z<^҈yQ>5m#X'F7V^/H:JCߚ޺A[@?dojQYxG ii+^wSMm ٿB4zknUִr#O,`4\n`s}?M#wi-K3B_o}.tOj՝K,h 9cUb//tkz놐T| dk}x[^@ʙAmԞSmi#6ڿvV>1@i>"n]ӜrN^w˺-V k*۔Ő Q/kAiCCi5Xw)y5m ٿBh#Un zk}N[ߖI-t[ʭïlgFkW.@_/i'քʙ ``+ kzm~crҺOޢ߯ѱ!7r`dH1SMm ٿB4zkn] X a#6zv=V D^l;tMmɭztMo\_F902rcG6j^w_p !olXƸr<AdZm_/cub6j6R0ȳM#ǃ/hO,1gB_P@]m+xr6l.aڐM^0r*orQbÇׇskzܨ,<|VVu6T#! uJ1缼{_0RL0 tbf#颻4v9dH! !2FXhSJP4qL꼼L^^޹&瞛rϯOX*- 9^Vrx>vm|o q;T?{nx3Kp#x-wnϿ:ߧ'kzT_>,> J.bꦧS'[ԕ&F>/>Ղp׆VMߚ࿶46vVN%L9調ɾKsT46+a8\~ p:&Ga5ȥpW}xtud_¥9 C_w5; p8_F;,ЗN `KiWr8rxy_SC]spݷO/x^ pi"TwZۃa%I?xs#cM>: پ+!@XgFQ*Z5oūN9l;ҡ/FhOA~op/#&GSo5z wy_Kr<1>5Usɓ} 1״|5pu˫ڀ2ڙfᅾw[WsUI9|}o]Opv^bOXŏ9Wg,ه{/?kW̓jыTەN% 8k̓Bt7RGF;j+gl0=mzt~i_~N9ƗjJ++k}ݓn69r|P,K'͔}NKuv9>ؔ.ϴx~yyUSF;S'R 7jPW E*zc{ӗɑ۟"lh8_[kM%z_ ?|pK/>h-]] 5īJ8XAF;RS7[C*a`+%֟[9\H^|JXiD@r<1>5պ0M%:oM˫nS[i6~zc{ s+9[W9]8u_`_$}Kj-̝~/VFhG\I%Q^ePťKZJ*˭C{Z_&GZϧ_kګ7S'ƧƻZSߜu*\^^=t>\g]F^Y8ֺ0Yم3 {ZK%|yPXo/_X>@suX2G1̍ҡlڷxpc9o[;}^ྕ)̭4 Z-:b^KeC#ε!l/7ݭJKC w_rSF;S h7<x)f?ԗjO0-0-has{'k>TЭ-on%lJeRk<nsj=3}t, {rƀ@Cw\sJ|3{};&_j_¥o/.=zoS]-|++ϭю.?ĿV 8 C.,>zV5ڑJT͟ U,K*f4*dDEl& IENDB`hypercorn-0.14.4/artwork/logo.svg000066400000000000000000000450561445231714500170030ustar00rootroot00000000000000
hypercorn-0.14.4/artwork/logo_small.png000066400000000000000000000166721445231714500201620ustar00rootroot00000000000000PNG  IHDRZ`DIDATx]}]Uu!yʗh"dCMSywwR)iTMPmK+ZZeL?TfHAJJfx{h*`04!hH}{ksͬ2yo6c؛0vޫ`K0``K`)6~xL=69!ZrB+6?1uzWSO!Z>|лyu7'3:ԛ86@q $mwu> MkgT]0]yn+n]{Wƶho|]^nc4=v!$y 6@Xu2%pOh7ddvȿ-+ 16N=~agA0!L"*>( EX.5HShcyc "(Dkʢ>X^_"#fl([R$rfj?{ة/mIGtXNB&:ͼhRx0"D|t ic9GkgFoH[I(*KoH~B"Mg`m }Dv)J`%𭚩Dֵ)1m 6j[ %~oh}JݦzO)vPzA-SX!iȤmv WsCjӐTskrɱ <ܐsPXi*ݚNډFjE+mv":m  *vMS! v5wYv2xzv3 ^IR0+]MܨO଴7>\q_Ǩ -Uܺƥ{SzyVinH&՟Sbaq eɵ#(S=Yrǔ 05 v557,|e*@OJin y t֘͜L4r3憴*¿vq&ѺDB^l M)maװ憔kdJ\{i-t9 `z3,ĉyma󅼅kn7ŝe&}.v IFB޼ynxrfٸOygnbD_K4mlfIlڟ.N;Ɉ6gHRkc7 w}_Y<& 7kMYF$fYiNOz#'V<پ?ic:y'`I_cGs{_Ǿq>/m >+ fٸ>M;2!сy{ ۓhN@8!;Z(ǭ(vGAtlOR Q}ԉf>>FV@, f=pG=b)6vO3b͍fxJn+)knhs>tu̢fMnk57&4wȇinsdF57է{h5*4sw&%3u(<܌C)L ӃYU\s3vb'+ mX82W[eC?vGg=SXENAL;=}"|?ҳF:2)<]w"s_~hUsjB3}$Azgr /kne9%>`!` AxU3m "䊷q7a kJ8;ǂكvVAZ}SfmPMk|DkoJ!JPS5 -^,*"jFݾZsCJg*8kol&XPoi 47ð7 Fe7POcpz9L}ӕlynHm!XD4vk!5WsY.O`cAA%nVLMlnL?YfVuŽ}=C_T]s +#cVDOgy`g ɥ> ﶲcm|pqθر[7s1y 57)-P5wi4Z=]7bGc ]k?cMC ^*~#c1ZTMf9f?GtC۶@@ 41X51kl:魘Bt'pGprhqhORv/ZH4d,bYK62r8fk^\*qI2J|>h3ɜ!Xoj$P|_!c@{M x1Ts/9rH?)?ڍ?R3 4'ѴQu|r5bYAn=;Pk(~1Ƙ};>jtJknG],PiO%i: %68oMCRKO c@Sin3f^sK?r6ce9sNwM8XЪNg1ݴDM !͗ؒ%ƍnNsCQ;1ץ7kI05qvR_jchZ.t57ʦ^wn#90pPj*O*eA `C_0|R #%cI%Ǩpb&hnHK߁$H=x㻚Z2}S47F!cJF=+oBKjR;gՁ{kpQicI^V}yWӗJacV2 o8;i6m+z-_B+Vm=:.ldl((cWs6?0i-?NDS!ic\٤u>#G8 K-vfs!icc#ZhaVmLjj]M_jxhnF0N t18.~'1(V$4?!*0dp[&11׬}HXROZ?p[Ȗ}<:L}[HU]sW6[@RcXsxMN^`c)[N57hêWy&} Pӌ1Xkn6Ѥ/IUI?cLUw0k@Ve- *kntNHS;פok^|ORqc-|eǀPVeUܐucf$>5t}u) 72ړ.OUY57nk(~u[zLgSI:ҪꚛN%vԿXɱ7w=׼׭A kUVuͭ Ɠ3﷒nX;,5We܊G;61rqjT _YwR lUܲinR;w5.7Kݻ㉝q%@hWKJw.L]v[mumeߖT|w&?.iӋLzXss>Ns4 77^MKj %6,ܓo$M**kn_&85icIm6@(&J_[ژer,6R JX[ ll?.?ZPTlttR SژNst?hyh!q$0V[:ixH*q -qmܽfy`,-۶@]&6fh;͍t&ڴͭP+(vzmigI*|k0NsCoYӝ)ֆ>7,-P_ȉK6{TۊsP: n v/%TygVeeMIc͉:;c珘ίr[$ ?(Wjq+u/dV\sKt% ~~%uo7_u-qI{g!0x:%Xu;#ocS_Pj~'y`c-v;vX;͍19/MRҘT|?J|5 ~I݄9h}/$ecopY-TkolhnR%OpjKIŏVeB+pNs3Ҡ``sRmigE(hs@:+f[@Qf-v,hgknjA[JbinUb[B\GfKinזfDzsmUҕv5H5;fùt}NCZ|ʚ*L5kNs&Z#l[O["fo-1$!ܜiNss֓47inNs3tESVbM*qEC 47cL*~cD7sO4n./ٜV&Mj~8M-4hnL11[ʬz0޾@Y٫)~ԋv_OzhTjw-TewY] Ms5j`&5f!{kAs5*Vfq]gd&o0{TM*qk@o 6yG$( N@1 'leFM Vhh|wxdyYRs/yE@6K~>y^Wpkn%אO`4#m+I'Ll滩tWMsܲ&&ps[47MRܤj?P|=5jn 7FH7ӓ &5̴?VbdY/X@6@[C~XW/ʟV 9'IRoߍ*V 9[G. l܈}3OsbrTj%Rinʤ%,4Dl0Z`y斡5T|qw#1-]kT?cazL-/Z@􆠹%mPbq p)"֧5R ard44-8"'3&F. Ϣ,p[qD4ϋRBg KsSOfH'ɒV6et_U2m ٪$Y MbUjSP& ?0|| "m 9h29A ʴ)(DM#6eVjb*͇ʐ470mt gNsAkal5=L^r24wGEChqT[Ԅ0B2bu!{ 51 &1:T(-{1uWQ8u,@DI1tHnSI-yWӧC o&N:5~uBR/Q(1ɶ!sȈUlZ~FWH->aCz%G=361r!Hr_RZI=4DUBjTR> RCIENDB`hypercorn-0.14.4/artwork/logo_small.svg000066400000000000000000000134351445231714500201670ustar00rootroot00000000000000
hypercorn-0.14.4/compliance/000077500000000000000000000000001445231714500157315ustar00rootroot00000000000000hypercorn-0.14.4/compliance/autobahn/000077500000000000000000000000001445231714500175325ustar00rootroot00000000000000hypercorn-0.14.4/compliance/autobahn/fuzzingclient.json000066400000000000000000000004151445231714500233200ustar00rootroot00000000000000{ "options": {"failByDrop": false}, "outdir": "./reports/servers", "servers": [{"agent": "websockets", "url": "ws://localhost:8000/", "options": {"version": 18}}], "cases": ["*"], "exclude-cases": ["12.*", "13.*"], "exclude-agent-cases": {} } hypercorn-0.14.4/compliance/autobahn/server.py000066400000000000000000000013141445231714500214110ustar00rootroot00000000000000async def app(scope, receive, send): while True: event = await receive() if event['type'] == 'websocket.disconnect': break elif event['type'] == 'websocket.connect': await send({'type': 'websocket.accept'}) elif event['type'] == 'websocket.receive': await send({ 'type': 'websocket.send', 'bytes': event['bytes'], 'text': event['text'], }) elif event['type'] == 'lifespan.startup': await send({'type': 'lifespan.startup.complete'}) elif event['type'] == 'lifespan.shutdown': await send({'type': 'lifespan.shutdown.complete'}) break hypercorn-0.14.4/compliance/autobahn/summarise.py000066400000000000000000000003721445231714500221130ustar00rootroot00000000000000import json import sys with open('reports/servers/index.json') as file_: report = json.load(file_) failures = sum(value['behavior'] == 'FAILED' for value in report['websockets'].values()) if failures > 0: sys.exit(1) else: sys.exit(0) hypercorn-0.14.4/compliance/h2spec/000077500000000000000000000000001445231714500171155ustar00rootroot00000000000000hypercorn-0.14.4/compliance/h2spec/cert.pem000066400000000000000000000031631445231714500205600ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEljCCAn4CCQCri/HA0BA3TDANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV SzAeFw0xNzEyMjgxNjQyMTNaFw0xODEyMjgxNjQyMTNaMA0xCzAJBgNVBAYTAlVL MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuwrDLc6o0Ctfc0NYzIUq xE+D+4Gc5u+Is3/wIjGYdzMdEdu1x/cnFAbLYp9AGC5Api1w4ntHGcZENRI+aJ6D 8Ev4FlqdVtlbcyNO9MqQw4GUTsidVhoZzU5N9IEDs5ZPbO6blSHoxSxatllqgJs/ 8ws+E6ED7F6sp3bwrsEEYSvbddJ5L/7T8I88IqpZ0dlNeNl7q1tD2x4ea63RvyL8 MKnnhnBa7G/UnjidDqf/mzVF2fOOqr6PFrQa2TOdKiZEXwnlZSYc/nZD7cC2KVZE +6gv7QbZA1vD8vXzWGjAhQODr3l7xuAug1kA0CZgnQXIS9UaedS5dNm5Go79d97Q +fE4u2ZdE1RTpbGTzSsPKMZFvFQrdi0UKFCp0+QxV8wyZ32arSjyQCaSBt6rKCpm zAfK5GstiKWM21gRwfsuo8oKSNE3VI7zfcSlxwRD9Ns3SwvIKo2Xr0K1K0aIOXgD P85cjAq3OJsWizZ1BhfN3f5pq0TQAojjj4q71y1t4mkPYShEygBY3wX6av2KtuSz Q4/ARC6pmnclL5tYFICOFuPCPz5m7/coIhhF21tgeDSLhNJ7PgVtp+n+1XWfCTLD GpW7nURNIy2fQqA2cBpYIbPlFx+mgJwlqsg55XFqVoUHpY6/HftuI4Zm/LRx8/fe mYNyIalJ3P+4iMFhd2DGp10CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAbHVdOrc1 XZ8T6kvvbbSYtf6CQsIbTBmIcJ3Ply7LVMs8B5uBSe1pzM2hZGKbrpbs9ZmPRJiH vl2m3UeDW+N12xkjgY/S6BCj95JotxQVTt5OlbZLuQ4a3BEFgX6oZO7dN7JA0XLx bfUMCvGFR+Nkx6bTnCzcxPtbBiOL4ZymRaoM8NFd5jTybRlkCOFOg4HewmmZ/kif cN0qFZekcDsHcqBUz3HVGNc1MGMRGvRcFyMOCq9t5FF2Zoa0AoExFdjotHqc9tLm VdGey5me38T0PaPIqO870b43ZDdL6QFO5LvEo8ca78fVGnUksLxJ46n8C5ivdztY +kdH3Lbp8OmA3ZodGq5okSxVFn3eTa19epOuKz5NietCfpsfmXiIvbIrgZO0u114 5QiJZ5tJtbOPexi8Jzy3vDNK1wAO7RXQ5JrNqogz9emjBL1PAHqNMAfEJnb5/oro eqj45w/qQmoDqblK8Q8U+lSFpVq82e76m5YItvMe1VmLmPGUoq4JFSpjzmzMlW+w o3g+4RW9LsvolFMdQbpi1XV1LsPQuaJxczVOR74DJHbGeuE61tCxjx9lpaWAJJVg PbPHGc1WBSnMb/nCloYUN7UU0wqzOUx3yqvveP8w2Fb8pv3wEqHY/cGu1+pHSn5M x3Xnu9MXCkttgGltMoWgUuiz55iLqAzkOQY= -----END CERTIFICATE----- hypercorn-0.14.4/compliance/h2spec/key.pem000066400000000000000000000063101445231714500204100ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC7CsMtzqjQK19z Q1jMhSrET4P7gZzm74izf/AiMZh3Mx0R27XH9ycUBstin0AYLkCmLXDie0cZxkQ1 Ej5onoPwS/gWWp1W2VtzI070ypDDgZROyJ1WGhnNTk30gQOzlk9s7puVIejFLFq2 WWqAmz/zCz4ToQPsXqyndvCuwQRhK9t10nkv/tPwjzwiqlnR2U142XurW0PbHh5r rdG/IvwwqeeGcFrsb9SeOJ0Op/+bNUXZ846qvo8WtBrZM50qJkRfCeVlJhz+dkPt wLYpVkT7qC/tBtkDW8Py9fNYaMCFA4OveXvG4C6DWQDQJmCdBchL1Rp51Ll02bka jv133tD58Ti7Zl0TVFOlsZPNKw8oxkW8VCt2LRQoUKnT5DFXzDJnfZqtKPJAJpIG 3qsoKmbMB8rkay2IpYzbWBHB+y6jygpI0TdUjvN9xKXHBEP02zdLC8gqjZevQrUr Rog5eAM/zlyMCrc4mxaLNnUGF83d/mmrRNACiOOPirvXLW3iaQ9hKETKAFjfBfpq /Yq25LNDj8BELqmadyUvm1gUgI4W48I/Pmbv9ygiGEXbW2B4NIuE0ns+BW2n6f7V dZ8JMsMalbudRE0jLZ9CoDZwGlghs+UXH6aAnCWqyDnlcWpWhQeljr8d+24jhmb8 tHHz996Zg3IhqUnc/7iIwWF3YManXQIDAQABAoICABYKl6OPRe96HP5tQkqfqsGF iU0bIg1IzvgwLHErHQd2+4b+ODa/VliS0Gbn01rGIJI0qqfV1TQhXCpQ4w/bFjs8 CJlBxmbUqGUyFPzd3h9b5sk99OSPoNjD0IXuqiwAm41/tM/nNhH+PxZcBSPwp6GR gpg3kknJglkduBEv5783tt30lplkUz928aQ4JOuIywthvaQc1is9KmKQEjaO/d8S NpluJhjUuN6IV2HBxGpa5cdgX0CZwizvvnY4Ed5Esivs855u1l3aO/kJi63lX620 TSmGdA5kQvwfpbSWa5GBL4R/MWnnQzPxSho9W4dFhiwBieQvgEdX3OtXTGFS3ZdT Fa5ZESvxgUBkHwfLBS0Cy4RIfGdZUr2ZrMsH/pVcqnZpBJ9qYgYT6p/14VZh4O8l ZTnckMMR/WTKM0BOFsNDKYSpF7XneKBLe47mWbFdaxEjPxU833j8YFSs+267oGh9 AOOPebe22+qLyv6wVHx8ckse1RxL3CMcdxr7QgXJilGcQGyrl+yGrhiIlxgUfPYW KSnDlK+dz4+ihZGx/X6zrbT8BcOvq2TPgp9a+OqMRsOhDNgqXnxgOnR5TnFf+S8s v3l0zXx9V2zZQNREudf+f+a0Qri996CgLu/x/LRwQj/YFRzzSXvdqsSWxQkboEX9 lZ6gzqpst2EVR6xdPCi1AoIBAQDceFLSBd/EbUfBxIEoAGjtE2J9pm9NJO69mSZA hUJUiHOmVnINnllFcizY31SVgno+g1lmeuVRF8lwXMs4QIweYQyHZv1DXq8JV2YJ ysr+qe0GYcOi412vBwS6GtR26qDVOvyXlUDd7j31wYiQHxqL1M4qcZ7nzlbe4ulS 8n5uKUSlCFEo5+4DPVecl6Y0aEwPeC3veqpJmrzL29PpdYaTUbH67G3p+MvqRLbt KEmsq/FgoGnNuvIT5eP5jA2d2fAMWntHrrlNV3W3FINlPjpoVKxDrBVFoxgqP/24 6/eC7R3IzesCtsXycPTDBQg0YEEN0bxptxXuZafnNkDmn1Q3AoIBAQDZL1b0BVv6 3HWPCZtMtoJTwnEKkmyMREBbIcJUoKtR8pOhVYUGqYHY2OtokQv97TPG0G9H1Z72 b1e6SwpGMV7oVCawXFLyZXJ6V2pcvJhwT37Cu4riMeuNmo78mH79engkvbNeq9sl fFFfaUGwBIXUApBcn5Yluc0FRdZ45RjhvHkKzy/ADWUS9dnHQXh0dbVVi75yajMn K6oQpaWKKXqyNkGQKFgr6QOtm+Fi1z2SzT+WTTb1JiOhkFnw7DXf8FI3e6TpQy5q LYqGwr18lKAukDY3HtC/Rx6Bv7GG0RgByFIQGFhyyPZAX+V8GEx//fogL0FVzkUm JaNoWPfuL78LAoIBAQC3uy6KCIsqz0d1m6VnCLBoojb6L7GhwJ2VNARE0MkuWWjH vlLeNpB+51+ofLWow0vMvPnMBa8FnaUqFqrk/iXHS2l9jb6SXl3Qkx1eG7p/8Gyv XNoE7SYtrtOppKJbV70g9j96s8+TI/BO1jJQqRseXQJTLM0YsUSECuYXUi867vld 70hzppUb7gsNXPQNyL1aRvVBFiDDpkigO1qmvGKicvq3+kC/M6/8U7d+fIypccF+ nTCPWrRTEMqkNKtEWVNLeDw0yM90PObE5Dt8LBfQyn+lBcvUdM62pw1zBnMGkUS5 C6JGaLseCDRyMcdcnrqYIam7D/Ee82ixruz3ROCRAoIBAFTCpfPcN5aC/ZpSTHq2 68wWoZlXpedkJ52pYjc28UWtHzKitqTv+I4RsmX/3ac/MKrR4+wsEbrpn1pEOQFF +V1AokzH61NQhkn63bbNn8yNKdKD8OLwSpbcEBvCxCTW7BaitmMnPQK3LubGpG02 hqhES+TqH2YfykTZiadq+bf3n2G5lFAmqiCpNFIQWhtRaPC29h+fFNGft+KBU0bF g24TwKirJiYU7WuO33p8uDoXwk49Wkp4lQVT2dYtyaTZHK0soyKqJm4n0d1gGSWK t60UeSQv8ZYFAoHutzD/X5gqfuRrK/G4PmrHQj+ZGBoHm9t9tcjwFIqbu9dYiYI6 vhsCggEAc2SFaHYadaasUm48iJe463x1IwUb7tibiX9DN1cBm6lbwOhCcSqyFwKj 5BXeBNPwFRNCAI+nxs1gMGUHMtcd234Fdg6GySWSnMsavgP4sn9gfZMC2OctNeHg F1JIPpf8i/X3wC6+9It8RlKHvStQXMnlJxsgJq4yJ6IfenLpzWl4f/YoX+01sLYh lAOfYRnMMScRIfOErOC28l4qKcPsGwFyJmB7TiVgxz83B12hnjA9bH8mk7OVpYJR p9LY/FGdPXo4JvKE4G1AEHaRjdgPqicFYDfXKIPQ1dy4hwQMcYlApDvOax/gaKrY /h39p3qBT+YHuuM7DwN83zVLIgyBzw== -----END PRIVATE KEY----- hypercorn-0.14.4/compliance/h2spec/server.py000066400000000000000000000014501445231714500207750ustar00rootroot00000000000000async def app(scope, receive, send): while True: event = await receive() if event['type'] == 'http.disconnect': break elif event['type'] == 'http.request' and not event.get('more_body', False): await send_data(send) break elif event['type'] == 'lifespan.startup': await send({'type': 'lifespan.startup.complete'}) elif event['type'] == 'lifespan.shutdown': await send({'type': 'lifespan.shutdown.complete'}) break async def send_data(send): await send({ 'type': 'http.response.start', 'status': 200, 'headers': [(b'content-length', b'5')], }) await send({ 'type': 'http.response.body', 'body': b'Hello', 'more_body': False, }) hypercorn-0.14.4/docs/000077500000000000000000000000001445231714500145475ustar00rootroot00000000000000hypercorn-0.14.4/docs/Makefile000066400000000000000000000011421445231714500162050ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = Hypercorn SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) hypercorn-0.14.4/docs/_static/000077500000000000000000000000001445231714500161755ustar00rootroot00000000000000hypercorn-0.14.4/docs/_static/logo.png000066400000000000000000000545651445231714500176620ustar00rootroot00000000000000PNG  IHDRZoD IDATx]G}s B"'UsWmB M7J)!RVBIh4nK*IHwj8J(8U =Z!狨8j",Bmiuw3g}=_`ses3[cNItEΉ5JXN4~WnMJ7"k+Nc#⯹"tV'S :_\4x~GrEGb,+?2%80AmE#v`H~|͈ WI7+6 \>/^R/ 8>*vzx+SAK+4"},p_wEwb :+\'j;{`pHT\#ONǞ VZϟW3sSW(xi낀Ɩss]'9}xE72\0\QE~O|="Z9` "7~9;Y90P\oG:'Ğ? 1b+'P{~$/佃;c%f:/K q'?{N%7+6"=_RunvHƞWj9^I.G_ė_v55P[NNz6"7SDŞOjb+ +9:-:\_Hcu[\'_ι3|0܌r']\bcnVl=OHv{KH~9&YɱжX~vb}y`pC\'9]\ojbCVwEz8tƝL7:7"=dG*~/ˮH1{X诸"z!N]/X7r cKsb+ҽW: B* \'p=W%/NrG񥷸")>mOÂH~SrLsj{V4Ή5H?UEw'uKΈ48 pus ƶɩ.c8_ mfB=7w]HWsgD:kB>B"y2z<͈!IfP-\Ѹu8=5e _p]Ήኴ^kop9W$UZPV\}pWJ++[>JzmK߾ϗ+{l_(3v_!pE_* ħ\%v @@tHT]܎\1 ֽbaͮ~_ȧcBm!H\'}O]YWBl !*U pɷ"y:zPuˮHrN"m_ոx$Y)xE7bgBu\B +;\'y"R{Cut35XXd;#Ҡϝ 㖁܌rEɕ ˧\%ɝz't_td+s7:mu`Upb+cӡ]0xi+d9I.__Dg]n<jmst_vEzsbM.H 9VY cnVl >EcŸbhk1PW4v"CsE#|ie?` fIgj#+}nF \|d_'\)X͈!W$ޱ7lq'H\$8c2Hvmů]\7q͊H_wnV!6"V`IaA}'ָ":ɥ,o?fZpZ١m1dsbM9 Wr!fzW/Vr͊! W mvEc}79W$;B_7;`@N aw2:L +H1zf&W]~=&n=@͹Ů|o>5M//"u}__IN١m!&htEzy7t?rEP:fŦ.nV:3"uur!t_r[-]]fIg]$]ilqEr*\)tqE뫓wgD1d H'q'Aۉ5:ňP5N$o|Νh"nw>7#>^[.+n6}e^5}z~ $wD7?sC vPC}_ͯӮbQ܌r'kt³shWW+֍ǞrM^劤>Ccҧ ˮHn,3z*%IX{5u N aՍi7;-j]+b|X7>wBZݬX{5VwaXtKb@&rNqEuK*w7+6"=}M/Ԙ:KHT}؝+;]J7((j͊H_v]sbc%P$[ ~۫:FW[Pմٖ=Ĥ]v+ch-+?nvEr^[ݬ:ɍtn뿶=ĔYV}WTĝL7N:SJݡû3"uEHxsbm~!eq3b>W$W/oXWs0^Գ:YWc]!W$;\𿡪E^$ӮBVg~ʝ b+c]받ZW$|WD'N$?AAX\'9QΉ5Hݶ/!$]O!B`r'ŋ]~++BNvg]nY,л"|Yay~-y\ߋ;ޫ__%"fNr>[{!B`pEzK_ī:=c^?:sb+=sx,;b B? :7T|W sjXr~fFW$}nɘ=!BN Õ:nI._߰`>[ŭIpA.Hr!V\*9=u[\q|ŝHv3b(!CH1H?ϸMU!Bu|7#u߉e!{Bw2:L~fd^|!VxYp5绢,ꒈɅJzv++G_ϝ|f&W#Yሳb+=!T|(|ZW{.+\$Wt١m}&to+@CX.֍=֊ 7ָ":ɥ8[sbM}!VW$D_w{\YRW$"?__r/ ݫmէ|ɬCˎ/QiQ|ĢWFzڪ}AF1ik#Or#h+}]F~YHnjmFe񛵑(7s>y_j+OVmG =FYCꭽyB[x-/|jImlU_V>[nċcc?Uߢ's+V>v_iTnԯOLny={yC's#enܨȯ3Ujngoon|zhV/:y[|nVV}״QOi#j:'޾'-ު:nI.;cϯaCKUT?۪C!†?o_[Vwܫv{+;jj9w[\*aA|ɾ n}8dEzn:%I^E.!y}92?yVo,a ABE^ؑg(eD5sʟ=!B}6D?ow-ٖ?6H8/ulo(n= J~2v*ȭk3΅BmwF>=6lӨ/ψu:Mc_GZ[Ŵ]~*yX{[hѭ!6{[gBmƴb_f{|UNW9JĻɅa7+jumf/ .;B./%TE[;~=˽匣MïB< 6ꋱ2wM굵Ozync]m'fUs&r+?'WwǘwF~ \v+m?:c&竄]a{b+=+=ĻY!W4v3$vNaZŎQxċsejWN[5Y)Xk+/c)gcEW_#Ç E]5o|B =Njz&s̝y[t9\ɱsXN}>.a{|i[ĊĻNc$/;nkHSB[NH#?PxBʭ9^5v8(lE |缤ڪ`>#Zo=(n WJl6Qܗ oooƞ%/+^d=l:Js>}J,W0=u^&V%>W:C{ ~j@̝|[ 1==~e)'[- 0w˱/[erh:T-y/a'n7ƞ^+n #բPsܨ_{mJ~^/j pu箧j=em5eW{?j;4-59|V~@!rI=5N2SNmc)Ŵnz{G/# ߝۏnIYꮢ,n.|8vu)}跅snzKU_Nm{brid?g-[ڪ=/_`-=>nwbS 砒jqJJ~#ox}0Zel'}odW/Đ4eRr#1vϵ*# }ngyl7Nzosڕ*B;1[>hV]T,J̥&~n~\ Y=]wV~v9|m 1207\nL!@0di+90RS3{SiSUs]1xϝ6ry4SiX/:V=zV5y5m2_V^>yˑ.Ğs^.5ٮBv;#Қ@-.茅X ৎ;B~C VRc rN2 ]!~-fvj[Uu@{%ǁ$G({νn]@[9}(ݖ^Z5<^FV}B຅*9:-7@=^ 5Bm}6n_֯;"m>}m?%e'ȭ_TE{Դj{~Xfo+k3vu-mէxJNƞWceT}Z,hWz'[ ڪ?y/m`/XiU_@Ej.1ɍ{akUjq~y(\``Ys[ؽֹ*yn8U95U|wU~Nvw"yߠrȶ[mR`י'ڲ5j[yc=NjϿ<{@<_Ŝ_˼}n_GbN} {l1F}9^Lnx|sT5џ#/ 0ˑ?G{|N?V;SU6h+g6<^bFǵJi[Br+kGӲơ.gG>!]nU1FmoW1FgNTqȨN\x|N?^8n5l5дS!Dˎ,pҐzQowPuw2-`UBc'wlɍ<,,~TrRoO$w7 ~Zȁ|W!h1.كʫ#OBOaG!@T;x+ڨ}#Fn6갛Avb+c;u!bz("N9-W[y5-eM|zC> <E!@|NP1 y/2zץ:i7;-hNqEs\䘛C%[u/ЏBUI oִg7'eW!OBOaG!@T'@X/ =\rEV!6IN肋=t/Çk#M/"[Uߜ[t4kʪ|Q}uD& z!@XG|+rۋ|};#RIG_G=,9XjBܖyf7v%Z.BUU ڨ7f;<㕌> <E!@|NP1<:ݜhtEz>:犤CK?‰5/Oqvʇr#sq,'!'B# *Vƙ.斗Q{BjJ!ܬx w\qE͈&5w:;:17=|y}=XA >3*ZS&V> <E!@|NP1e <E!@|NP.XSFmVu㮓Kfȁ!mo/Wzq nU! }gBB}yi#'CZ=I ,B#>vBp9}|^F90v{|!6"ZJ,ݜy7w ĭf[5n~=s׼cnI ,B#>vB=Q3SNu5X~_ѽw/B97NVǸ\q+F0~'mF1j+m2#OBOaG!@T+;:gݖw[\1d }hwK=c^4ݖ?rV}>g&Ǿ3.kY1̘W`-8f=K#OT4{~.Pc엶_g?wCm꧔U//kQu0n_qH6\}BU}c>߱o!}k'k:fPc[;zꓝ :;;z+'wL e =htEz>X/V}׆#v[ĭDV[,{Ll6Bc!@O}x"X"X!@o%&&Du Q{Db+5XPI!ۘ_6{[#[1C oo΍u1ZD'!'BՁ .Bŋ`0Vo%xsb+ҽ\WtƝLXf;r#Ж?V;F.0e~fokh+\5eMeg9z!@!E0z+ѲلD-YFm|{+֍Nz6]\~h#Cڪ{Bt[r>#[C4|X"ESVp B/B@[f[}O/`yEoH\L_wnV=4Ss#O ؽ{ɍB@Yu6k}`9a2Ƣ|y:r'!@"q,^y[~_u_rfbj4xIvNr y$PC{ǹ_o/b;!AB^v1՘/c.ES`pEx BJ4Ί/ =u[\1d |hbf/̍yV߅Y !垷 r#f[}Os1~B$X .Bŋ`0VBU!VÇnwgD: s"衉L6lϹ|_ս.J"7}hѭ>ѱǭ89 OBE08"X!@o%&Ѫ+N7'ehtEz>:$B!Fb)mV2Y)!(Ff~̹!@O}D,BA!E0z+'U!_!/iݬx yW4v[!Zvec>ϹC\~VJ G "! `" ȭ/X9^I. 8fŦ !6Qc?U6tEgEm^ϖVcMB$X .Bŋ`0V"o˟ ypQ;bݸg,Εskh9}rYI!mg|&mES`pEx BJ4zk_KȻCO+W$YW۸}F<`:"B!mcV#!b9z b B/B@[o L˪[CO$]'>H]!6'O֡н/!(^|DXfL OBE08"X!@o%"og?T_=ݖBO4"9hEIv!`ܨ "! `" uP`+F/ʨI-dvwgD_Wp%gƦ^O~Vb O?~kn hWp[k'!@"q,^6'^F^FY)f&W λ}YnImo>Zz|Y!rͶs;o3ϫ^'!@"q,^ZmMeIaJ[ulƐXt[wbSq !6Qc?7덣>ok2{I[97cV~:пc9/rz:gyrm{<>OT4{~.D[VdMnuEg?!ܨ_5L'hZ[ǿ|rl;xzkٛ#!|Z'sيM׉١sv\Wߢ[/"Xr,>xkzoRڪcۏnIN?+u'B4|Wo&{SO̻/\,=YF}1dN x`0Fk7r`d(Bl_ =u!_!&&:T9ȟ ~Vr0vtlcuֆ|pB$XEx BުiMoݠzgRÕpBNuל? ~k?#Xuy$(~Vr {WLܪ=櫈9!OBU!@\!` 譖`y;ܨ|&7P]m+^ǝh::Џ}0{ant ۇ|tBAHڪ?񙃺2[mIB=[ >p&@cLBrICf;۩O6U'ȍUчoPřy ѴٖܪG}&8XyxrGd<}>當ɼ~(~VN!ȭ*ȯ)nNz/yNذ`'@O}` wV*NŋU:J}NZFnNm%qu\=rya^*. !4M>=6=wU;cbz` G !@os'|&;Di#/i'֔0ǕhїV=w7̥Ww>j~VC sw"ONF<)%,> VB ? @:V 5m_P)mձcKi4 miǞ/Wq+`zH_F^BS!?Bޟe XzFaڦ~U)#u!{냷&}'!@!.ݱ_ !@O}z`u JLqm>涴}u>1չUbM6 ҥ ZB!ȍ<\g9v߽ OBU!BOB2UnV=+9St[<3*.!LLnߋw/z` G !@ot`y;ܨ|@u!}f/̍tf%^W~VS ڪ?hAىCS!?BJ@:V״ٖܪG}`e*Gdupum?jj  'b+B$X,$(!@[U2V:+NWuD&z]'&^:"BaZ^OWFYz` G !@o%"7oh̭| QKڨ=n'oMB<69ܨ_ "B!Y;\B$X,$(!@[ !涹Vuz~k* SڪcoW3Q^O!m+6~Vc0V=:^ OBU!BOB2Bl] zks~Mϓ^@!r#}'c\=I #XIPBՁBѺO2ڪ}Uծέ:UQ_s~E/Sf[rN_M+1{{k2{I[yoƾ3vyQoB#F}* m35 5V_xJد*{ss`|9s@P yu[o緦n ZwE୉6߆CyvB!M#ww{1NXZw!6|<6(OzJ>Xs֯UB_?RF[6*B譞G*M:mot!ޚ޺asZvtk!4M߫=v_ sX~F﵆E08&c^XM#]7|w^[U<zܪG?NolbZU슰/˭<^xF9^!^ұZ[7G?jYK~d)=T[BED﵆UᴄƏRV*柝>Ǫx\@[P/oЧRmՁoUŮQ[ucNLnuqZ>KS>t g5B-`隰Hυꦥ?W_`h5S }S#-v-U< [=yoOQE7/QXU튘8"~~iFu[k*۴ B?=漤j!+}>=׭Ks^lOV͍5m3?nvEFET) !߷ΊN;B:^9ҴN>|,O><>!{ NN_7u(mPVT+n⒌yՖWc'@o?ҚdH%7V=`^U+bbrsN]F>|bmǾz`'{Gȱ`'vɭ\`E+1sl{gܾcnPͶ.xsOP|[7xQ8=zm?xq냷&sC!O!Fy5=N?bbjt?݃/BOE{^<}jRL-9k'joǣAVUUg<7 []yWBm[rХ*l[B>Fkz#XZvtkq C 6/zg]_ە8&EJ*BUm<El?%}."V=zoNϿE]]!5'嶒~~V|jCn0~d/Ǟku_CVc^!t[i#V9{9!@)|Uh>Rڛ[[isvEʉ5]yy--~VN-A-{ME­KU=7v |دJ^F>ҺO{ί6J[EL$IDATyiGoBo譮|OoNoޗ2 hWDƎmV+}-cvKnu꣱_ޫؑ~>mE2FYc$XZnV~%)T5|w/ksª:^sv{ x`BׯMRojNoMe |Wf;~Y!@n~hUV{zi-^˴Q{*V;򢦕[u!몼ק4u!-#,sH=B*C ~zbmnԯi#rCU+b9F ii+Vwq1~Vu`۷6/Ӫ 6?g/=fVo:>R X+Sn˭:Bkqv*oL|hͶkG]sWڪ/h>|="(kdUÇV]uBjWD/nm&gB[jMe3yGs&r*Vܪ? ڪb/Xݖ ž Aywߋ^#Vk>A0Qd!\ڨF=CU+ZÇF?mA8Xm %i#C\wXJNolKnգ5XXBznʞ[+4tO@NecP6BK!VZYW+inI.oS;V>>=\zMp57'/oX4W+T~FP5mƴ{bN]BA x-/XT)7ӱ砢]V/dyXyrlv[k] 㭩lS&Xkgb/o wEhΗ=~mVz@UƎmV=BhFo{m9zb#X "X\^==v-#^M87jg-M#P%mz&C#X!}u!'>4␽R?l9)_!t;{6R{oMBBˎny6Ch+Hʋp4l\7'bۥKڨ=‰5ArN/B Rn!j{U Xan?{ۥ.붼KX6dlSnko?ձ^yx{Bć&^[ߟm=T`jlKoMeNkOx2\nABܕ[ua4=X*h+cqe]`vSu>wFo7r\[x~!}EoP'F7V,6P{+"X Xܪ7ԩͶm[rAn]mc/{C?.PmC}/l`W`i9<[?,/6Co2!;:Q[ulZTAOyMLn=T`iMoݐ5}u![[7`+#8pr[kt;{6R>yK*:}:FJ5ܭ};L#6;ۙ[D~IG8&d-;5Ta񛵕O~k7jXf[rN^v˺- }ˮT)75j: Gn>3j-VCs#^v/y5m :VFKL~o\6`q㏁B!t[ʭ};N77V=.t_F902zAҴ{M8v19Ss+gb/|r`+ kzm{^rfbjtsA[} !3r`dHO[y%:մٖsдꭹQOՠuAu7153uD&vf;IGŦCߴٖܪGc۵<<|hx}9J[uOߟ!FqDۨ:}u![[7uI-d@)nF~NdCLXkQ{/|Aj٦㡧vV>._FN =@]VF>/IsRnˍ:{ۥ.붼+[S٦Oנ.ccG6 ϡ㏍=>4>7pEo[S٦`Zm仴w^y.َix<=~ͱ{˦rWnՅ uvi丶v6jȁsĐ5C=: @_&F7V^vV ڪbڽ1VS{UAn;=~D4r`dH~Tf[BAӪV=]~ݖB|Smc J[yi7m%v-#^z5д*:cUǢ/z.tkz놐RmՁvWPsWpkQ{/|6l !lgn_-{ԀꞫJoNm^Wu[%XT)75wt~5Ҳ[- _'>|hx}n %M!VFKL~Qo +^u9)~\ݖr.^wYoo9z<^҈yQ>5m#X'F7V^/H:JCߚ޺A[@?dojQYxG ii+^wSMm ٿB4zknUִr#O,`4\n`s}?M#wi-K3B_o}.tOj՝K,h 9cUb//tkz놐T| dk}x[^@ʙAmԞSmi#6ڿvV>1@i>"n]ӜrN^w˺-V k*۔Ő Q/kAiCCi5Xw)y5m ٿBh#Un zk}N[ߖI-t[ʭïlgFkW.@_/i'քʙ ``+ kzm~crҺOޢ߯ѱ!7r`dH1SMm ٿB4zkn] X a#6zv=V D^l;tMmɭztMo\_F902rcG6j^w_p !olXƸr<AdZm_/cub6j6R0ȳM#ǃ/hO,1gB_P@]m+xr6l.aڐM^0r*orQbÇׇskzܨ,<|VVu6T#! uJ1缼{_0RL0 tbf#颻4v9dH! !2FXhSJP4qL꼼L^^޹&瞛rϯOX*- 9^Vrx>vm|o q;T?{nx3Kp#x-wnϿ:ߧ'kzT_>,> J.bꦧS'[ԕ&F>/>Ղp׆VMߚ࿶46vVN%L9調ɾKsT46+a8\~ p:&Ga5ȥpW}xtud_¥9 C_w5; p8_F;,ЗN `KiWr8rxy_SC]spݷO/x^ pi"TwZۃa%I?xs#cM>: پ+!@XgFQ*Z5oūN9l;ҡ/FhOA~op/#&GSo5z wy_Kr<1>5Usɓ} 1״|5pu˫ڀ2ڙfᅾw[WsUI9|}o]Opv^bOXŏ9Wg,ه{/?kW̓jыTەN% 8k̓Bt7RGF;j+gl0=mzt~i_~N9ƗjJ++k}ݓn69r|P,K'͔}NKuv9>ؔ.ϴx~yyUSF;S'R 7jPW E*zc{ӗɑ۟"lh8_[kM%z_ ?|pK/>h-]] 5īJ8XAF;RS7[C*a`+%֟[9\H^|JXiD@r<1>5պ0M%:oM˫nS[i6~zc{ s+9[W9]8u_`_$}Kj-̝~/VFhG\I%Q^ePťKZJ*˭C{Z_&GZϧ_kګ7S'ƧƻZSߜu*\^^=t>\g]F^Y8ֺ0Yم3 {ZK%|yPXo/_X>@suX2G1̍ҡlڷxpc9o[;}^ྕ)̭4 Z-:b^KeC#ε!l/7ݭJKC w_rSF;S h7<x)f?ԗjO0-0-has{'k>TЭ-on%lJeRk<nsj=3}t, {rƀ@Cw\sJ|3{};&_j_¥o/.=zoS]-|++ϭю.?ĿV 8 C.,>zV5ڑJT͟ U,K*f4*dDEl& IENDB`hypercorn-0.14.4/docs/_static/logo_small.png000066400000000000000000000166721445231714500210470ustar00rootroot00000000000000PNG  IHDRZ`DIDATx]}]Uu!yʗh"dCMSywwR)iTMPmK+ZZeL?TfHAJJfx{h*`04!hH}{ksͬ2yo6c؛0vޫ`K0``K`)6~xL=69!ZrB+6?1uzWSO!Z>|лyu7'3:ԛ86@q $mwu> MkgT]0]yn+n]{Wƶho|]^nc4=v!$y 6@Xu2%pOh7ddvȿ-+ 16N=~agA0!L"*>( EX.5HShcyc "(Dkʢ>X^_"#fl([R$rfj?{ة/mIGtXNB&:ͼhRx0"D|t ic9GkgFoH[I(*KoH~B"Mg`m }Dv)J`%𭚩Dֵ)1m 6j[ %~oh}JݦzO)vPzA-SX!iȤmv WsCjӐTskrɱ <ܐsPXi*ݚNډFjE+mv":m  *vMS! v5wYv2xzv3 ^IR0+]MܨO଴7>\q_Ǩ -Uܺƥ{SzyVinH&՟Sbaq eɵ#(S=Yrǔ 05 v557,|e*@OJin y t֘͜L4r3憴*¿vq&ѺDB^l M)maװ憔kdJ\{i-t9 `z3,ĉyma󅼅kn7ŝe&}.v IFB޼ynxrfٸOygnbD_K4mlfIlڟ.N;Ɉ6gHRkc7 w}_Y<& 7kMYF$fYiNOz#'V<پ?ic:y'`I_cGs{_Ǿq>/m >+ fٸ>M;2!сy{ ۓhN@8!;Z(ǭ(vGAtlOR Q}ԉf>>FV@, f=pG=b)6vO3b͍fxJn+)knhs>tu̢fMnk57&4wȇinsdF57է{h5*4sw&%3u(<܌C)L ӃYU\s3vb'+ mX82W[eC?vGg=SXENAL;=}"|?ҳF:2)<]w"s_~hUsjB3}$Azgr /kne9%>`!` AxU3m "䊷q7a kJ8;ǂكvVAZ}SfmPMk|DkoJ!JPS5 -^,*"jFݾZsCJg*8kol&XPoi 47ð7 Fe7POcpz9L}ӕlynHm!XD4vk!5WsY.O`cAA%nVLMlnL?YfVuŽ}=C_T]s +#cVDOgy`g ɥ> ﶲcm|pqθر[7s1y 57)-P5wi4Z=]7bGc ]k?cMC ^*~#c1ZTMf9f?GtC۶@@ 41X51kl:魘Bt'pGprhqhORv/ZH4d,bYK62r8fk^\*qI2J|>h3ɜ!Xoj$P|_!c@{M x1Ts/9rH?)?ڍ?R3 4'ѴQu|r5bYAn=;Pk(~1Ƙ};>jtJknG],PiO%i: %68oMCRKO c@Sin3f^sK?r6ce9sNwM8XЪNg1ݴDM !͗ؒ%ƍnNsCQ;1ץ7kI05qvR_jchZ.t57ʦ^wn#90pPj*O*eA `C_0|R #%cI%Ǩpb&hnHK߁$H=x㻚Z2}S47F!cJF=+oBKjR;gՁ{kpQicI^V}yWӗJacV2 o8;i6m+z-_B+Vm=:.ldl((cWs6?0i-?NDS!ic\٤u>#G8 K-vfs!icc#ZhaVmLjj]M_jxhnF0N t18.~'1(V$4?!*0dp[&11׬}HXROZ?p[Ȗ}<:L}[HU]sW6[@RcXsxMN^`c)[N57hêWy&} Pӌ1Xkn6Ѥ/IUI?cLUw0k@Ve- *kntNHS;פok^|ORqc-|eǀPVeUܐucf$>5t}u) 72ړ.OUY57nk(~u[zLgSI:ҪꚛN%vԿXɱ7w=׼׭A kUVuͭ Ɠ3﷒nX;,5We܊G;61rqjT _YwR lUܲinR;w5.7Kݻ㉝q%@hWKJw.L]v[mumeߖT|w&?.iӋLzXss>Ns4 77^MKj %6,ܓo$M**kn_&85icIm6@(&J_[ژer,6R JX[ ll?.?ZPTlttR SژNst?hyh!q$0V[:ixH*q -qmܽfy`,-۶@]&6fh;͍t&ڴͭP+(vzmigI*|k0NsCoYӝ)ֆ>7,-P_ȉK6{TۊsP: n v/%TygVeeMIc͉:;c珘ίr[$ ?(Wjq+u/dV\sKt% ~~%uo7_u-qI{g!0x:%Xu;#ocS_Pj~'y`c-v;vX;͍19/MRҘT|?J|5 ~I݄9h}/$ecopY-TkolhnR%OpjKIŏVeB+pNs3Ҡ``sRmigE(hs@:+f[@Qf-v,hgknjA[JbinUb[B\GfKinזfDzsmUҕv5H5;fùt}NCZ|ʚ*L5kNs&Z#l[O["fo-1$!ܜiNss֓47inNs3tESVbM*qEC 47cL*~cD7sO4n./ٜV&Mj~8M-4hnL11[ʬz0޾@Y٫)~ԋv_OzhTjw-TewY] Ms5j`&5f!{kAs5*Vfq]gd&o0{TM*qk@o 6yG$( N@1 'leFM Vhh|wxdyYRs/yE@6K~>y^Wpkn%אO`4#m+I'Ll滩tWMsܲ&&ps[47MRܤj?P|=5jn 7FH7ӓ &5̴?VbdY/X@6@[C~XW/ʟV 9'IRoߍ*V 9[G. l܈}3OsbrTj%Rinʤ%,4Dl0Z`y斡5T|qw#1-]kT?cazL-/Z@􆠹%mPbq p)"֧5R ard44-8"'3&F. Ϣ,p[qD4ϋRBg KsSOfH'ɒV6et_U2m ٪$Y MbUjSP& ?0|| "m 9h29A ʴ)(DM#6eVjb*͇ʐ470mt gNsAkal5=L^r24wGEChqT[Ԅ0B2bu!{ 51 &1:T(-{1uWQ8u,@DI1tHnSI-yWӧC o&N:5~uBR/Q(1ɶ!sȈUlZ~FWH->aCz%G=361r!Hr_RZI=4DUBjTR> RCIENDB`hypercorn-0.14.4/docs/conf.py000066400000000000000000000124151445231714500160510ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Hypercorn documentation build configuration file, created by # sphinx-quickstart on Sun May 21 14:18:44 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('../')) from importlib.metadata import version as meta_version # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Hypercorn' copyright = '2018 - 2020 Philip Jones' author = 'Philip Jones' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = meta_version("hypercorn") # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "pydata_sphinx_theme" html_logo = "_static/logo_small.png" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { "external_links": [ {"name": "Source code", "url": "https://github.com/pgjones/hypercorn"}, {"name": "Issues", "url": "https://github.com/pgjones/hypercorn/issues"}, ], "icon_links": [ { "name": "Github", "url": "https://github.com/pgjones/hypercorn", "icon": "fab fa-github", }, ], } # html_sidebars = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'Hypercorndoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Hypercorn.tex', 'Hypercorn Documentation', 'Philip Jones', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'hypercorn', 'Hypercorn Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Hypercorn', 'Hypercorn Documentation', author, 'Hypercorn', 'One line description of project.', 'Miscellaneous'), ] def setup(app): app.add_css_file('css/hypercorn.css') suppress_warnings = ["ref.python"] hypercorn-0.14.4/docs/discussion/000077500000000000000000000000001445231714500167325ustar00rootroot00000000000000hypercorn-0.14.4/docs/discussion/backpressure.rst000066400000000000000000000013071445231714500221560ustar00rootroot00000000000000.. _backpressure: Managing backpressure ===================== The connection between Hypercorn and a client can be paused by either party to allow that party time to process the information it has received, i.e. to catch up. When the connection is paused the sender effectively receives pressure to stop sending data. This is commonly termed back pressure. Hypercorn will respond to client backpressure by pausing the sending of data. This back pressure will propogate back to any ASGI framework via a blocked (without blocking the event loop) ASGI send awaitable. In other words any ``await send(message)`` calls will block the coroutine till the client backpressure has abated or the connection is closed. hypercorn-0.14.4/docs/discussion/closing.rst000066400000000000000000000040221445231714500211200ustar00rootroot00000000000000.. _closing: Connection closure ================== Connection closure is a difficult part of the connection lifecycle with choices needing to be made by Hypercorn about how to respond and what to send to the ASGI application. Before a connection is fully closed, it is often 'half-closed' by one side sending an EOF (empty bytestring b""). If sent by the client Hypercorn will not expect any further messages, but will allow messages to be sent to the client. This follows the HTTPWG guidance in `rfc.section.9.6.p.12 `_. Client disconnection -------------------- If the client disconnects unexpectedly, i.e. whilst the server is still expecting to read or send data, the read/send socket action will raise an exception. This exception is caught and a Closed event is sent to the protocol. The protocol should then send each stream a StreamClosed event and delete the stream. Server disconnection -------------------- In the normal course of actions a stream should send a EndBody or EndData followed by a StreamClosed event to indicate that the stream has finished and the connection can be closed. However if the application errors the stream may only be able to send a StreamClosed event. Therefore the protocol only sends a StreamClosed event back to the stream on receipt of the StreamClosed from the stream. The protocol only sends a Closed event to the server if the connection must be closed, e.g. HTTP/1 without keep alive or an error. ASGI messages ------------- I've chosen to allow ASGI applications to continue to send messages to the server after the connection has closed and after the server has sent a disconnect message. Specifically Hypercorn will not error and instead no-op on receipt. This ensures that there isn't a race condition after the server has sent the disconnect message. Hypercorn guarantees to send the disconnect message, and send it only once, to each application instance. This message will be sent on closure of the connection (either on client or server closure). hypercorn-0.14.4/docs/discussion/design_choices.rst000066400000000000000000000004771445231714500224420ustar00rootroot00000000000000.. _design_choices: Design Choices ============== Callbacks or streaming ---------------------- The asyncio callback ``create_server`` approach is faster than the streaming ``start_server`` approach, and hence is used. This is based on benchmarking and the `uvloop `_ research. hypercorn-0.14.4/docs/discussion/dos_mitigations.rst000066400000000000000000000151301445231714500226600ustar00rootroot00000000000000.. _dos_mitigations: Denial Of Service mitigations ============================= There are multiple ways a client can overload a server and deny service to other clients. A simple example can simply be to call an expensive route at a rate high enough that the server's resources are exhausted. Whilst this could happen innocently (if a route was popular) it takes a lot of client resource, there are malicious methods to exhaust server resources. There are two attack methods mitigated and discussed here, the first aims to open as many connections to the server as possible without freeing them, thereby eventually exhausting all the connections and preventing other clients from connecting. As most request response cycles last milliseconds before the connection is closed the key is to somehow hold the connection open. The second aims to exhaust the server's memory and hence either slow the server to a crawl or kill the server application, thereby preventing the server from replying to other clients. The key here is to somehow make the server write a lot of information to memory. Inactive connection ------------------- This attack is of the first type and aims to exhaust the server's connections. It works by opening connections to the server and then doing nothing with the connection. A poorly configured server would simply wait for the client to do something therefore holding the connection open. To mitigate this Hypercorn has a keep alive timeout that will keep an inactive connection alive for this time. It can be configured via the configuration ``keep_alive_timeout`` setting. The default value for the keep alive timeout is 5 seconds which is the max recommended default in the Gunicorn settings. .. note:: Connections are not considered inactive whilst the request is being processed. So this will only timeout connections inactive before a request, or after a response and before the next request. This does not affect websocket connections. Large request body ------------------ This attack is of the second type and aims to exhaust the server's memory by inviting it to receive a large request body (and hence write the body to memory). A poorly configured server would have no limit on the request body size and potentially allow a single request to exhaust the server. It is up to the framework to guard against this attack. This is to allow the framework to consume the request body if desired. Slow request body ----------------- This attack is of the first type and aims to exhaust the server's connections by inviting it to wait a long time for the request's body. A poorly configured server would wait indefinitely for the request body. It is up to the framework to guard against this attack. This is to allow the framework to consume the request body if desired. No response consumption ----------------------- This attack is of the second type and aims to exhaust the server's memory by failing to consume the data sent to the client. This failure results in backpressure on the server that leads to the response being written to memory rather than the connection. A poorly configured server would ignore the backpressure and exhaust its memory. (Note this requires a route that respondes with a lot of data, e.g. video streaming). To mitigate this Hypercorn responds to the backpressure and pauses (blocks) the coroutine writing the response. Slow response consumption ------------------------- This attack is of the first type and aims to exhaust the server's connections by inviting the server to take a long time sending the response, for example by applying backpressure indefinetly. A poorly configured server would simply wait indefinetly trying to send the response. It is up to the framework to guard against this attack. This is to allow for responses that purposely take a long time, e.g. server sent events. Large websocket message ----------------------- This attack is of the second type and aims to exhaust the server's memory by inviting it to receive very large websocket messages. A poorly configured server would have no limit on the message size and potentially allow a single message to exhaust the server. To mitigate this Hypercorn limits the message size to the value set in the application config ``websocket_max_message_size``. Any message larger than this limit will trigger the websocket to be closed abruptly. The default value for ``websocket_max_message_size`` is 16 MB, which is chosen as it is the limit discussed in the Flask documentation. Slow SSL handshake ------------------ This attack is of the first type and aims to exhaust the server's connections by failing to complete (or potentially even start) the SSL handshake. A poorly configured server would simply wait indefinetly for the SSL handshake to complete. To mitigate this Hypercorn has a ssl handshake timeout that will close connections that take longer to complete the ssl handshake. It can be configured via the configuration ``ssl_handshake_timeout`` setting. The default value for ``ssl_handshake_timeout`` is 60 seconds, which is chosen as it is the limit used in NGINX. HTTP/2 attacks -------------- There are a number of specific attacks against the HTTP/2 protocol that are designed to exhuast the server's resources. These are only feasible against a server serving over HTTP/2, and hence apply to Hypercorn by default. Flood attacks ^^^^^^^^^^^^^ This attack aims to exhaust the server's resources by requesting ack frames whilst not consuming them. It is conceptually similar to the "No response consumption" attack discussed above however this attack slowly consumes frames that are unrelated to the HTTP response such as ping, setting, and reset frames. To mitigate this Hypercorn responds to the backpressure and pauses (blocks) the reading data from the client. Depending on the state of the connection a timeout should then close the connection. Data Dribble ^^^^^^^^^^^^ This attack aims to exhaust the server's resources by requesting a response whilst setting the window size to a very small value e.g. 1 byte. The server could then exhaust memory buffering the response. It is conceptually similar to the "No response consumption" attack discussed above. It is up to the framework to guard against this attack. This is to allow for responses that purposely take a long time, e.g. server sent events. Internal Data Buffering ^^^^^^^^^^^^^^^^^^^^^^^ This attack is conceptually similar to the "Flood attack" and "No response consumption" attack in that it requires the server to buffer data that it cannot send to the client. To mitigate this Hypercorn responds to the backpressure and pauses (blocks) the coroutine writing the response. hypercorn-0.14.4/docs/discussion/http2.rst000066400000000000000000000026301445231714500205260ustar00rootroot00000000000000.. _http2: HTTP/2 ====== Hypercorn is based on the excellent `hyper-h2 `_ library. TLS settings ------------ The recommendations in this documentation for the SSL/TLS ciphers and version are from `RFC 7540 `_. As required in the RFC ``ECDHE+AESGCM`` is the minimal cipher set HTTP/2 and TLSv2 the minimal TLS version servers should support. By default Hypercorn will use this as the cipher set. ALPN Protocol ~~~~~~~~~~~~~ The ALPN Protocols should be set to include ``h2`` and ``http/1.1`` as Hypercorn supports both. It is feasible to omit one to only serve the other. If these aren't set most clients will assume Hypercorn is a HTTP/1.1 only server. By default Hypercorn will set h2 and http/1.1 as the ALPN protocols. No-TLS ~~~~~~ Most clients, including all the web browsers only support HTTP/2 over TLS. Hypercorn, however, supports the h2c HTTP/1.1 to HTTP/2 upgrade process. This allows a client to send a HTTP/1.1 request with a ``Upgrade: h2c`` header that results in the connection being upgraded to HTTP/2. To test this try .. code-block:: shell $ curl --http2 http://url:port/path Note that in the absence of either the upgrade header or an ALPN protocol Hypercorn will assume and treat the connection as HTTP/1.1. HTTP/2 features --------------- Hypercorn supports pipeling, flow control, server push, and prioritisation. hypercorn-0.14.4/docs/discussion/index.rst000066400000000000000000000002571445231714500205770ustar00rootroot00000000000000=========== Discussions =========== .. toctree:: :maxdepth: 1 backpressure.rst closing.rst design_choices.rst dos_mitigations.rst http2.rst workers.rst hypercorn-0.14.4/docs/discussion/workers.rst000066400000000000000000000012451445231714500211620ustar00rootroot00000000000000.. _workers: Workers ======= Hypercorn supports asyncio, uvloop, or trio worker classes thereby allowing ASGI applications writen with these in mind to be used. Asyncio ------- Asyncio is the default event loop implementation that is part of the standard library. It is relatively well supported by third party libraries. Uvloop ------ Uvloop is a different event loop policy for asyncio. It is used as it is quicker than the asyncio default, however it does not work on Windows. Trio ---- Trio is a third party event loop implementation that is not compatible with asyncio. It is less supported, however the API is much nicer to use and it is harder to make mistakes. hypercorn-0.14.4/docs/how_to_guides/000077500000000000000000000000001445231714500174065ustar00rootroot00000000000000hypercorn-0.14.4/docs/how_to_guides/api_usage.rst000066400000000000000000000064231445231714500221020ustar00rootroot00000000000000.. _api_usage: API Usage ========= Most usage of Hypercorn is expected to be via the command line, as explained in the :ref:`usage` documentation. Alternatively it is possible to use Hypercorn programmatically via the ``serve`` function available for either the asyncio or trio :ref:`workers` (note the asyncio ``serve`` can be used with uvloop). In Python 3.7, or better, this can be done as follows, first you need to create a Hypercorn Config instance, .. code-block:: python from hypercorn.config import Config config = Config() config.bind = ["localhost:8080"] # As an example configuration setting Then assuming you have an ASGI or WSGI framework instance called ``app``, using asyncio, .. code-block:: python import asyncio from hypercorn.asyncio import serve asyncio.run(serve(app, config)) The same for Trio, .. code-block:: python import trio from hypercorn.trio import serve trio.run(serve, app, config) The same for uvloop, .. code-block:: python import asyncio import uvloop from hypercorn.asyncio import serve uvloop.install() asyncio.run(serve(app, config)) Features caveat --------------- The API usage assumes that you wish to control how the event loop is configured and where the event loop runs. Therefore the configuration options to change the worker class and number of workers have no affect when using serve. Graceful shutdown ----------------- To shutdown the app the ``serve`` function takes an additional ``shutdown_trigger`` argument that will be awaited by Hypercorn. If the ``shutdown_trigger`` returns it will trigger a graceful shutdown. An example use of this functionality is to shutdown on receipt of a TERM signal, .. code-block:: python import asyncio import signal shutdown_event = asyncio.Event() def _signal_handler(*_: Any) -> None: shutdown_event.set() loop = asyncio.get_event_loop() loop.add_signal_handler(signal.SIGTERM, _signal_handler) loop.run_until_complete( serve(app, config, shutdown_trigger=shutdown_event.wait) ) No signal handling ------------------ If you don't want any signal handling you can set the ``shutdown_trigger`` to return an awaitable that doesn't complete, for example returning an empty Future, .. code-block:: python loop.run_until_complete( serve(app, config, shutdown_trigger=lambda: asyncio.Future()) ) SSL Error reporting ------------------- SSLErrors can be raised during the SSL handshake with the connecting client. These errors are handled by the event loop and reported via the loop's exception handler. Using Hypercorn via the command line will mean that these errors are ignored. To ignore (or otherwise handle) these errors when using the API configure the event loop exception handler, .. code-block:: python def _exception_handler(loop, context): exception = context.get("exception") if isinstance(exception, ssl.SSLError): pass # Handshake failure else: loop.default_exception_handler(context) loop.set_exception_handler(_exception_handler) Forcing ASGI or WSGI mode ------------------------- The ``serve`` function takes a ``mode`` argument that can be ``"asgi"`` or ``"wsgi"`` to force the app to be considered ASGI or WSGI as required. hypercorn-0.14.4/docs/how_to_guides/binds.rst000066400000000000000000000023651445231714500212450ustar00rootroot00000000000000.. _binds: Binds ===== Hypercorn serves by binding to sockets, sockets are specified by their address and can be IPv4, IPv6, a unix domain (on unix) or a file descriptor. By default Hypercorn will bind to "127.0.0.1:8000". Unix domain ----------- To specify a unix domain socket use a ``unix:`` prefix before specify an address. For example, .. code-block:: sh $ hypercorn --bind unix:/tmp/socket.sock It is possible to control the permissions and ownership of the created socket using the ``umask``, ``user``, and ``group`` configurations respectively. File descriptor --------------- To specify a file descriptor to bind too use a ``fd://`` prefix before the descriptor number. For example, .. code-block:: sh $ hypercorn --bind fd://2 Multiple binds -------------- Hypercorn supports binding to multiple addresses and serving on all of them at the same time. This allows for example binding to an IPv4 and an IPv6 address. To do this simply specify multiple binds either on the command line, or in the configuration file. For example for a dual stack binding, .. code-block:: sh $ hypercorn --bind '0.0.0.0:5000' --bind '[::]:5000' ... or within the configuration file, .. code-block:: python bind = ["0.0.0.0:5000", "[::]:5000"] hypercorn-0.14.4/docs/how_to_guides/configuring.rst000066400000000000000000000260121445231714500224530ustar00rootroot00000000000000.. _how_to_configure: Configuring =========== Hypercorn is configured via a command line arguments, or via a :class:`hypercorn.config.Config` instance, which can be created manually, loaded from a TOML, Python file, Python module, or a dictionary instance. Via a TOML file --------------- `TOML `_ is the prefered format for Hypercorn configuration files. Files in this format can be loaded via the command line using the ``-c``, ``--config`` option e.g. .. code-block:: hypercorn --config file_path/file_name.toml To load programatically :meth:`hypercorn.config.Config.from_toml` can be used, .. code-block:: python config = Config() config.from_toml("file_path/file_name.toml") Via a Python module ------------------- A Python module or an instance within a python module can be used to configure Hypercorn. In both cases the attributes matching configuration values will be used. This can be specified via the command line using the ``-c``, ``--config`` option with the ``python:`` prefix e.g. .. code-block:: hypercorn --config python:module_name To load programatically :meth:`hypercorn.config.Config.from_object` can be used, .. code-block:: python config = Config() config.from_object("module_name.instance") Via a Python file ------------------- A Python file can be loaded and the attributes matching configuration values used to configure Hypercorn. This can be specified via the command line using the ``-c``, ``--config`` option with the ``file:`` prefix e.g. .. code-block:: hypercorn --config file:file_path/file_name.py To load programatically :meth:`hypercorn.config.Config.from_pyfile` can be used, .. code-block:: python config = Config() config.from_pyfile("file_path/file_name.py") Configuration options ===================== ========================== ============================= =============================================== ======================== Attribute Command line Purpose Default -------------------------- ----------------------------- ----------------------------------------------- ------------------------ access_log_format ``--access-logformat`` The log format for the access log, see :ref:`how_to_log`. accesslog ``--access-logfile`` The target logger for access logs, use ``-`` for stdout. alpn_protocols N/A The HTTP protocols to advertise over ``h2`` and ``http/1.1`` ALPN. alt_svc_headers N/A List of header values to return as Alt-Svc headers. application_path N/A The path location of the ASGI cwd application. backlog ``--backlog`` The maximum number of pending 100 connections. bind ``-b``, ``--bind`` The TCP host/address to bind to. Should be either host:port, host, unix:path or fd://num, e.g. 127.0.0.1:5000, 127.0.0.1, unix:/tmp/socket or fd://33 respectively. ca_certs ``--ca-certs`` Path to the SSL CA certificate file. certfile ``--certfile`` Path to the SSL certificate file. ciphers ``--ciphers`` Ciphers to use for the SSL setup. ``ECDHE+AESGCM`` debug ``--debug`` Enable debug mode, i.e. extra logging ``False`` and checks. dogstatsd_tags N/A DogStatsd format tag, see :ref:`using_statsd`. errorlog ``--error-logfile`` The target location for the error log, ``--log-file`` use `-` for stderr. graceful_timeout ``--graceful-timeout`` Time to wait after SIGTERM or Ctrl-C for any remaining requests (tasks) to read_timeout ``--read-timeout`` Seconds to wait before timing out reads No timeout. on TCP sockets. group ``-g``, ``--group`` Group to own any unix sockets. h11_max_incomplete_size N/A The max HTTP/1.1 request line + headers 16KiB size in bytes. h11_pass_raw_headers N/A Pass the raw headers from h11 to the ``False`` Request object, which preserves header casing. h2_max_concurrent_streams N/A Maximum number of HTTP/2 concurrent 100 streams. h2_max_header_list_size N/A Maximum number of HTTP/2 headers. 65536 h2_max_inbound_frame_size N/A Maximum size of a HTTP/2 frame. 16KiB include_date_header N/A Include the ``True`` ``Date: Tue, 15 Nov 1994 08:12:31 GMT`` header. include_server_header N/A Include the ``Server: Hypercorn`` header. ``True`` insecure_bind ``--insecure-bind`` The TCP host/address to bind to. SSL options will not apply to these binds. See *bind* for formatting options. Care must be taken! See HTTP -> HTTPS redirection docs. keep_alive_timeout ``--keep-alive`` Seconds to keep inactive connections alive 5s before closing. keyfile ``--keyfile`` Path to the SSL key file. logconfig ``--log-config`` A Python logging configuration file. This The logging ini format. can be prefixed with 'json:' or 'toml:' to load the configuration from a file in that format. logconfig_dict N/A A Python logging configuration dictionary. logger_class N/A Type of class to use for logging. loglevel ``--log-level`` The (error) log level. ``INFO`` max_app_queue_size N/A The maximum number of events to queue up 10 sending to the ASGI application. pid_path ``-p``, ``--pid`` Location to write the PID (Program ID) to. quic_bind ``--quic-bind`` The UDP/QUIC host/address to bind to. See *bind* for formatting options. root_path ``--root-path`` The setting for the ASGI root_path variable. server_names ``--server-name`` The hostnames that can be served, requests to different hosts will be responded to with 404s. shutdown_timeout N/A Timeout when waiting for Lifespan 60s shutdowns to complete. ssl_handshake_timeout N/A Timeout when waiting for SSL handshakes to 60s complete. startup_timeout N/A Timeout when waiting for Lifespan 60s startups to complete. statsd_host ``--statsd-host`` The host:port of the statsd server. statsd_prefix ``--statsd-prefix`` Prefix for all statsd messages. umask ``-m``, ``--umask`` The permissions bit mask to use on any unix sockets. use_reloader ``--reload`` Enable automatic reloads on code changes. user ``-u``, ``--user`` User to own any unix sockets. verify_flags N/A SSL context verify flags. verify_mode ``--verify-mode`` SSL verify mode for peer's certificate, see ssl.VerifyMode enum for possible values. websocket_max_message_size N/A Maximum size of a WebSocket frame. 16MiB websocket_ping_interval ``--websocket-ping-interval`` If set this is the time in seconds between pings sent to the client. This can be used to keep the websocket connection alive. worker_class ``-k``, ``--worker-class`` The type of worker to use. Options include asyncio, uvloop (pip install hypercorn[uvloop]), and trio (pip install hypercorn[trio]). workers ``-w``, ``--workers`` The number of workers to spawn and use. 1 wsgi_max_body_size N/A The maximum size of a body that will be 16MiB accepted in WSGI mode. ========================== ============================= =============================================== ======================== hypercorn-0.14.4/docs/how_to_guides/dispatch_apps.rst000066400000000000000000000025161445231714500227660ustar00rootroot00000000000000.. _dispatch_apps: Dispatch to multiple ASGI applications ====================================== It is often useful serve multiple ASGI applications at once, under differing root paths. Hypercorn does not support this directly, but the ``DispatcherMiddleware`` included with Hypercorn can. This middleware allows multiple applications to be served on different mounts. The ``DispatcherMiddleware`` takes a dictionary of applications keyed by the root path. The order of entry in this dictionary is important, as the root paths will be checked in this order. Hence it is important to add ``/a/b`` before ``/a`` or the latter will match everything first. Also note that the root path should not include the trailing slash. An example usage is to to serve a graphql application alongside a static file serving application. Using the graphql app is called ``graphql_app`` serving everything with the root path ``/graphql`` and a static file app called ``static_app`` serving everything else i.e. a root path of ``/`` the ``DispatcherMiddleware`` can be setup as, .. code-block:: python from hypercorn.middleware import DispatcherMiddleware dispatcher_app = DispatcherMiddleware({ "/graphql": graphql_app, "/": static_app, }) which can then be served by hypercorn, .. code-block:: shell $ hypercorn module:dispatcher_app hypercorn-0.14.4/docs/how_to_guides/http_https_redirect.rst000066400000000000000000000035451445231714500242310ustar00rootroot00000000000000.. _http_https_redirect: HTTP to HTTPS Redirects ======================= When serving over HTTPS it is often desired (and wise) to redirect any HTTP requests to HTTPS. To do this Hypercorn must listen to requests on secure and insecure binds. This is possible using the ``insecure-bind`` option which specifies binds that will be insecure regardless of the SSL settings. For example, .. code-block:: shell $ hypercorn --certfile cert.pem --keyfile key.pem --bind localhost:443 --insecure-bind localhost:80 module:app will serve on 443 over HTTPS and 80 over HTTP. .. warning:: Care must be taken when serving over secure and insecure binds to ensure that only redirects are served over HTTP. Hypercorn will not and cannot ensure this for you. Middleware ---------- With Hypercorn listening on both secure and insecure binds middleware such as the one in the hypercorn middleware module, :class:`~hypercorn.middleware.HTTPToHTTPSRedirectMiddleware`, can be used to ensure HTTP requests are redirected to HTTPS. Alternatively you can do this directly in your ASGI application. .. warning:: Ensure that any redirection middleware is the outermost wrapper of your app i.e. ensure that only the redirection middleware receives HTTP requests. To use the ``HTTPToHTTPSRedirectMiddleware`` wrap your app and specify the host the redirects should be aimed at. If you want to redirect users from ``http://example.com`` to ``https://example.com`` the host should be ``example.com`` as in the example below, .. code-block:: python redirected_app = HTTPToHTTPSRedirectMiddleware(app, host="example.com") You can then serve the redirect_app over a secure and an insecure bind as explained above, for example, .. code-block:: shell $ hypercorn --certfile cert.pem --keyfile key.pem --bind localhost:443 --insecure-bind localhost:80 module:redirected_app hypercorn-0.14.4/docs/how_to_guides/index.rst000066400000000000000000000003541445231714500212510ustar00rootroot00000000000000============= How to guides ============= .. toctree:: :maxdepth: 1 api_usage.rst binds.rst configuring.rst dispatch_apps.rst http_https_redirect.rst logging.rst server_names.rst statsd.rst wsgi_apps.rst hypercorn-0.14.4/docs/how_to_guides/logging.rst000066400000000000000000000051501445231714500215670ustar00rootroot00000000000000.. _how_to_log: Logging ======= Hypercorn has two loggers, an access logger and an error logger. By default neither will actively log. The special value of ``-`` can be used as the logging target in order to log to stdout and stderr respectively. Any other value is considered a filepath to target. Configuring the Python logger ----------------------------- The Python logger can be configured using the ``logconfig`` or ``logconfig_dict`` configuration attributes. The latter, ``logconfig_dict`` will be passed to ``dictConfig`` after the loggers have been created. The ``logconfig`` variable should point at a file to be used by the ``fileConfig`` function. Alternatively it can point to a JSON or TOML formatted file which will be loaded and passed to the ``dictConfig`` function. To use a JSON formatted file prefix the filepath with ``json:`` and for TOML use ``toml:``. Configuring access logs ----------------------- The access log format can be configured by specifying the atoms (see below) to include in a specific format. By default hypercorn will choose ``%(h)s %(l)s %(l)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"`` as the format. The configuration variable ``access_log_format`` specifies the format used. Access log atoms ```````````````` The following atoms, a superset of those in `Gunicorn `_, are available for use. =========== =========== Identifier Description =========== =========== h remote address l ``'-'`` u user name t date of the request r status line without query string (e.g. ``GET / h11``) R status line with query string (e.g. ``GET /?a=b h11``) m request method U URL path without query string Uq URL path with query string q query string H protocol s status st status phrase (e.g. ``OK``, ``Forbidden``, ``Not Found``) S scheme {http, https, ws, wss} B response length b response length or ``'-'`` (CLF format) f referer a user agent T request time in seconds D request time in microseconds L request time in decimal seconds p process ID {Header}i request header {Header}o response header {Variable}e environment variable =========== =========== Customising the logger ---------------------- The logger class can be customised by changing the ``logger_class`` attribute of the ``Config`` class. This is only possible when using the python based configuration file. The ``hypercorn.logging.Logger`` class is used by default. hypercorn-0.14.4/docs/how_to_guides/server_names.rst000066400000000000000000000010061445231714500226260ustar00rootroot00000000000000.. _server_names: Server names ============ Hypercorn can be configured to only respond to requests that have a recognised host header value by adding the recognised hosts to the ``server_names`` configuration variable. Any requests that have a host value not in this list will be responded to with a 404. DNS rebinding attacks --------------------- Setting the ``server_names`` configuration variable helps mitigate `DNS rebinding attacks `_ and hence is recommended. hypercorn-0.14.4/docs/how_to_guides/statsd.rst000066400000000000000000000023301445231714500214400ustar00rootroot00000000000000.. _using_statsd: Statsd Logging ============== Hypercorn can optionally log metrics using the `StatsD `_ or `DogStatsD `_ protocols. The metrics logged are, - ``hypercorn.requests``: rate of requests - ``hypercorn.request.duration``: request duration in milliseconds - ``hypercorn.request.status.[#code]``: rate of responses by status code - ``hypercorn.log.critical``: rate of critical log messages - ``hypercorn.log.error``: rate of error log messages - ``hypercorn.log.warning``: rate of warning log messages - ``hypercorn.log.exception``: rate of exceptional log messages Usage ----- Setting the config ``statsd_host`` to ``[host]:[port]`` will result in these metrics being set to that host, port combination. The config ``statsd_prefix`` can be used to prefix all metrics and ``dogstatsd_tags`` can be used to add tags to each metric. Customising the statsd logger ----------------------------- The statsd logger class can be customised by calling ``set_statsd_logger_class`` method of the ``Config`` class. This is only possible when using the python based configuration file. The ``hypercorn.statsd.StatsdLogger`` class is used by default. hypercorn-0.14.4/docs/how_to_guides/wsgi_apps.rst000066400000000000000000000024631445231714500221410ustar00rootroot00000000000000.. _wsgi_apps: Serve WSGI applications ======================= Hypercorn directly serves WSGI applications: .. code-block:: shell $ hypercorn module:wsgi_app .. warning:: The full response from the WSGI app will be stored in memory before being sent. This prevents the WSGI app from streaming a response. WSGI Middleware --------------- If a WSGI application is being combined with ASGI middleware it is best to use either ``AsyncioWSGIMiddleware`` or ``TrioWSGIMiddleware`` middleware. To do so simply wrap the WSGI app with the appropriate middleware for the hypercorn worker, .. code-block:: python from hypercorn.middleware import AsyncioWSGIMiddleware, TrioWSGIMiddleware asyncio_app = AsyncioWSGIMiddleware(wsgi_app) trio_app = TrioWSGIMiddleware(wsgi_app) which can then be passed to other middleware served by hypercorn, Limiting the request body size ------------------------------ As the request body is stored in memory before being processed it is important to limit the max size. This is configured by the ``wsgi_max_body_size`` configuration attribute. When using middleware the ``AsyncioWSGIMiddleware`` and ``TrioWSGIMiddleware`` have a default max size that can be configured, .. code-block:: python app = AsyncioWSGIMiddleware(wsgi_app, max_body_size=20) # Bytes hypercorn-0.14.4/docs/index.rst000066400000000000000000000022161445231714500164110ustar00rootroot00000000000000:orphan: .. title:: Hypercorn documentation .. image:: _static/logo.png :width: 300px :alt: Hypercorn Hypercorn is an `ASGI `_ web server based on the sans-io hyper, `h11 `_, `h2 `_, and `wsproto `_ libraries and inspired by Gunicorn. Hypercorn supports HTTP/1, HTTP/2, WebSockets (over HTTP/1 and HTTP/2), ASGI/2, and ASGI/3 specifications. Hypercorn can utilise asyncio, uvloop, or trio worker types. Hypercorn was initially part of `Quart `_ before being separated out into a standalone ASGI server. Hypercorn forked from version 0.5.0 of Quart. Hypercorn is developed on `Github `_. You are very welcome to open `issues `_ or propose `pull requests `_. Contents -------- .. toctree:: :maxdepth: 2 tutorials/index.rst how_to_guides/index.rst discussion/index.rst reference/index.rst hypercorn-0.14.4/docs/make.bat000066400000000000000000000014471445231714500161620ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=Hypercorn if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd hypercorn-0.14.4/docs/reference/000077500000000000000000000000001445231714500165055ustar00rootroot00000000000000hypercorn-0.14.4/docs/reference/api.rst000066400000000000000000000001501445231714500200040ustar00rootroot00000000000000API Reference ============= .. toctree:: :maxdepth: 2 :caption: Contents: source/modules.rst hypercorn-0.14.4/docs/reference/index.rst000066400000000000000000000001101445231714500203360ustar00rootroot00000000000000========= Reference ========= .. toctree:: :maxdepth: 1 api.rst hypercorn-0.14.4/docs/tutorials/000077500000000000000000000000001445231714500165755ustar00rootroot00000000000000hypercorn-0.14.4/docs/tutorials/index.rst000066400000000000000000000001601445231714500204330ustar00rootroot00000000000000========= Tutorials ========= .. toctree:: :maxdepth: 1 installation.rst quickstart.rst usage.rst hypercorn-0.14.4/docs/tutorials/installation.rst000066400000000000000000000013551445231714500220340ustar00rootroot00000000000000.. _installation: Installation ============ Hypercorn is only compatible with Python 3.7 or higher and can be installed using pipenv or your favorite python package manager. .. code-block:: sh pipenv install hypercorn It is sufficient to run this single command in your working directory. Besides installing dependency, it will also create a Pipfile if one doesn't exist yet along with a linked virtualenv. Now you'll be able to activate your virtualenv using: .. code-block:: sh pipenv shell To learn more about it visit `pipenv docs `_ If you do not have Python 3.7 or better an error message ``Python 3.7 is the minimum required version`` will be displayed. hypercorn-0.14.4/docs/tutorials/quickstart.rst000066400000000000000000000014621445231714500215240ustar00rootroot00000000000000.. _quickstart: Quickstart ========== Hello World ----------- A very simple ASGI app that simply returns a response containing ``hello`` is, (file ``hello_world.py``) .. code-block:: python async def app(scope, receive, send): if scope["type"] != "http": raise Exception("Only the HTTP protocol is supported") await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ (b'content-type', b'text/plain'), (b'content-length', b'5'), ], }) await send({ 'type': 'http.response.body', 'body': b'hello', }) and is simply run via .. code-block:: console hypercorn hello_world:app and tested by .. code-block:: sh curl localhost:8000 hypercorn-0.14.4/docs/tutorials/usage.rst000066400000000000000000000011111445231714500204250ustar00rootroot00000000000000.. _usage: Usage ===== Hypercorn is invoked via the command line script ``hypercorn`` .. code-block:: shell $ hypercorn [OPTIONS] MODULE_APP where ``MODULE_APP`` has the pattern ``$(MODULE_NAME):$(VARIABLE_NAME)`` with the module name as a full (dotted) path to a python module containing a named variable that conforms to the ASGI or WSGI framework specifications. The ``MODULE_APP`` can be prefixed with ``asgi:`` or ``wsgi:`` to ensure that the loaded app is treated as either an asgi or wsgi app. See :ref:`how_to_configure` for the full list of command line arguments. hypercorn-0.14.4/pyproject.toml000066400000000000000000000056341445231714500165430ustar00rootroot00000000000000[tool.poetry] name = "Hypercorn" version = "0.14.4" description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" authors = ["pgjones "] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", ] include = ["src/hypercorn/py.typed"] license = "MIT" readme = "README.rst" repository = "https://github.com/pgjones/hypercorn/" documentation = "https://hypercorn.readthedocs.io" [tool.poetry.dependencies] python = ">=3.7" aioquic = { version = ">= 0.9.0, < 1.0", optional = true } exceptiongroup = { version = ">= 1.1.0", python = "<3.11", optional = true } h11 = "*" h2 = ">=3.1.0" priority = "*" pydata_sphinx_theme = { version = "*", optional = true } tomli = { version = "*", python = "<3.11" } trio = { version = ">=0.22.0", optional = true } typing_extensions = { version = ">=3.7.4", python = "<3.8" } uvloop = { version = "*", markers = "platform_system != 'Windows'", optional = true } wsproto = ">=0.14.0" [tool.poetry.dev-dependencies] hypothesis = "*" mock = "*" pytest = "*" pytest-asyncio = "*" pytest-trio = "*" trio = "*" [tool.poetry.scripts] hypercorn = "hypercorn.__main__:main" [tool.poetry.extras] docs = ["pydata_sphinx_theme"] h3 = ["aioquic"] trio = ["exceptiongroup", "trio"] uvloop = ["uvloop"] [tool.black] line-length = 100 target-version = ["py37"] [tool.isort] combine_as_imports = true force_grid_wrap = 0 include_trailing_comma = true known_first_party = "hypercorn, tests" line_length = 100 multi_line_output = 3 no_lines_before = "LOCALFOLDER" order_by_type = false reverse_relative = true [tool.mypy] allow_redefinition = true disallow_any_generics = false disallow_subclassing_any = true disallow_untyped_calls = false disallow_untyped_defs = true implicit_reexport = true no_implicit_optional = true show_error_codes = true strict = true strict_equality = true strict_optional = false warn_redundant_casts = true warn_return_any = false warn_unused_configs = true warn_unused_ignores = true [[tool.mypy.overrides]] module =["aioquic.*", "cryptography.*", "h11.*", "h2.*", "priority.*", "pytest_asyncio.*", "trio.*", "uvloop.*"] ignore_missing_imports = true [tool.pytest.ini_options] addopts = "--no-cov-on-fail --showlocals --strict-markers" asyncio_mode = "strict" testpaths = ["tests"] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" hypercorn-0.14.4/setup.cfg000066400000000000000000000001531445231714500154370ustar00rootroot00000000000000[flake8] ignore = E203, E252, FI58, W503, W504 max_line_length = 100 min_version = 3.7 require_code = True hypercorn-0.14.4/src/000077500000000000000000000000001445231714500144065ustar00rootroot00000000000000hypercorn-0.14.4/src/hypercorn/000077500000000000000000000000001445231714500164175ustar00rootroot00000000000000hypercorn-0.14.4/src/hypercorn/__init__.py000066400000000000000000000001261445231714500205270ustar00rootroot00000000000000from __future__ import annotations from .config import Config __all__ = ("Config",) hypercorn-0.14.4/src/hypercorn/__main__.py000066400000000000000000000241441445231714500205160ustar00rootroot00000000000000from __future__ import annotations import argparse import ssl import sys import warnings from typing import List, Optional from .config import Config from .run import run sentinel = object() def _load_config(config_path: Optional[str]) -> Config: if config_path is None: return Config() elif config_path.startswith("python:"): return Config.from_object(config_path[len("python:") :]) elif config_path.startswith("file:"): return Config.from_pyfile(config_path[len("file:") :]) else: return Config.from_toml(config_path) def main(sys_args: Optional[List[str]] = None) -> None: parser = argparse.ArgumentParser() parser.add_argument( "application", help="The application to dispatch to as path.to.module:instance.path" ) parser.add_argument("--access-log", help="Deprecated, see access-logfile", default=sentinel) parser.add_argument( "--access-logfile", help="The target location for the access log, use `-` for stdout", default=sentinel, ) parser.add_argument( "--access-logformat", help="The log format for the access log, see help docs", default=sentinel, ) parser.add_argument( "--backlog", help="The maximum number of pending connections", type=int, default=sentinel ) parser.add_argument( "-b", "--bind", dest="binds", help=""" The TCP host/address to bind to. Should be either host:port, host, unix:path or fd://num, e.g. 127.0.0.1:5000, 127.0.0.1, unix:/tmp/socket or fd://33 respectively. """, default=[], action="append", ) parser.add_argument("--ca-certs", help="Path to the SSL CA certificate file", default=sentinel) parser.add_argument("--certfile", help="Path to the SSL certificate file", default=sentinel) parser.add_argument("--cert-reqs", help="See verify mode argument", type=int, default=sentinel) parser.add_argument("--ciphers", help="Ciphers to use for the SSL setup", default=sentinel) parser.add_argument( "-c", "--config", help="Location of a TOML config file, or when prefixed with `file:` a Python file, or when prefixed with `python:` a Python module.", # noqa: E501 default=None, ) parser.add_argument( "--debug", help="Enable debug mode, i.e. extra logging and checks", action="store_true", default=sentinel, ) parser.add_argument("--error-log", help="Deprecated, see error-logfile", default=sentinel) parser.add_argument( "--error-logfile", "--log-file", dest="error_logfile", help="The target location for the error log, use `-` for stderr", default=sentinel, ) parser.add_argument( "--graceful-timeout", help="""Time to wait after SIGTERM or Ctrl-C for any remaining requests (tasks) to complete.""", default=sentinel, type=int, ) parser.add_argument( "--read-timeout", help="""Seconds to wait before timing out reads on TCP sockets""", default=sentinel, type=int, ) parser.add_argument( "-g", "--group", help="Group to own any unix sockets.", default=sentinel, type=int ) parser.add_argument( "-k", "--worker-class", dest="worker_class", help="The type of worker to use. " "Options include asyncio, uvloop (pip install hypercorn[uvloop]), " "and trio (pip install hypercorn[trio]).", default=sentinel, ) parser.add_argument( "--keep-alive", help="Seconds to keep inactive connections alive for", default=sentinel, type=int, ) parser.add_argument("--keyfile", help="Path to the SSL key file", default=sentinel) parser.add_argument( "--keyfile-password", help="Password to decrypt the SSL key file", default=sentinel ) parser.add_argument( "--insecure-bind", dest="insecure_binds", help="""The TCP host/address to bind to. SSL options will not apply to these binds. See *bind* for formatting options. Care must be taken! See HTTP -> HTTPS redirection docs. """, default=[], action="append", ) parser.add_argument( "--log-config", help=""""A Python logging configuration file. This can be prefixed with 'json:' or 'toml:' to load the configuration from a file in that format. Default is the logging ini format.""", default=sentinel, ) parser.add_argument( "--log-level", help="The (error) log level, defaults to info", default=sentinel ) parser.add_argument( "-p", "--pid", help="Location to write the PID (Program ID) to.", default=sentinel ) parser.add_argument( "--quic-bind", dest="quic_binds", help="""The UDP/QUIC host/address to bind to. See *bind* for formatting options. """, default=[], action="append", ) parser.add_argument( "--reload", help="Enable automatic reloads on code changes", action="store_true", default=sentinel, ) parser.add_argument( "--root-path", help="The setting for the ASGI root_path variable", default=sentinel ) parser.add_argument( "--server-name", dest="server_names", help="""The hostnames that can be served, requests to different hosts will be responded to with 404s. """, default=[], action="append", ) parser.add_argument( "--statsd-host", help="The host:port of the statsd server", default=sentinel ) parser.add_argument("--statsd-prefix", help="Prefix for all statsd messages", default="") parser.add_argument( "-m", "--umask", help="The permissions bit mask to use on any unix sockets.", default=sentinel, type=int, ) parser.add_argument( "-u", "--user", help="User to own any unix sockets.", default=sentinel, type=int ) def _convert_verify_mode(value: str) -> ssl.VerifyMode: try: return ssl.VerifyMode[value] except KeyError: raise argparse.ArgumentTypeError(f"'{value}' is not a valid verify mode") parser.add_argument( "--verify-mode", help="SSL verify mode for peer's certificate, see ssl.VerifyMode enum for possible values.", type=_convert_verify_mode, default=sentinel, ) parser.add_argument( "--websocket-ping-interval", help="""If set this is the time in seconds between pings sent to the client. This can be used to keep the websocket connection alive.""", default=sentinel, type=int, ) parser.add_argument( "-w", "--workers", dest="workers", help="The number of workers to spawn and use", default=sentinel, type=int, ) args = parser.parse_args(sys_args or sys.argv[1:]) config = _load_config(args.config) config.application_path = args.application if args.log_level is not sentinel: config.loglevel = args.log_level if args.access_logformat is not sentinel: config.access_log_format = args.access_logformat if args.access_log is not sentinel: warnings.warn( "The --access-log argument is deprecated, use `--access-logfile` instead", DeprecationWarning, ) config.accesslog = args.access_log if args.access_logfile is not sentinel: config.accesslog = args.access_logfile if args.backlog is not sentinel: config.backlog = args.backlog if args.ca_certs is not sentinel: config.ca_certs = args.ca_certs if args.certfile is not sentinel: config.certfile = args.certfile if args.cert_reqs is not sentinel: config.cert_reqs = args.cert_reqs if args.ciphers is not sentinel: config.ciphers = args.ciphers if args.debug is not sentinel: config.debug = args.debug if args.error_log is not sentinel: warnings.warn( "The --error-log argument is deprecated, use `--error-logfile` instead", DeprecationWarning, ) config.errorlog = args.error_log if args.error_logfile is not sentinel: config.errorlog = args.error_logfile if args.graceful_timeout is not sentinel: config.graceful_timeout = args.graceful_timeout if args.read_timeout is not sentinel: config.read_timeout = args.read_timeout if args.group is not sentinel: config.group = args.group if args.keep_alive is not sentinel: config.keep_alive_timeout = args.keep_alive if args.keyfile is not sentinel: config.keyfile = args.keyfile if args.keyfile_password is not sentinel: config.keyfile_password = args.keyfile_password if args.log_config is not sentinel: config.logconfig = args.log_config if args.pid is not sentinel: config.pid_path = args.pid if args.root_path is not sentinel: config.root_path = args.root_path if args.reload is not sentinel: config.use_reloader = args.reload if args.statsd_host is not sentinel: config.statsd_host = args.statsd_host if args.statsd_prefix is not sentinel: config.statsd_prefix = args.statsd_prefix if args.umask is not sentinel: config.umask = args.umask if args.user is not sentinel: config.user = args.user if args.worker_class is not sentinel: config.worker_class = args.worker_class if args.verify_mode is not sentinel: config.verify_mode = args.verify_mode if args.websocket_ping_interval is not sentinel: config.websocket_ping_interval = args.websocket_ping_interval if args.workers is not sentinel: config.workers = args.workers if len(args.binds) > 0: config.bind = args.binds if len(args.insecure_binds) > 0: config.insecure_bind = args.insecure_binds if len(args.quic_binds) > 0: config.quic_bind = args.quic_binds if len(args.server_names) > 0: config.server_names = args.server_names run(config) if __name__ == "__main__": main() hypercorn-0.14.4/src/hypercorn/app_wrappers.py000066400000000000000000000116201445231714500214740ustar00rootroot00000000000000from __future__ import annotations from functools import partial from io import BytesIO from typing import Callable, List, Optional, Tuple from .typing import ( ASGIFramework, ASGIReceiveCallable, ASGISendCallable, HTTPScope, Scope, WSGIFramework, ) class InvalidPathError(Exception): pass class ASGIWrapper: def __init__(self, app: ASGIFramework) -> None: self.app = app async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable, sync_spawn: Callable, call_soon: Callable, ) -> None: await self.app(scope, receive, send) class WSGIWrapper: def __init__(self, app: WSGIFramework, max_body_size: int) -> None: self.app = app self.max_body_size = max_body_size async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable, sync_spawn: Callable, call_soon: Callable, ) -> None: if scope["type"] == "http": await self.handle_http(scope, receive, send, sync_spawn, call_soon) elif scope["type"] == "websocket": await send({"type": "websocket.close"}) # type: ignore elif scope["type"] == "lifespan": return else: raise Exception(f"Unknown scope type, {scope['type']}") async def handle_http( self, scope: HTTPScope, receive: ASGIReceiveCallable, send: ASGISendCallable, sync_spawn: Callable, call_soon: Callable, ) -> None: body = bytearray() while True: message = await receive() body.extend(message.get("body", b"")) # type: ignore if len(body) > self.max_body_size: await send({"type": "http.response.start", "status": 400, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) return if not message.get("more_body"): break try: environ = _build_environ(scope, body) except InvalidPathError: await send({"type": "http.response.start", "status": 404, "headers": []}) else: await sync_spawn(self.run_app, environ, partial(call_soon, send)) await send({"type": "http.response.body", "body": b"", "more_body": False}) def run_app(self, environ: dict, send: Callable) -> None: headers: List[Tuple[bytes, bytes]] status_code: Optional[int] = None def start_response( status: str, response_headers: List[Tuple[str, str]], exc_info: Optional[Exception] = None, ) -> None: nonlocal headers, status_code raw, _ = status.split(" ", 1) status_code = int(raw) headers = [ (name.lower().encode("ascii"), value.encode("ascii")) for name, value in response_headers ] send({"type": "http.response.start", "status": status_code, "headers": headers}) for output in self.app(environ, start_response): send({"type": "http.response.body", "body": output, "more_body": True}) def _build_environ(scope: HTTPScope, body: bytes) -> dict: server = scope.get("server") or ("localhost", 80) path = scope["path"] script_name = scope.get("root_path", "") if path.startswith(script_name): path = path[len(script_name) :] path = path if path != "" else "/" else: raise InvalidPathError() environ = { "REQUEST_METHOD": scope["method"], "SCRIPT_NAME": script_name.encode("utf8").decode("latin1"), "PATH_INFO": path.encode("utf8").decode("latin1"), "QUERY_STRING": scope["query_string"].decode("ascii"), "SERVER_NAME": server[0], "SERVER_PORT": server[1], "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], "wsgi.version": (1, 0), "wsgi.url_scheme": scope.get("scheme", "http"), "wsgi.input": BytesIO(body), "wsgi.errors": BytesIO(), "wsgi.multithread": True, "wsgi.multiprocess": True, "wsgi.run_once": False, } if "client" in scope: environ["REMOTE_ADDR"] = scope["client"][0] for raw_name, raw_value in scope.get("headers", []): name = raw_name.decode("latin1") if name == "content-length": corrected_name = "CONTENT_LENGTH" elif name == "content-type": corrected_name = "CONTENT_TYPE" else: corrected_name = "HTTP_%s" % name.upper().replace("-", "_") # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case value = raw_value.decode("latin1") if corrected_name in environ: value = environ[corrected_name] + "," + value # type: ignore environ[corrected_name] = value return environ hypercorn-0.14.4/src/hypercorn/asyncio/000077500000000000000000000000001445231714500200645ustar00rootroot00000000000000hypercorn-0.14.4/src/hypercorn/asyncio/__init__.py000066400000000000000000000030241445231714500221740ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import Awaitable, Callable, Optional from .run import worker_serve from ..config import Config from ..typing import Framework from ..utils import wrap_app try: from typing import Literal except ImportError: from typing_extensions import Literal # type: ignore async def serve( app: Framework, config: Config, *, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, mode: Optional[Literal["asgi", "wsgi"]] = None, ) -> None: """Serve an ASGI or WSGI framework app given the config. This allows for a programmatic way to serve an ASGI or WSGI framework, it can be used via, .. code-block:: python asyncio.run(serve(app, config)) It is assumed that the event-loop is configured before calling this function, therefore configuration values that relate to loop setup or process setup are ignored. Arguments: app: The ASGI or WSGI application to serve. config: A Hypercorn configuration object. shutdown_trigger: This should return to trigger a graceful shutdown. mode: Specify if the app is WSGI or ASGI. """ if config.debug: warnings.warn("The config `debug` has no affect when using serve", Warning) if config.workers != 1: warnings.warn("The config `workers` has no affect when using serve", Warning) await worker_serve( wrap_app(app, config.wsgi_max_body_size, mode), config, shutdown_trigger=shutdown_trigger ) hypercorn-0.14.4/src/hypercorn/asyncio/lifespan.py000066400000000000000000000074521445231714500222470ustar00rootroot00000000000000from __future__ import annotations import asyncio from functools import partial from typing import Any, Callable from ..config import Config from ..typing import AppWrapper, ASGIReceiveEvent, ASGISendEvent, LifespanScope from ..utils import LifespanFailureError, LifespanTimeoutError class UnexpectedMessageError(Exception): pass class Lifespan: def __init__(self, app: AppWrapper, config: Config, loop: asyncio.AbstractEventLoop) -> None: self.app = app self.config = config self.startup = asyncio.Event() self.shutdown = asyncio.Event() self.app_queue: asyncio.Queue = asyncio.Queue(config.max_app_queue_size) self.supported = True self.loop = loop # This mimics the Trio nursery.start task_status and is # required to ensure the support has been checked before # waiting on timeouts. self._started = asyncio.Event() async def handle_lifespan(self) -> None: self._started.set() scope: LifespanScope = { "type": "lifespan", "asgi": {"spec_version": "2.0", "version": "3.0"}, } def _call_soon(func: Callable, *args: Any) -> Any: future = asyncio.run_coroutine_threadsafe(func(*args), self.loop) return future.result() try: await self.app( scope, self.asgi_receive, self.asgi_send, partial(self.loop.run_in_executor, None), _call_soon, ) except LifespanFailureError: # Lifespan failures should crash the server raise except Exception: self.supported = False if not self.startup.is_set(): await self.config.log.warning( "ASGI Framework Lifespan error, continuing without Lifespan support" ) elif not self.shutdown.is_set(): await self.config.log.exception( "ASGI Framework Lifespan error, shutdown without Lifespan support" ) else: await self.config.log.exception("ASGI Framework Lifespan errored after shutdown.") finally: self.startup.set() self.shutdown.set() async def wait_for_startup(self) -> None: await self._started.wait() if not self.supported: return await self.app_queue.put({"type": "lifespan.startup"}) try: await asyncio.wait_for(self.startup.wait(), timeout=self.config.startup_timeout) except asyncio.TimeoutError as error: raise LifespanTimeoutError("startup") from error async def wait_for_shutdown(self) -> None: await self._started.wait() if not self.supported: return await self.app_queue.put({"type": "lifespan.shutdown"}) try: await asyncio.wait_for(self.shutdown.wait(), timeout=self.config.shutdown_timeout) except asyncio.TimeoutError as error: raise LifespanTimeoutError("shutdown") from error async def asgi_receive(self) -> ASGIReceiveEvent: return await self.app_queue.get() async def asgi_send(self, message: ASGISendEvent) -> None: if message["type"] == "lifespan.startup.complete": self.startup.set() elif message["type"] == "lifespan.shutdown.complete": self.shutdown.set() elif message["type"] == "lifespan.startup.failed": self.startup.set() raise LifespanFailureError("startup", message.get("message", "")) elif message["type"] == "lifespan.shutdown.failed": self.shutdown.set() raise LifespanFailureError("shutdown", message.get("message", "")) else: raise UnexpectedMessageError(message["type"]) hypercorn-0.14.4/src/hypercorn/asyncio/run.py000066400000000000000000000212111445231714500212370ustar00rootroot00000000000000from __future__ import annotations import asyncio import platform import signal import ssl from functools import partial from multiprocessing.synchronize import Event as EventType from os import getpid from socket import socket from typing import Any, Awaitable, Callable, Optional from weakref import WeakSet from .lifespan import Lifespan from .statsd import StatsdLogger from .tcp_server import TCPServer from .udp_server import UDPServer from .worker_context import WorkerContext from ..config import Config, Sockets from ..typing import AppWrapper from ..utils import ( check_multiprocess_shutdown_event, load_application, raise_shutdown, repr_socket_addr, ShutdownError, ) async def _windows_signal_support() -> None: # See https://bugs.python.org/issue23057, to catch signals on # Windows it is necessary for an IO event to happen periodically. # Fixed by Python 3.8 while True: await asyncio.sleep(1) def _share_socket(sock: socket) -> socket: # Windows requires the socket be explicitly shared across # multiple workers (processes). from socket import fromshare # type: ignore sock_data = sock.share(getpid()) # type: ignore return fromshare(sock_data) async def worker_serve( app: AppWrapper, config: Config, *, sockets: Optional[Sockets] = None, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, ) -> None: config.set_statsd_logger_class(StatsdLogger) loop = asyncio.get_event_loop() if shutdown_trigger is None: signal_event = asyncio.Event() def _signal_handler(*_: Any) -> None: # noqa: N803 signal_event.set() for signal_name in {"SIGINT", "SIGTERM", "SIGBREAK"}: if hasattr(signal, signal_name): try: loop.add_signal_handler(getattr(signal, signal_name), _signal_handler) except NotImplementedError: # Add signal handler may not be implemented on Windows signal.signal(getattr(signal, signal_name), _signal_handler) shutdown_trigger = signal_event.wait # type: ignore lifespan = Lifespan(app, config, loop) lifespan_task = loop.create_task(lifespan.handle_lifespan()) await lifespan.wait_for_startup() if lifespan_task.done(): exception = lifespan_task.exception() if exception is not None: raise exception if sockets is None: sockets = config.create_sockets() ssl_handshake_timeout = None if config.ssl_enabled: ssl_context = config.create_ssl_context() ssl_handshake_timeout = config.ssl_handshake_timeout context = WorkerContext() server_tasks: WeakSet = WeakSet() async def _server_callback(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: server_tasks.add(asyncio.current_task(loop)) await TCPServer(app, loop, config, context, reader, writer) servers = [] for sock in sockets.secure_sockets: if config.workers > 1 and platform.system() == "Windows": sock = _share_socket(sock) servers.append( await asyncio.start_server( _server_callback, backlog=config.backlog, ssl=ssl_context, sock=sock, ssl_handshake_timeout=ssl_handshake_timeout, ) ) bind = repr_socket_addr(sock.family, sock.getsockname()) await config.log.info(f"Running on https://{bind} (CTRL + C to quit)") for sock in sockets.insecure_sockets: if config.workers > 1 and platform.system() == "Windows": sock = _share_socket(sock) servers.append( await asyncio.start_server(_server_callback, backlog=config.backlog, sock=sock) ) bind = repr_socket_addr(sock.family, sock.getsockname()) await config.log.info(f"Running on http://{bind} (CTRL + C to quit)") for sock in sockets.quic_sockets: if config.workers > 1 and platform.system() == "Windows": sock = _share_socket(sock) _, protocol = await loop.create_datagram_endpoint( lambda: UDPServer(app, loop, config, context), sock=sock ) server_tasks.add(loop.create_task(protocol.run())) bind = repr_socket_addr(sock.family, sock.getsockname()) await config.log.info(f"Running on https://{bind} (QUIC) (CTRL + C to quit)") tasks = [] if platform.system() == "Windows": tasks.append(loop.create_task(_windows_signal_support())) tasks.append(loop.create_task(raise_shutdown(shutdown_trigger))) try: if len(tasks): gathered_tasks = asyncio.gather(*tasks) await gathered_tasks else: loop.run_forever() except (ShutdownError, KeyboardInterrupt): pass finally: await context.terminated.set() for server in servers: server.close() await server.wait_closed() # Retrieve the Gathered Tasks Cancelled Exception, to # prevent a warning that this hasn't been done. gathered_tasks.exception() try: gathered_server_tasks = asyncio.gather(*server_tasks) await asyncio.wait_for(gathered_server_tasks, config.graceful_timeout) except asyncio.TimeoutError: pass finally: # Retrieve the Gathered Tasks Cancelled Exception, to # prevent a warning that this hasn't been done. gathered_server_tasks.exception() await lifespan.wait_for_shutdown() lifespan_task.cancel() await lifespan_task def asyncio_worker( config: Config, sockets: Optional[Sockets] = None, shutdown_event: Optional[EventType] = None ) -> None: app = load_application(config.application_path, config.wsgi_max_body_size) shutdown_trigger = None if shutdown_event is not None: shutdown_trigger = partial(check_multiprocess_shutdown_event, shutdown_event, asyncio.sleep) if config.workers > 1 and platform.system() == "Windows": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # type: ignore _run( partial(worker_serve, app, config, sockets=sockets), debug=config.debug, shutdown_trigger=shutdown_trigger, ) def uvloop_worker( config: Config, sockets: Optional[Sockets] = None, shutdown_event: Optional[EventType] = None ) -> None: try: import uvloop except ImportError as error: raise Exception("uvloop is not installed") from error else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) app = load_application(config.application_path, config.wsgi_max_body_size) shutdown_trigger = None if shutdown_event is not None: shutdown_trigger = partial(check_multiprocess_shutdown_event, shutdown_event, asyncio.sleep) _run( partial(worker_serve, app, config, sockets=sockets), debug=config.debug, shutdown_trigger=shutdown_trigger, ) def _run( main: Callable, *, debug: bool = False, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, ) -> None: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.set_debug(debug) loop.set_exception_handler(_exception_handler) try: loop.run_until_complete(main(shutdown_trigger=shutdown_trigger)) except KeyboardInterrupt: pass finally: try: _cancel_all_tasks(loop) loop.run_until_complete(loop.shutdown_asyncgens()) try: loop.run_until_complete(loop.shutdown_default_executor()) except AttributeError: pass # shutdown_default_executor is new to Python 3.9 finally: asyncio.set_event_loop(None) loop.close() def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None: tasks = [task for task in asyncio.all_tasks(loop) if not task.done()] if not tasks: return for task in tasks: task.cancel() loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) for task in tasks: if not task.cancelled() and task.exception() is not None: loop.call_exception_handler( { "message": "unhandled exception during shutdown", "exception": task.exception(), "task": task, } ) def _exception_handler(loop: asyncio.AbstractEventLoop, context: dict) -> None: exception = context.get("exception") if isinstance(exception, ssl.SSLError): pass # Handshake failure else: loop.default_exception_handler(context) hypercorn-0.14.4/src/hypercorn/asyncio/statsd.py000066400000000000000000000014011445231714500217340ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Optional from ..config import Config from ..statsd import StatsdLogger as Base class _DummyProto(asyncio.DatagramProtocol): pass class StatsdLogger(Base): def __init__(self, config: Config) -> None: super().__init__(config) self.address = config.statsd_host.rsplit(":", 1) self.transport: Optional[asyncio.BaseTransport] = None async def _socket_send(self, message: bytes) -> None: if self.transport is None: self.transport, _ = await asyncio.get_event_loop().create_datagram_endpoint( _DummyProto, remote_addr=(self.address[0], int(self.address[1])) ) self.transport.sendto(message) # type: ignore hypercorn-0.14.4/src/hypercorn/asyncio/task_group.py000066400000000000000000000047641445231714500226270ustar00rootroot00000000000000from __future__ import annotations import asyncio import weakref from functools import partial from types import TracebackType from typing import Any, Awaitable, Callable, Optional from ..config import Config from ..typing import AppWrapper, ASGIReceiveCallable, ASGIReceiveEvent, ASGISendEvent, Scope async def _handle( app: AppWrapper, config: Config, scope: Scope, receive: ASGIReceiveCallable, send: Callable[[Optional[ASGISendEvent]], Awaitable[None]], sync_spawn: Callable, call_soon: Callable, ) -> None: try: await app(scope, receive, send, sync_spawn, call_soon) except asyncio.CancelledError: raise except Exception: await config.log.exception("Error in ASGI Framework") finally: await send(None) class TaskGroup: def __init__(self, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop self._tasks: weakref.WeakSet = weakref.WeakSet() self._exiting = False async def spawn_app( self, app: AppWrapper, config: Config, scope: Scope, send: Callable[[Optional[ASGISendEvent]], Awaitable[None]], ) -> Callable[[ASGIReceiveEvent], Awaitable[None]]: app_queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue(config.max_app_queue_size) def _call_soon(func: Callable, *args: Any) -> Any: future = asyncio.run_coroutine_threadsafe(func(*args), self._loop) return future.result() self.spawn( _handle, app, config, scope, app_queue.get, send, partial(self._loop.run_in_executor, None), _call_soon, ) return app_queue.put def spawn(self, func: Callable, *args: Any) -> None: if self._exiting: raise RuntimeError("Spawning whilst exiting") self._tasks.add(self._loop.create_task(func(*args))) async def __aenter__(self) -> "TaskGroup": return self async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: self._exiting = True if exc_type is not None: self._cancel_tasks() try: task = asyncio.gather(*self._tasks) await task finally: task.cancel() try: await task except asyncio.CancelledError: pass def _cancel_tasks(self) -> None: for task in self._tasks: task.cancel() hypercorn-0.14.4/src/hypercorn/asyncio/tcp_server.py000066400000000000000000000113351445231714500226150ustar00rootroot00000000000000from __future__ import annotations import asyncio from ssl import SSLError from typing import Any, Generator, Optional from .task_group import TaskGroup from .worker_context import WorkerContext from ..config import Config from ..events import Closed, Event, RawData, Updated from ..protocol import ProtocolWrapper from ..typing import AppWrapper from ..utils import parse_socket_addr MAX_RECV = 2**16 class TCPServer: def __init__( self, app: AppWrapper, loop: asyncio.AbstractEventLoop, config: Config, context: WorkerContext, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, ) -> None: self.app = app self.config = config self.context = context self.loop = loop self.protocol: ProtocolWrapper self.reader = reader self.writer = writer self.send_lock = asyncio.Lock() self.idle_lock = asyncio.Lock() self._idle_handle: Optional[asyncio.Task] = None def __await__(self) -> Generator[Any, None, None]: return self.run().__await__() async def run(self) -> None: socket = self.writer.get_extra_info("socket") try: client = parse_socket_addr(socket.family, socket.getpeername()) server = parse_socket_addr(socket.family, socket.getsockname()) ssl_object = self.writer.get_extra_info("ssl_object") if ssl_object is not None: ssl = True alpn_protocol = ssl_object.selected_alpn_protocol() else: ssl = False alpn_protocol = "http/1.1" async with TaskGroup(self.loop) as task_group: self.protocol = ProtocolWrapper( self.app, self.config, self.context, task_group, ssl, client, server, self.protocol_send, alpn_protocol, ) await self.protocol.initiate() await self._start_idle() await self._read_data() except OSError: pass finally: await self._close() async def protocol_send(self, event: Event) -> None: if isinstance(event, RawData): async with self.send_lock: try: self.writer.write(event.data) await self.writer.drain() except (ConnectionError, RuntimeError): await self.protocol.handle(Closed()) elif isinstance(event, Closed): await self._close() await self.protocol.handle(Closed()) elif isinstance(event, Updated): if event.idle: await self._start_idle() else: await self._stop_idle() async def _read_data(self) -> None: while not self.reader.at_eof(): try: data = await asyncio.wait_for(self.reader.read(MAX_RECV), self.config.read_timeout) except ( ConnectionError, OSError, asyncio.TimeoutError, TimeoutError, SSLError, ): break else: await self.protocol.handle(RawData(data)) await self.protocol.handle(Closed()) async def _close(self) -> None: try: self.writer.write_eof() except (NotImplementedError, OSError, RuntimeError): pass # Likely SSL connection try: self.writer.close() await self.writer.wait_closed() except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError, RuntimeError): pass # Already closed await self._stop_idle() async def _initiate_server_close(self) -> None: await self.protocol.handle(Closed()) self.writer.close() async def _start_idle(self) -> None: async with self.idle_lock: if self._idle_handle is None: self._idle_handle = self.loop.create_task(self._run_idle()) async def _stop_idle(self) -> None: async with self.idle_lock: if self._idle_handle is not None: self._idle_handle.cancel() try: await self._idle_handle except asyncio.CancelledError: pass self._idle_handle = None async def _run_idle(self) -> None: try: await asyncio.wait_for(self.context.terminated.wait(), self.config.keep_alive_timeout) except asyncio.TimeoutError: pass await asyncio.shield(self._initiate_server_close()) hypercorn-0.14.4/src/hypercorn/asyncio/udp_server.py000066400000000000000000000042031445231714500226130ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Optional, Tuple, TYPE_CHECKING from .task_group import TaskGroup from .worker_context import WorkerContext from ..config import Config from ..events import Event, RawData from ..typing import AppWrapper from ..utils import parse_socket_addr if TYPE_CHECKING: # h3/Quic is an optional part of Hypercorn from ..protocol.quic import QuicProtocol # noqa: F401 class UDPServer(asyncio.DatagramProtocol): def __init__( self, app: AppWrapper, loop: asyncio.AbstractEventLoop, config: Config, context: WorkerContext, ) -> None: self.app = app self.config = config self.context = context self.loop = loop self.protocol: "QuicProtocol" self.protocol_queue: asyncio.Queue = asyncio.Queue(10) self.transport: Optional[asyncio.DatagramTransport] = None def connection_made(self, transport: asyncio.DatagramTransport) -> None: # type: ignore self.transport = transport def datagram_received(self, data: bytes, address: Tuple[bytes, str]) -> None: # type: ignore try: self.protocol_queue.put_nowait(RawData(data=data, address=address)) # type: ignore except asyncio.QueueFull: pass # Just throw the data away, is UDP async def run(self) -> None: # h3/Quic is an optional part of Hypercorn from ..protocol.quic import QuicProtocol # noqa: F811 socket = self.transport.get_extra_info("socket") server = parse_socket_addr(socket.family, socket.getsockname()) async with TaskGroup(self.loop) as task_group: self.protocol = QuicProtocol( self.app, self.config, self.context, task_group, server, self.protocol_send ) while not self.context.terminated.is_set() or not self.protocol.idle: event = await self.protocol_queue.get() await self.protocol.handle(event) async def protocol_send(self, event: Event) -> None: if isinstance(event, RawData): self.transport.sendto(event.data, event.address) hypercorn-0.14.4/src/hypercorn/asyncio/worker_context.py000066400000000000000000000014531445231714500235160ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Type, Union from ..typing import Event class EventWrapper: def __init__(self) -> None: self._event = asyncio.Event() async def clear(self) -> None: self._event.clear() async def wait(self) -> None: await self._event.wait() async def set(self) -> None: self._event.set() def is_set(self) -> bool: return self._event.is_set() class WorkerContext: event_class: Type[Event] = EventWrapper def __init__(self) -> None: self.terminated = self.event_class() @staticmethod async def sleep(wait: Union[float, int]) -> None: return await asyncio.sleep(wait) @staticmethod def time() -> float: return asyncio.get_event_loop().time() hypercorn-0.14.4/src/hypercorn/config.py000066400000000000000000000317661445231714500202530ustar00rootroot00000000000000from __future__ import annotations import importlib import importlib.util import logging import os import socket import stat import sys import types import warnings from dataclasses import dataclass from ssl import ( create_default_context, OP_NO_COMPRESSION, Purpose, SSLContext, TLSVersion, VerifyFlags, VerifyMode, ) from time import time from typing import Any, AnyStr, Dict, List, Mapping, Optional, Tuple, Type, Union from wsgiref.handlers import format_date_time if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib from .logging import Logger BYTES = 1 OCTETS = 1 SECONDS = 1.0 FilePath = Union[AnyStr, os.PathLike] SocketKind = Union[int, socket.SocketKind] @dataclass class Sockets: secure_sockets: List[socket.socket] insecure_sockets: List[socket.socket] quic_sockets: List[socket.socket] class SocketTypeError(Exception): def __init__(self, expected: SocketKind, actual: SocketKind) -> None: super().__init__( f'Unexpected socket type, wanted "{socket.SocketKind(expected)}" got ' f'"{socket.SocketKind(actual)}"' ) class Config: _bind = ["127.0.0.1:8000"] _insecure_bind: List[str] = [] _quic_bind: List[str] = [] _quic_addresses: List[Tuple] = [] _log: Optional[Logger] = None _root_path: str = "" access_log_format = '%(h)s %(l)s %(l)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' accesslog: Union[logging.Logger, str, None] = None alpn_protocols = ["h2", "http/1.1"] alt_svc_headers: List[str] = [] application_path: str backlog = 100 ca_certs: Optional[str] = None certfile: Optional[str] = None ciphers: str = "ECDHE+AESGCM" debug = False dogstatsd_tags = "" errorlog: Union[logging.Logger, str, None] = "-" graceful_timeout: float = 3 * SECONDS read_timeout: Optional[int] = None group: Optional[int] = None h11_max_incomplete_size = 16 * 1024 * BYTES h11_pass_raw_headers = False h2_max_concurrent_streams = 100 h2_max_header_list_size = 2**16 h2_max_inbound_frame_size = 2**14 * OCTETS include_date_header = True include_server_header = True keep_alive_timeout = 5 * SECONDS keyfile: Optional[str] = None keyfile_password: Optional[str] = None logconfig: Optional[str] = None logconfig_dict: Optional[dict] = None logger_class = Logger loglevel: str = "INFO" max_app_queue_size: int = 10 pid_path: Optional[str] = None server_names: List[str] = [] shutdown_timeout = 60 * SECONDS ssl_handshake_timeout = 60 * SECONDS startup_timeout = 60 * SECONDS statsd_host: Optional[str] = None statsd_prefix = "" umask: Optional[int] = None use_reloader = False user: Optional[int] = None verify_flags: Optional[VerifyFlags] = None verify_mode: Optional[VerifyMode] = None websocket_max_message_size = 16 * 1024 * 1024 * BYTES websocket_ping_interval: Optional[float] = None worker_class = "asyncio" workers = 1 wsgi_max_body_size = 16 * 1024 * 1024 * BYTES def set_cert_reqs(self, value: int) -> None: warnings.warn("Please use verify_mode instead", Warning) self.verify_mode = VerifyMode(value) cert_reqs = property(None, set_cert_reqs) @property def log(self) -> Logger: if self._log is None: self._log = self.logger_class(self) return self._log @property def bind(self) -> List[str]: return self._bind @bind.setter def bind(self, value: Union[List[str], str]) -> None: if isinstance(value, str): self._bind = [value] else: self._bind = value @property def insecure_bind(self) -> List[str]: return self._insecure_bind @insecure_bind.setter def insecure_bind(self, value: Union[List[str], str]) -> None: if isinstance(value, str): self._insecure_bind = [value] else: self._insecure_bind = value @property def quic_bind(self) -> List[str]: return self._quic_bind @quic_bind.setter def quic_bind(self, value: Union[List[str], str]) -> None: if isinstance(value, str): self._quic_bind = [value] else: self._quic_bind = value @property def root_path(self) -> str: return self._root_path @root_path.setter def root_path(self, value: str) -> None: self._root_path = value.rstrip("/") def create_ssl_context(self) -> Optional[SSLContext]: if not self.ssl_enabled: return None context = create_default_context(Purpose.CLIENT_AUTH) context.set_ciphers(self.ciphers) context.minimum_version = TLSVersion.TLSv1_2 # RFC 7540 Section 9.2: MUST be TLS >=1.2 context.options = OP_NO_COMPRESSION # RFC 7540 Section 9.2.1: MUST disable compression context.set_alpn_protocols(self.alpn_protocols) if self.certfile is not None and self.keyfile is not None: context.load_cert_chain( certfile=self.certfile, keyfile=self.keyfile, password=self.keyfile_password, ) if self.ca_certs is not None: context.load_verify_locations(self.ca_certs) if self.verify_mode is not None: context.verify_mode = self.verify_mode if self.verify_flags is not None: context.verify_flags = self.verify_flags return context @property def ssl_enabled(self) -> bool: return self.certfile is not None and self.keyfile is not None def create_sockets(self) -> Sockets: if self.ssl_enabled: secure_sockets = self._create_sockets(self.bind) insecure_sockets = self._create_sockets(self.insecure_bind) quic_sockets = self._create_sockets(self.quic_bind, socket.SOCK_DGRAM) self._set_quic_addresses(quic_sockets) else: secure_sockets = [] insecure_sockets = self._create_sockets(self.bind) quic_sockets = [] return Sockets(secure_sockets, insecure_sockets, quic_sockets) def _set_quic_addresses(self, sockets: List[socket.socket]) -> None: self._quic_addresses = [] for sock in sockets: name = sock.getsockname() if type(name) is not str and len(name) >= 2: self._quic_addresses.append(name) else: warnings.warn( f'Cannot create a alt-svc header for the QUIC socket with address "{name}"', Warning, ) def _create_sockets( self, binds: List[str], type_: int = socket.SOCK_STREAM ) -> List[socket.socket]: sockets: List[socket.socket] = [] for bind in binds: binding: Any = None if bind.startswith("unix:"): sock = socket.socket(socket.AF_UNIX, type_) binding = bind[5:] try: if stat.S_ISSOCK(os.stat(binding).st_mode): os.remove(binding) except FileNotFoundError: pass elif bind.startswith("fd://"): sock = socket.socket(fileno=int(bind[5:])) actual_type = sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) if actual_type != type_: raise SocketTypeError(type_, actual_type) else: bind = bind.replace("[", "").replace("]", "") try: value = bind.rsplit(":", 1) host, port = value[0], int(value[1]) except (ValueError, IndexError): host, port = bind, 8000 sock = socket.socket(socket.AF_INET6 if ":" in host else socket.AF_INET, type_) if self.workers > 1: try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except AttributeError: pass binding = (host, port) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if bind.startswith("unix:"): if self.umask is not None: current_umask = os.umask(self.umask) sock.bind(binding) if self.user is not None and self.group is not None: os.chown(binding, self.user, self.group) if self.umask is not None: os.umask(current_umask) elif bind.startswith("fd://"): pass else: sock.bind(binding) sock.setblocking(False) try: sock.set_inheritable(True) except AttributeError: pass sockets.append(sock) return sockets def response_headers(self, protocol: str) -> List[Tuple[bytes, bytes]]: headers = [] if self.include_date_header: headers.append((b"date", format_date_time(time()).encode("ascii"))) if self.include_server_header: headers.append((b"server", f"hypercorn-{protocol}".encode("ascii"))) for alt_svc_header in self.alt_svc_headers: headers.append((b"alt-svc", alt_svc_header.encode())) if len(self.alt_svc_headers) == 0 and self._quic_addresses: from aioquic.h3.connection import H3_ALPN for version in H3_ALPN: for addr in self._quic_addresses: port = addr[1] headers.append((b"alt-svc", b'%s=":%d"; ma=3600' % (version.encode(), port))) return headers def set_statsd_logger_class(self, statsd_logger: Type[Logger]) -> None: if self.logger_class == Logger and self.statsd_host is not None: self.logger_class = statsd_logger @classmethod def from_mapping( cls: Type["Config"], mapping: Optional[Mapping[str, Any]] = None, **kwargs: Any ) -> "Config": """Create a configuration from a mapping. This allows either a mapping to be directly passed or as keyword arguments, for example, .. code-block:: python config = {'keep_alive_timeout': 10} Config.from_mapping(config) Config.from_mapping(keep_alive_timeout=10) Arguments: mapping: Optionally a mapping object. kwargs: Optionally a collection of keyword arguments to form a mapping. """ mappings: Dict[str, Any] = {} if mapping is not None: mappings.update(mapping) mappings.update(kwargs) config = cls() for key, value in mappings.items(): try: setattr(config, key, value) except AttributeError: pass return config @classmethod def from_pyfile(cls: Type["Config"], filename: FilePath) -> "Config": """Create a configuration from a Python file. .. code-block:: python Config.from_pyfile('hypercorn_config.py') Arguments: filename: The filename which gives the path to the file. """ file_path = os.fspath(filename) spec = importlib.util.spec_from_file_location("module.name", file_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return cls.from_object(module) @classmethod def from_toml(cls: Type["Config"], filename: FilePath) -> "Config": """Load the configuration values from a TOML formatted file. This allows configuration to be loaded as so .. code-block:: python Config.from_toml('config.toml') Arguments: filename: The filename which gives the path to the file. """ file_path = os.fspath(filename) with open(file_path, "rb") as file_: data = tomllib.load(file_) return cls.from_mapping(data) @classmethod def from_object(cls: Type["Config"], instance: Union[object, str]) -> "Config": """Create a configuration from a Python object. This can be used to reference modules or objects within modules for example, .. code-block:: python Config.from_object('module') Config.from_object('module.instance') from module import instance Config.from_object(instance) are valid. Arguments: instance: Either a str referencing a python object or the object itself. """ if isinstance(instance, str): try: instance = importlib.import_module(instance) except ImportError: path, config = instance.rsplit(".", 1) module = importlib.import_module(path) instance = getattr(module, config) mapping = { key: getattr(instance, key) for key in dir(instance) if not isinstance(getattr(instance, key), types.ModuleType) and not key.startswith("__") } return cls.from_mapping(mapping) hypercorn-0.14.4/src/hypercorn/events.py000066400000000000000000000005771445231714500203060ustar00rootroot00000000000000from __future__ import annotations from abc import ABC from dataclasses import dataclass from typing import Optional, Tuple class Event(ABC): pass @dataclass(frozen=True) class RawData(Event): data: bytes address: Optional[Tuple[str, int]] = None @dataclass(frozen=True) class Closed(Event): pass @dataclass(frozen=True) class Updated(Event): idle: bool hypercorn-0.14.4/src/hypercorn/logging.py000066400000000000000000000160371445231714500204260ustar00rootroot00000000000000from __future__ import annotations import json import logging import os import sys import time from http import HTTPStatus from logging.config import dictConfig, fileConfig from typing import Any, IO, Mapping, Optional, TYPE_CHECKING, Union if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib if TYPE_CHECKING: from .config import Config from .typing import ResponseSummary, WWWScope def _create_logger( name: str, target: Union[logging.Logger, str, None], level: Optional[str], sys_default: IO, *, propagate: bool = True, ) -> Optional[logging.Logger]: if isinstance(target, logging.Logger): return target if target: logger = logging.getLogger(name) logger.handlers = [ logging.StreamHandler(sys_default) if target == "-" else logging.FileHandler(target) # type: ignore # noqa: E501 ] logger.propagate = propagate formatter = logging.Formatter( "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", "[%Y-%m-%d %H:%M:%S %z]", ) logger.handlers[0].setFormatter(formatter) if level is not None: logger.setLevel(logging.getLevelName(level.upper())) return logger else: return None class Logger: def __init__(self, config: "Config") -> None: self.access_log_format = config.access_log_format self.access_logger = _create_logger( "hypercorn.access", config.accesslog, config.loglevel, sys.stdout, propagate=False, ) self.error_logger = _create_logger( "hypercorn.error", config.errorlog, config.loglevel, sys.stderr ) if config.logconfig is not None: if config.logconfig.startswith("json:"): with open(config.logconfig[5:]) as file_: dictConfig(json.load(file_)) elif config.logconfig.startswith("toml:"): with open(config.logconfig[5:], "rb") as file_: dictConfig(tomllib.load(file_)) else: log_config = { "__file__": config.logconfig, "here": os.path.dirname(config.logconfig), } fileConfig(config.logconfig, defaults=log_config, disable_existing_loggers=False) else: if config.logconfig_dict is not None: dictConfig(config.logconfig_dict) async def access( self, request: "WWWScope", response: "ResponseSummary", request_time: float ) -> None: if self.access_logger is not None: self.access_logger.info( self.access_log_format, self.atoms(request, response, request_time) ) async def critical(self, message: str, *args: Any, **kwargs: Any) -> None: if self.error_logger is not None: self.error_logger.critical(message, *args, **kwargs) async def error(self, message: str, *args: Any, **kwargs: Any) -> None: if self.error_logger is not None: self.error_logger.error(message, *args, **kwargs) async def warning(self, message: str, *args: Any, **kwargs: Any) -> None: if self.error_logger is not None: self.error_logger.warning(message, *args, **kwargs) async def info(self, message: str, *args: Any, **kwargs: Any) -> None: if self.error_logger is not None: self.error_logger.info(message, *args, **kwargs) async def debug(self, message: str, *args: Any, **kwargs: Any) -> None: if self.error_logger is not None: self.error_logger.debug(message, *args, **kwargs) async def exception(self, message: str, *args: Any, **kwargs: Any) -> None: if self.error_logger is not None: self.error_logger.exception(message, *args, **kwargs) async def log(self, level: int, message: str, *args: Any, **kwargs: Any) -> None: if self.error_logger is not None: self.error_logger.log(level, message, *args, **kwargs) def atoms( self, request: "WWWScope", response: "ResponseSummary", request_time: float ) -> Mapping[str, str]: """Create and return an access log atoms dictionary. This can be overidden and customised if desired. It should return a mapping between an access log format key and a value. """ return AccessLogAtoms(request, response, request_time) def __getattr__(self, name: str) -> Any: return getattr(self.error_logger, name) class AccessLogAtoms(dict): def __init__( self, request: "WWWScope", response: "ResponseSummary", request_time: float ) -> None: for name, value in request["headers"]: self[f"{{{name.decode('latin1').lower()}}}i"] = value.decode("latin1") for name, value in response.get("headers", []): self[f"{{{name.decode('latin1').lower()}}}o"] = value.decode("latin1") for name, value in os.environ.items(): self[f"{{{name.lower()}}}e"] = value protocol = request.get("http_version", "ws") client = request.get("client") if client is None: remote_addr = None elif len(client) == 2: remote_addr = f"{client[0]}:{client[1]}" elif len(client) == 1: remote_addr = client[0] else: # make sure not to throw UnboundLocalError remote_addr = f"" if request["type"] == "http": method = request["method"] else: method = "GET" query_string = request["query_string"].decode() path_with_qs = request["path"] + ("?" + query_string if query_string else "") status_code = response["status"] try: status_phrase = HTTPStatus(status_code).phrase except ValueError: status_phrase = f"" self.update( { "h": remote_addr, "l": "-", "t": time.strftime("[%d/%b/%Y:%H:%M:%S %z]"), "r": f"{method} {request['path']} {protocol}", "R": f"{method} {path_with_qs} {protocol}", "s": response["status"], "st": status_phrase, "S": request["scheme"], "m": method, "U": request["path"], "Uq": path_with_qs, "q": query_string, "H": protocol, "b": self["{Content-Length}o"], "B": self["{Content-Length}o"], "f": self["{Referer}i"], "a": self["{User-Agent}i"], "T": int(request_time), "D": int(request_time * 1_000_000), "L": f"{request_time:.6f}", "p": f"<{os.getpid()}>", } ) def __getitem__(self, key: str) -> str: try: if key.startswith("{"): return super().__getitem__(key.lower()) else: return super().__getitem__(key) except KeyError: return "-" hypercorn-0.14.4/src/hypercorn/middleware/000077500000000000000000000000001445231714500205345ustar00rootroot00000000000000hypercorn-0.14.4/src/hypercorn/middleware/__init__.py000066400000000000000000000005151445231714500226460ustar00rootroot00000000000000from __future__ import annotations from .dispatcher import DispatcherMiddleware from .http_to_https import HTTPToHTTPSRedirectMiddleware from .wsgi import AsyncioWSGIMiddleware, TrioWSGIMiddleware __all__ = ( "AsyncioWSGIMiddleware", "DispatcherMiddleware", "HTTPToHTTPSRedirectMiddleware", "TrioWSGIMiddleware", ) hypercorn-0.14.4/src/hypercorn/middleware/dispatcher.py000066400000000000000000000103221445231714500232320ustar00rootroot00000000000000from __future__ import annotations import asyncio from functools import partial from typing import Callable, Dict from ..asyncio.task_group import TaskGroup from ..typing import ASGIFramework, Scope MAX_QUEUE_SIZE = 10 class _DispatcherMiddleware: def __init__(self, mounts: Dict[str, ASGIFramework]) -> None: self.mounts = mounts async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> None: if scope["type"] == "lifespan": await self._handle_lifespan(scope, receive, send) else: for path, app in self.mounts.items(): if scope["path"].startswith(path): scope["path"] = scope["path"][len(path) :] or "/" return await app(scope, receive, send) await send( { "type": "http.response.start", "status": 404, "headers": [(b"content-length", b"0")], } ) await send({"type": "http.response.body"}) async def _handle_lifespan(self, scope: Scope, receive: Callable, send: Callable) -> None: pass class AsyncioDispatcherMiddleware(_DispatcherMiddleware): async def _handle_lifespan(self, scope: Scope, receive: Callable, send: Callable) -> None: self.app_queues: Dict[str, asyncio.Queue] = { path: asyncio.Queue(MAX_QUEUE_SIZE) for path in self.mounts } self.startup_complete = {path: False for path in self.mounts} self.shutdown_complete = {path: False for path in self.mounts} async with TaskGroup(asyncio.get_event_loop()) as task_group: for path, app in self.mounts.items(): task_group.spawn( app, scope, self.app_queues[path].get, partial(self.send, path, send), ) while True: message = await receive() for queue in self.app_queues.values(): await queue.put(message) if message["type"] == "lifespan.shutdown": break async def send(self, path: str, send: Callable, message: dict) -> None: if message["type"] == "lifespan.startup.complete": self.startup_complete[path] = True if all(self.startup_complete.values()): await send({"type": "lifespan.startup.complete"}) elif message["type"] == "lifespan.shutdown.complete": self.shutdown_complete[path] = True if all(self.shutdown_complete.values()): await send({"type": "lifespan.shutdown.complete"}) class TrioDispatcherMiddleware(_DispatcherMiddleware): async def _handle_lifespan(self, scope: Scope, receive: Callable, send: Callable) -> None: import trio self.app_queues = {path: trio.open_memory_channel(MAX_QUEUE_SIZE) for path in self.mounts} self.startup_complete = {path: False for path in self.mounts} self.shutdown_complete = {path: False for path in self.mounts} async with trio.open_nursery() as nursery: for path, app in self.mounts.items(): nursery.start_soon( app, scope, self.app_queues[path][1].receive, partial(self.send, path, send), ) while True: message = await receive() for channels in self.app_queues.values(): await channels[0].send(message) if message["type"] == "lifespan.shutdown": break async def send(self, path: str, send: Callable, message: dict) -> None: if message["type"] == "lifespan.startup.complete": self.startup_complete[path] = True if all(self.startup_complete.values()): await send({"type": "lifespan.startup.complete"}) elif message["type"] == "lifespan.shutdown.complete": self.shutdown_complete[path] = True if all(self.shutdown_complete.values()): await send({"type": "lifespan.shutdown.complete"}) DispatcherMiddleware = AsyncioDispatcherMiddleware # Remove with version 0.11 hypercorn-0.14.4/src/hypercorn/middleware/http_to_https.py000066400000000000000000000050731445231714500240160ustar00rootroot00000000000000from __future__ import annotations from typing import Callable, Optional from urllib.parse import urlunsplit from ..typing import ASGIFramework, HTTPScope, Scope, WebsocketScope, WWWScope class HTTPToHTTPSRedirectMiddleware: def __init__(self, app: ASGIFramework, host: Optional[str]) -> None: self.app = app self.host = host async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> None: if scope["type"] == "http" and scope["scheme"] == "http": await self._send_http_redirect(scope, send) elif scope["type"] == "websocket" and scope["scheme"] == "ws": # If the server supports the WebSocket Denial Response # extension we can send a redirection response, if not we # can only deny the WebSocket connection. if "websocket.http.response" in scope.get("extensions", {}): await self._send_websocket_redirect(scope, send) else: await send({"type": "websocket.close"}) else: return await self.app(scope, receive, send) async def _send_http_redirect(self, scope: HTTPScope, send: Callable) -> None: new_url = self._new_url("https", scope) await send( { "type": "http.response.start", "status": 307, "headers": [(b"location", new_url.encode())], } ) await send({"type": "http.response.body"}) async def _send_websocket_redirect(self, scope: WebsocketScope, send: Callable) -> None: # If the HTTP version is 2 we should redirect with a https # scheme not wss. scheme = "wss" if scope.get("http_version", "1.1") == "2": scheme = "https" new_url = self._new_url(scheme, scope) await send( { "type": "websocket.http.response.start", "status": 307, "headers": [(b"location", new_url.encode())], } ) await send({"type": "websocket.http.response.body"}) def _new_url(self, scheme: str, scope: WWWScope) -> str: host = self.host if host is None: for key, value in scope["headers"]: if key == b"host": host = value.decode("latin-1") break if host is None: raise ValueError("Host to redirect to cannot be determined") path = scope.get("root_path", "") + scope["raw_path"].decode() return urlunsplit((scheme, host, path, scope["query_string"].decode(), "")) hypercorn-0.14.4/src/hypercorn/middleware/wsgi.py000066400000000000000000000027211445231714500220610ustar00rootroot00000000000000from __future__ import annotations import asyncio from functools import partial from typing import Any, Callable, Iterable from ..app_wrappers import WSGIWrapper from ..typing import ASGIReceiveCallable, ASGISendCallable, Scope, WSGIFramework MAX_BODY_SIZE = 2**16 WSGICallable = Callable[[dict, Callable], Iterable[bytes]] class InvalidPathError(Exception): pass class _WSGIMiddleware: def __init__(self, wsgi_app: WSGIFramework, max_body_size: int = MAX_BODY_SIZE) -> None: self.wsgi_app = WSGIWrapper(wsgi_app, max_body_size) self.max_body_size = max_body_size async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: pass class AsyncioWSGIMiddleware(_WSGIMiddleware): async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: loop = asyncio.get_event_loop() def _call_soon(func: Callable, *args: Any) -> Any: future = asyncio.run_coroutine_threadsafe(func(*args), loop) return future.result() await self.wsgi_app(scope, receive, send, partial(loop.run_in_executor, None), _call_soon) class TrioWSGIMiddleware(_WSGIMiddleware): async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: import trio await self.wsgi_app(scope, receive, send, trio.to_thread.run_sync, trio.from_thread.run) hypercorn-0.14.4/src/hypercorn/protocol/000077500000000000000000000000001445231714500202605ustar00rootroot00000000000000hypercorn-0.14.4/src/hypercorn/protocol/__init__.py000077500000000000000000000053701445231714500224010ustar00rootroot00000000000000from __future__ import annotations from typing import Awaitable, Callable, Optional, Tuple, Union from .h2 import H2Protocol from .h11 import H2CProtocolRequiredError, H2ProtocolAssumedError, H11Protocol from ..config import Config from ..events import Event, RawData from ..typing import AppWrapper, TaskGroup, WorkerContext class ProtocolWrapper: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, task_group: TaskGroup, ssl: bool, client: Optional[Tuple[str, int]], server: Optional[Tuple[str, int]], send: Callable[[Event], Awaitable[None]], alpn_protocol: Optional[str] = None, ) -> None: self.app = app self.config = config self.context = context self.task_group = task_group self.ssl = ssl self.client = client self.server = server self.send = send self.protocol: Union[H11Protocol, H2Protocol] if alpn_protocol == "h2": self.protocol = H2Protocol( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.send, ) else: self.protocol = H11Protocol( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.send, ) async def initiate(self) -> None: return await self.protocol.initiate() async def handle(self, event: Event) -> None: try: return await self.protocol.handle(event) except H2ProtocolAssumedError as error: self.protocol = H2Protocol( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.send, ) await self.protocol.initiate() if error.data != b"": return await self.protocol.handle(RawData(data=error.data)) except H2CProtocolRequiredError as error: self.protocol = H2Protocol( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.send, ) await self.protocol.initiate(error.headers, error.settings) if error.data != b"": return await self.protocol.handle(RawData(data=error.data)) hypercorn-0.14.4/src/hypercorn/protocol/events.py000066400000000000000000000017651445231714500221470ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import List, Tuple @dataclass(frozen=True) class Event: stream_id: int @dataclass(frozen=True) class Request(Event): headers: List[Tuple[bytes, bytes]] http_version: str method: str raw_path: bytes @dataclass(frozen=True) class Body(Event): data: bytes @dataclass(frozen=True) class EndBody(Event): pass @dataclass(frozen=True) class Data(Event): data: bytes @dataclass(frozen=True) class EndData(Event): pass @dataclass(frozen=True) class Response(Event): headers: List[Tuple[bytes, bytes]] status_code: int @dataclass(frozen=True) class InformationalResponse(Event): headers: List[Tuple[bytes, bytes]] status_code: int def __post_init__(self) -> None: if self.status_code >= 200 or self.status_code < 100: raise ValueError(f"Status code must be 1XX not {self.status_code}") @dataclass(frozen=True) class StreamClosed(Event): pass hypercorn-0.14.4/src/hypercorn/protocol/h11.py000077500000000000000000000262471445231714500212410ustar00rootroot00000000000000from __future__ import annotations from itertools import chain from typing import Awaitable, Callable, cast, Optional, Tuple, Type, Union import h11 from .events import ( Body, Data, EndBody, EndData, Event as StreamEvent, InformationalResponse, Request, Response, StreamClosed, ) from .http_stream import HTTPStream from .ws_stream import WSStream from ..config import Config from ..events import Closed, Event, RawData, Updated from ..typing import AppWrapper, H11SendableEvent, TaskGroup, WorkerContext STREAM_ID = 1 class H2CProtocolRequiredError(Exception): def __init__(self, data: bytes, request: h11.Request) -> None: settings = "" headers = [(b":method", request.method), (b":path", request.target)] for name, value in request.headers: if name.lower() == b"http2-settings": settings = value.decode() elif name.lower() == b"host": headers.append((b":authority", value)) headers.append((name, value)) self.data = data self.headers = headers self.settings = settings class H2ProtocolAssumedError(Exception): def __init__(self, data: bytes) -> None: self.data = data class H11WSConnection: # This class matches the h11 interface, and either passes data # through without altering it (for Data, EndData) or sends h11 # events (Response, Body, EndBody). our_state = None # Prevents recycling the connection they_are_waiting_for_100_continue = False their_state = None trailing_data = (b"", False) def __init__(self, h11_connection: h11.Connection) -> None: self.buffer = bytearray(h11_connection.trailing_data[0]) self.h11_connection = h11_connection def receive_data(self, data: bytes) -> None: self.buffer.extend(data) def next_event(self) -> Union[Data, Type[h11.NEED_DATA]]: if self.buffer: event = Data(stream_id=STREAM_ID, data=bytes(self.buffer)) self.buffer = bytearray() return event else: return h11.NEED_DATA def send(self, event: H11SendableEvent) -> bytes: return self.h11_connection.send(event) def start_next_cycle(self) -> None: pass class H11Protocol: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, task_group: TaskGroup, ssl: bool, client: Optional[Tuple[str, int]], server: Optional[Tuple[str, int]], send: Callable[[Event], Awaitable[None]], ) -> None: self.app = app self.can_read = context.event_class() self.client = client self.config = config self.connection: Union[h11.Connection, H11WSConnection] = h11.Connection( h11.SERVER, max_incomplete_event_size=self.config.h11_max_incomplete_size ) self.context = context self.send = send self.server = server self.ssl = ssl self.stream: Optional[Union[HTTPStream, WSStream]] = None self.task_group = task_group async def initiate(self) -> None: pass async def handle(self, event: Event) -> None: if isinstance(event, RawData): self.connection.receive_data(event.data) await self._handle_events() elif isinstance(event, Closed): if self.stream is not None: await self._close_stream() async def stream_send(self, event: StreamEvent) -> None: if isinstance(event, Response): if event.status_code >= 200: await self._send_h11_event( h11.Response( headers=list(chain(event.headers, self.config.response_headers("h11"))), status_code=event.status_code, ) ) else: await self._send_h11_event( h11.InformationalResponse( headers=list(chain(event.headers, self.config.response_headers("h11"))), status_code=event.status_code, ) ) elif isinstance(event, InformationalResponse): pass # Ignore for HTTP/1 elif isinstance(event, Body): await self._send_h11_event(h11.Data(data=event.data)) elif isinstance(event, EndBody): await self._send_h11_event(h11.EndOfMessage()) elif isinstance(event, Data): await self.send(RawData(data=event.data)) elif isinstance(event, EndData): pass elif isinstance(event, StreamClosed): await self._maybe_recycle() async def _handle_events(self) -> None: while True: if self.connection.they_are_waiting_for_100_continue: await self._send_h11_event( h11.InformationalResponse( status_code=100, headers=self.config.response_headers("h11") ) ) try: event = self.connection.next_event() except h11.RemoteProtocolError: if self.connection.our_state in {h11.IDLE, h11.SEND_RESPONSE}: await self._send_error_response(400) await self.send(Closed()) break else: if isinstance(event, h11.Request): await self.send(Updated(idle=False)) await self._check_protocol(event) await self._create_stream(event) elif event is h11.PAUSED: await self.can_read.clear() await self.can_read.wait() elif isinstance(event, h11.ConnectionClosed) or event is h11.NEED_DATA: break elif self.stream is None: break elif isinstance(event, h11.Data): await self.stream.handle(Body(stream_id=STREAM_ID, data=event.data)) elif isinstance(event, h11.EndOfMessage): await self.stream.handle(EndBody(stream_id=STREAM_ID)) elif isinstance(event, Data): # WebSocket pass through await self.stream.handle(event) async def _create_stream(self, request: h11.Request) -> None: upgrade_value = "" connection_value = "" for name, value in request.headers: sanitised_name = name.decode("latin1").strip().lower() if sanitised_name == "upgrade": upgrade_value = value.decode("latin1").strip() elif sanitised_name == "connection": connection_value = value.decode("latin1").strip() connection_tokens = connection_value.lower().split(",") if ( any(token.strip() == "upgrade" for token in connection_tokens) and upgrade_value.lower() == "websocket" and request.method.decode("ascii").upper() == "GET" ): self.stream = WSStream( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.stream_send, STREAM_ID, ) self.connection = H11WSConnection(cast(h11.Connection, self.connection)) else: self.stream = HTTPStream( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.stream_send, STREAM_ID, ) if self.config.h11_pass_raw_headers: headers = request.headers.raw_items() else: headers = list(request.headers) await self.stream.handle( Request( stream_id=STREAM_ID, headers=headers, http_version=request.http_version.decode(), method=request.method.decode("ascii").upper(), raw_path=request.target, ) ) async def _send_h11_event(self, event: H11SendableEvent) -> None: try: data = self.connection.send(event) except h11.LocalProtocolError: if self.connection.their_state != h11.ERROR: raise else: await self.send(RawData(data=data)) async def _send_error_response(self, status_code: int) -> None: await self._send_h11_event( h11.Response( status_code=status_code, headers=list( chain( [(b"content-length", b"0"), (b"connection", b"close")], self.config.response_headers("h11"), ) ), ) ) await self._send_h11_event(h11.EndOfMessage()) async def _maybe_recycle(self) -> None: await self._close_stream() if ( not self.context.terminated.is_set() and self.connection.our_state is h11.DONE and self.connection.their_state is h11.DONE ): try: self.connection.start_next_cycle() except h11.LocalProtocolError: await self.send(Closed()) else: self.response = None self.scope = None await self.can_read.set() await self.send(Updated(idle=True)) else: await self.can_read.set() await self.send(Closed()) async def _close_stream(self) -> None: if self.stream is not None: await self.stream.handle(StreamClosed(stream_id=STREAM_ID)) self.stream = None async def _check_protocol(self, event: h11.Request) -> None: upgrade_value = "" has_body = False for name, value in event.headers: sanitised_name = name.decode("latin1").strip().lower() if sanitised_name == "upgrade": upgrade_value = value.decode("latin1").strip() elif sanitised_name in {"content-length", "transfer-encoding"}: has_body = True # h2c Upgrade requests with a body are a pain as the body must # be fully recieved in HTTP/1.1 before the upgrade response # and HTTP/2 takes over, so Hypercorn ignores the upgrade and # responds in HTTP/1.1. Use a preflight OPTIONS request to # initiate the upgrade if really required (or just use h2). if upgrade_value.lower() == "h2c" and not has_body: await self._send_h11_event( h11.InformationalResponse( status_code=101, headers=self.config.response_headers("h11") + [(b"connection", b"upgrade"), (b"upgrade", b"h2c")], ) ) raise H2CProtocolRequiredError(self.connection.trailing_data[0], event) elif event.method == b"PRI" and event.target == b"*" and event.http_version == b"2.0": raise H2ProtocolAssumedError( b"PRI * HTTP/2.0\r\n\r\n" + self.connection.trailing_data[0] ) hypercorn-0.14.4/src/hypercorn/protocol/h2.py000077500000000000000000000345751445231714500211640ustar00rootroot00000000000000from __future__ import annotations from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union import h2 import h2.connection import h2.events import h2.exceptions import priority from .events import ( Body, Data, EndBody, EndData, Event as StreamEvent, InformationalResponse, Request, Response, StreamClosed, ) from .http_stream import HTTPStream from .ws_stream import WSStream from ..config import Config from ..events import Closed, Event, RawData, Updated from ..typing import AppWrapper, Event as IOEvent, TaskGroup, WorkerContext from ..utils import filter_pseudo_headers BUFFER_HIGH_WATER = 2 * 2**14 # Twice the default max frame size (two frames worth) BUFFER_LOW_WATER = BUFFER_HIGH_WATER / 2 class BufferCompleteError(Exception): pass class StreamBuffer: def __init__(self, event_class: Type[IOEvent]) -> None: self.buffer = bytearray() self._complete = False self._is_empty = event_class() self._paused = event_class() async def drain(self) -> None: await self._is_empty.wait() def set_complete(self) -> None: self._complete = True async def close(self) -> None: self._complete = True self.buffer = bytearray() await self._is_empty.set() await self._paused.set() @property def complete(self) -> bool: return self._complete and len(self.buffer) == 0 async def push(self, data: bytes) -> None: if self._complete: raise BufferCompleteError() self.buffer.extend(data) await self._is_empty.clear() if len(self.buffer) >= BUFFER_HIGH_WATER: await self._paused.wait() await self._paused.clear() async def pop(self, max_length: int) -> bytes: length = min(len(self.buffer), max_length) data = bytes(self.buffer[:length]) del self.buffer[:length] if len(data) < BUFFER_LOW_WATER: await self._paused.set() if len(self.buffer) == 0: await self._is_empty.set() return data class H2Protocol: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, task_group: TaskGroup, ssl: bool, client: Optional[Tuple[str, int]], server: Optional[Tuple[str, int]], send: Callable[[Event], Awaitable[None]], ) -> None: self.app = app self.client = client self.closed = False self.config = config self.context = context self.task_group = task_group self.connection = h2.connection.H2Connection( config=h2.config.H2Configuration(client_side=False, header_encoding=None) ) self.connection.DEFAULT_MAX_INBOUND_FRAME_SIZE = config.h2_max_inbound_frame_size self.connection.local_settings = h2.settings.Settings( client=False, initial_values={ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: config.h2_max_concurrent_streams, h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: config.h2_max_header_list_size, h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL: 1, }, ) self.send = send self.server = server self.ssl = ssl self.streams: Dict[int, Union[HTTPStream, WSStream]] = {} # The below are used by the sending task self.has_data = self.context.event_class() self.priority = priority.PriorityTree() self.stream_buffers: Dict[int, StreamBuffer] = {} @property def idle(self) -> bool: return len(self.streams) == 0 or all(stream.idle for stream in self.streams.values()) async def initiate( self, headers: Optional[List[Tuple[bytes, bytes]]] = None, settings: Optional[str] = None ) -> None: if settings is not None: self.connection.initiate_upgrade_connection(settings) else: self.connection.initiate_connection() await self._flush() if headers is not None: event = h2.events.RequestReceived() event.stream_id = 1 event.headers = headers await self._create_stream(event) await self.streams[event.stream_id].handle(EndBody(stream_id=event.stream_id)) self.task_group.spawn(self.send_task) async def send_task(self) -> None: # This should be run in a seperate task to the rest of this # class. This allows it seperately choose when to send, # crucially in what order. while not self.closed: try: stream_id = next(self.priority) except priority.DeadlockError: await self.has_data.wait() await self.has_data.clear() else: await self._send_data(stream_id) async def _send_data(self, stream_id: int) -> None: try: chunk_size = min( self.connection.local_flow_control_window(stream_id), self.connection.max_outbound_frame_size, ) chunk_size = max(0, chunk_size) data = await self.stream_buffers[stream_id].pop(chunk_size) if data: self.connection.send_data(stream_id, data) await self._flush() else: self.priority.block(stream_id) if self.stream_buffers[stream_id].complete: self.connection.end_stream(stream_id) await self._flush() del self.stream_buffers[stream_id] self.priority.remove_stream(stream_id) except (h2.exceptions.StreamClosedError, KeyError, h2.exceptions.ProtocolError): # Stream or connection has closed whilst waiting to send # data, not a problem - just force close it. await self.stream_buffers[stream_id].close() del self.stream_buffers[stream_id] self.priority.remove_stream(stream_id) async def handle(self, event: Event) -> None: if isinstance(event, RawData): try: events = self.connection.receive_data(event.data) except h2.exceptions.ProtocolError: await self._flush() await self.send(Closed()) else: await self._handle_events(events) elif isinstance(event, Closed): self.closed = True stream_ids = list(self.streams.keys()) for stream_id in stream_ids: await self._close_stream(stream_id) await self.has_data.set() async def stream_send(self, event: StreamEvent) -> None: try: if isinstance(event, (InformationalResponse, Response)): self.connection.send_headers( event.stream_id, [(b":status", b"%d" % event.status_code)] + event.headers + self.config.response_headers("h2"), ) await self._flush() elif isinstance(event, (Body, Data)): self.priority.unblock(event.stream_id) await self.has_data.set() await self.stream_buffers[event.stream_id].push(event.data) elif isinstance(event, (EndBody, EndData)): self.stream_buffers[event.stream_id].set_complete() self.priority.unblock(event.stream_id) await self.has_data.set() await self.stream_buffers[event.stream_id].drain() elif isinstance(event, StreamClosed): await self._close_stream(event.stream_id) idle = len(self.streams) == 0 or all( stream.idle for stream in self.streams.values() ) if idle and self.context.terminated.is_set(): self.connection.close_connection() await self._flush() await self.send(Updated(idle=idle)) elif isinstance(event, Request): await self._create_server_push(event.stream_id, event.raw_path, event.headers) except ( BufferCompleteError, KeyError, priority.MissingStreamError, h2.exceptions.ProtocolError, ): # Connection has closed whilst blocked on flow control or # connection has advanced ahead of the last emitted event. return async def _handle_events(self, events: List[h2.events.Event]) -> None: for event in events: if isinstance(event, h2.events.RequestReceived): if self.context.terminated.is_set(): self.connection.reset_stream(event.stream_id) self.connection.update_settings( {h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 0} ) else: await self._create_stream(event) await self.send(Updated(idle=False)) elif isinstance(event, h2.events.DataReceived): await self.streams[event.stream_id].handle( Body(stream_id=event.stream_id, data=event.data) ) self.connection.acknowledge_received_data( event.flow_controlled_length, event.stream_id ) elif isinstance(event, h2.events.StreamEnded): await self.streams[event.stream_id].handle(EndBody(stream_id=event.stream_id)) elif isinstance(event, h2.events.StreamReset): await self._close_stream(event.stream_id) await self._window_updated(event.stream_id) elif isinstance(event, h2.events.WindowUpdated): await self._window_updated(event.stream_id) elif isinstance(event, h2.events.PriorityUpdated): await self._priority_updated(event) elif isinstance(event, h2.events.RemoteSettingsChanged): if h2.settings.SettingCodes.INITIAL_WINDOW_SIZE in event.changed_settings: await self._window_updated(None) elif isinstance(event, h2.events.ConnectionTerminated): await self.send(Closed()) await self._flush() async def _flush(self) -> None: data = self.connection.data_to_send() if data != b"": await self.send(RawData(data=data)) async def _window_updated(self, stream_id: Optional[int]) -> None: if stream_id is None or stream_id == 0: # Unblock all streams for stream_id in list(self.stream_buffers.keys()): self.priority.unblock(stream_id) elif stream_id is not None and stream_id in self.stream_buffers: self.priority.unblock(stream_id) await self.has_data.set() async def _priority_updated(self, event: h2.events.PriorityUpdated) -> None: try: self.priority.reprioritize( stream_id=event.stream_id, depends_on=event.depends_on or None, weight=event.weight, exclusive=event.exclusive, ) except priority.MissingStreamError: # Received PRIORITY frame before HEADERS frame self.priority.insert_stream( stream_id=event.stream_id, depends_on=event.depends_on or None, weight=event.weight, exclusive=event.exclusive, ) self.priority.block(event.stream_id) await self.has_data.set() async def _create_stream(self, request: h2.events.RequestReceived) -> None: for name, value in request.headers: if name == b":method": method = value.decode("ascii").upper() elif name == b":path": raw_path = value if method == "CONNECT": self.streams[request.stream_id] = WSStream( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.stream_send, request.stream_id, ) else: self.streams[request.stream_id] = HTTPStream( self.app, self.config, self.context, self.task_group, self.ssl, self.client, self.server, self.stream_send, request.stream_id, ) self.stream_buffers[request.stream_id] = StreamBuffer(self.context.event_class) try: self.priority.insert_stream(request.stream_id) except priority.DuplicateStreamError: # Recieved PRIORITY frame before HEADERS frame pass else: self.priority.block(request.stream_id) await self.streams[request.stream_id].handle( Request( stream_id=request.stream_id, headers=filter_pseudo_headers(request.headers), http_version="2", method=method, raw_path=raw_path, ) ) async def _create_server_push( self, stream_id: int, path: bytes, headers: List[Tuple[bytes, bytes]] ) -> None: push_stream_id = self.connection.get_next_available_stream_id() request_headers = [(b":method", b"GET"), (b":path", path)] request_headers.extend(headers) request_headers.extend(self.config.response_headers("h2")) try: self.connection.push_stream( stream_id=stream_id, promised_stream_id=push_stream_id, request_headers=request_headers, ) await self._flush() except h2.exceptions.ProtocolError: # Client does not accept push promises or we are trying to # push on a push promises request. pass else: event = h2.events.RequestReceived() event.stream_id = push_stream_id event.headers = request_headers await self._create_stream(event) await self.streams[event.stream_id].handle(EndBody(stream_id=event.stream_id)) async def _close_stream(self, stream_id: int) -> None: if stream_id in self.streams: stream = self.streams.pop(stream_id) await stream.handle(StreamClosed(stream_id=stream_id)) await self.has_data.set() hypercorn-0.14.4/src/hypercorn/protocol/h3.py000066400000000000000000000123041445231714500211440ustar00rootroot00000000000000from __future__ import annotations from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union from aioquic.h3.connection import H3Connection from aioquic.h3.events import DataReceived, HeadersReceived from aioquic.h3.exceptions import NoAvailablePushIDError from aioquic.quic.connection import QuicConnection from aioquic.quic.events import QuicEvent from .events import ( Body, Data, EndBody, EndData, Event as StreamEvent, InformationalResponse, Request, Response, StreamClosed, ) from .http_stream import HTTPStream from .ws_stream import WSStream from ..config import Config from ..typing import AppWrapper, TaskGroup, WorkerContext from ..utils import filter_pseudo_headers class H3Protocol: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, task_group: TaskGroup, client: Optional[Tuple[str, int]], server: Optional[Tuple[str, int]], quic: QuicConnection, send: Callable[[], Awaitable[None]], ) -> None: self.app = app self.client = client self.config = config self.context = context self.connection = H3Connection(quic) self.send = send self.server = server self.streams: Dict[int, Union[HTTPStream, WSStream]] = {} self.task_group = task_group async def handle(self, quic_event: QuicEvent) -> None: for event in self.connection.handle_event(quic_event): if isinstance(event, HeadersReceived): if not self.context.terminated.is_set(): await self._create_stream(event) if event.stream_ended: await self.streams[event.stream_id].handle( EndBody(stream_id=event.stream_id) ) elif isinstance(event, DataReceived): await self.streams[event.stream_id].handle( Body(stream_id=event.stream_id, data=event.data) ) if event.stream_ended: await self.streams[event.stream_id].handle(EndBody(stream_id=event.stream_id)) async def stream_send(self, event: StreamEvent) -> None: if isinstance(event, (InformationalResponse, Response)): self.connection.send_headers( event.stream_id, [(b":status", b"%d" % event.status_code)] + event.headers + self.config.response_headers("h3"), ) await self.send() elif isinstance(event, (Body, Data)): self.connection.send_data(event.stream_id, event.data, False) await self.send() elif isinstance(event, (EndBody, EndData)): self.connection.send_data(event.stream_id, b"", True) await self.send() elif isinstance(event, StreamClosed): pass # ?? elif isinstance(event, Request): await self._create_server_push(event.stream_id, event.raw_path, event.headers) async def _create_stream(self, request: HeadersReceived) -> None: for name, value in request.headers: if name == b":method": method = value.decode("ascii").upper() elif name == b":path": raw_path = value if method == "CONNECT": self.streams[request.stream_id] = WSStream( self.app, self.config, self.context, self.task_group, True, self.client, self.server, self.stream_send, request.stream_id, ) else: self.streams[request.stream_id] = HTTPStream( self.app, self.config, self.context, self.task_group, True, self.client, self.server, self.stream_send, request.stream_id, ) await self.streams[request.stream_id].handle( Request( stream_id=request.stream_id, headers=filter_pseudo_headers(request.headers), http_version="3", method=method, raw_path=raw_path, ) ) async def _create_server_push( self, stream_id: int, path: bytes, headers: List[Tuple[bytes, bytes]] ) -> None: request_headers = [(b":method", b"GET"), (b":path", path)] request_headers.extend(headers) request_headers.extend(self.config.response_headers("h3")) try: push_stream_id = self.connection.send_push_promise( stream_id=stream_id, headers=request_headers ) except NoAvailablePushIDError: # Client does not accept push promises or we are trying to # push on a push promises request. pass else: event = HeadersReceived( stream_id=push_stream_id, stream_ended=True, headers=request_headers ) await self._create_stream(event) await self.streams[event.stream_id].handle(EndBody(stream_id=event.stream_id)) hypercorn-0.14.4/src/hypercorn/protocol/http_stream.py000066400000000000000000000172651445231714500231770ustar00rootroot00000000000000from __future__ import annotations from enum import auto, Enum from time import time from typing import Awaitable, Callable, Optional, Tuple from urllib.parse import unquote from .events import Body, EndBody, Event, InformationalResponse, Request, Response, StreamClosed from ..config import Config from ..typing import ( AppWrapper, ASGISendEvent, HTTPResponseStartEvent, HTTPScope, TaskGroup, WorkerContext, ) from ..utils import ( build_and_validate_headers, suppress_body, UnexpectedMessageError, valid_server_name, ) PUSH_VERSIONS = {"2", "3"} EARLY_HINTS_VERSIONS = {"2", "3"} class ASGIHTTPState(Enum): # The ASGI Spec is clear that a response should not start till the # framework has sent at least one body message hence why this # state tracking is required. REQUEST = auto() RESPONSE = auto() CLOSED = auto() class HTTPStream: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, task_group: TaskGroup, ssl: bool, client: Optional[Tuple[str, int]], server: Optional[Tuple[str, int]], send: Callable[[Event], Awaitable[None]], stream_id: int, ) -> None: self.app = app self.client = client self.closed = False self.config = config self.context = context self.response: HTTPResponseStartEvent self.scope: HTTPScope self.send = send self.scheme = "https" if ssl else "http" self.server = server self.start_time: float self.state = ASGIHTTPState.REQUEST self.stream_id = stream_id self.task_group = task_group @property def idle(self) -> bool: return False async def handle(self, event: Event) -> None: if self.closed: return elif isinstance(event, Request): self.start_time = time() path, _, query_string = event.raw_path.partition(b"?") self.scope = { "type": "http", "http_version": event.http_version, "asgi": {"spec_version": "2.1", "version": "3.0"}, "method": event.method, "scheme": self.scheme, "path": unquote(path.decode("ascii")), "raw_path": path, "query_string": query_string, "root_path": self.config.root_path, "headers": event.headers, "client": self.client, "server": self.server, "extensions": {}, } if event.http_version in PUSH_VERSIONS: self.scope["extensions"]["http.response.push"] = {} if event.http_version in EARLY_HINTS_VERSIONS: self.scope["extensions"]["http.response.early_hint"] = {} if valid_server_name(self.config, event): self.app_put = await self.task_group.spawn_app( self.app, self.config, self.scope, self.app_send ) else: await self._send_error_response(404) self.closed = True elif isinstance(event, Body): await self.app_put( {"type": "http.request", "body": bytes(event.data), "more_body": True} ) elif isinstance(event, EndBody): await self.app_put({"type": "http.request", "body": b"", "more_body": False}) elif isinstance(event, StreamClosed): self.closed = True if self.app_put is not None: await self.app_put({"type": "http.disconnect"}) async def app_send(self, message: Optional[ASGISendEvent]) -> None: if message is None: # ASGI App has finished sending messages if not self.closed: # Cleanup if required if self.state == ASGIHTTPState.REQUEST: await self._send_error_response(500) await self.send(StreamClosed(stream_id=self.stream_id)) else: if message["type"] == "http.response.start" and self.state == ASGIHTTPState.REQUEST: self.response = message elif ( message["type"] == "http.response.push" and self.scope["http_version"] in PUSH_VERSIONS ): if not isinstance(message["path"], str): raise TypeError(f"{message['path']} should be a str") headers = [(b":scheme", self.scope["scheme"].encode())] for name, value in self.scope["headers"]: if name == b"host": headers.append((b":authority", value)) headers.extend(build_and_validate_headers(message["headers"])) await self.send( Request( stream_id=self.stream_id, headers=headers, http_version=self.scope["http_version"], method="GET", raw_path=message["path"].encode(), ) ) elif ( message["type"] == "http.response.early_hint" and self.scope["http_version"] in EARLY_HINTS_VERSIONS and self.state == ASGIHTTPState.REQUEST ): headers = [(b"link", bytes(link).strip()) for link in message["links"]] await self.send( InformationalResponse( stream_id=self.stream_id, headers=headers, status_code=103, ) ) elif message["type"] == "http.response.body" and self.state in { ASGIHTTPState.REQUEST, ASGIHTTPState.RESPONSE, }: if self.state == ASGIHTTPState.REQUEST: headers = build_and_validate_headers(self.response.get("headers", [])) await self.send( Response( stream_id=self.stream_id, headers=headers, status_code=int(self.response["status"]), ) ) self.state = ASGIHTTPState.RESPONSE if ( not suppress_body(self.scope["method"], int(self.response["status"])) and message.get("body", b"") != b"" ): await self.send( Body(stream_id=self.stream_id, data=bytes(message.get("body", b""))) ) if not message.get("more_body", False): if self.state != ASGIHTTPState.CLOSED: self.state = ASGIHTTPState.CLOSED await self.config.log.access( self.scope, self.response, time() - self.start_time ) await self.send(EndBody(stream_id=self.stream_id)) await self.send(StreamClosed(stream_id=self.stream_id)) else: raise UnexpectedMessageError(self.state, message["type"]) async def _send_error_response(self, status_code: int) -> None: await self.send( Response( stream_id=self.stream_id, headers=[(b"content-length", b"0"), (b"connection", b"close")], status_code=status_code, ) ) await self.send(EndBody(stream_id=self.stream_id)) self.state = ASGIHTTPState.CLOSED await self.config.log.access( self.scope, {"status": status_code, "headers": []}, time() - self.start_time ) hypercorn-0.14.4/src/hypercorn/protocol/quic.py000066400000000000000000000117511445231714500216000ustar00rootroot00000000000000from __future__ import annotations from functools import partial from typing import Awaitable, Callable, Dict, Optional, Tuple from aioquic.buffer import Buffer from aioquic.h3.connection import H3_ALPN from aioquic.quic.configuration import QuicConfiguration from aioquic.quic.connection import QuicConnection from aioquic.quic.events import ( ConnectionIdIssued, ConnectionIdRetired, ConnectionTerminated, ProtocolNegotiated, ) from aioquic.quic.packet import ( encode_quic_version_negotiation, PACKET_TYPE_INITIAL, pull_quic_header, ) from .h3 import H3Protocol from ..config import Config from ..events import Closed, Event, RawData from ..typing import AppWrapper, TaskGroup, WorkerContext class QuicProtocol: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, task_group: TaskGroup, server: Optional[Tuple[str, int]], send: Callable[[Event], Awaitable[None]], ) -> None: self.app = app self.config = config self.context = context self.connections: Dict[bytes, QuicConnection] = {} self.http_connections: Dict[QuicConnection, H3Protocol] = {} self.send = send self.server = server self.task_group = task_group self.quic_config = QuicConfiguration(alpn_protocols=H3_ALPN, is_client=False) self.quic_config.load_cert_chain(certfile=config.certfile, keyfile=config.keyfile) @property def idle(self) -> bool: return len(self.connections) == 0 and len(self.http_connections) == 0 async def handle(self, event: Event) -> None: if isinstance(event, RawData): try: header = pull_quic_header(Buffer(data=event.data), host_cid_length=8) except ValueError: return if ( header.version is not None and header.version not in self.quic_config.supported_versions ): data = encode_quic_version_negotiation( source_cid=header.destination_cid, destination_cid=header.source_cid, supported_versions=self.quic_config.supported_versions, ) await self.send(RawData(data=data, address=event.address)) return connection = self.connections.get(header.destination_cid) if ( connection is None and len(event.data) >= 1200 and header.packet_type == PACKET_TYPE_INITIAL and not self.context.terminated.is_set() ): connection = QuicConnection( configuration=self.quic_config, original_destination_connection_id=header.destination_cid, ) self.connections[header.destination_cid] = connection self.connections[connection.host_cid] = connection if connection is not None: connection.receive_datagram(event.data, event.address, now=self.context.time()) await self._handle_events(connection, event.address) elif isinstance(event, Closed): pass async def send_all(self, connection: QuicConnection) -> None: for data, address in connection.datagrams_to_send(now=self.context.time()): await self.send(RawData(data=data, address=address)) async def _handle_events( self, connection: QuicConnection, client: Optional[Tuple[str, int]] = None ) -> None: event = connection.next_event() while event is not None: if isinstance(event, ConnectionTerminated): pass elif isinstance(event, ProtocolNegotiated): self.http_connections[connection] = H3Protocol( self.app, self.config, self.context, self.task_group, client, self.server, connection, partial(self.send_all, connection), ) elif isinstance(event, ConnectionIdIssued): self.connections[event.connection_id] = connection elif isinstance(event, ConnectionIdRetired): del self.connections[event.connection_id] if connection in self.http_connections: await self.http_connections[connection].handle(event) event = connection.next_event() await self.send_all(connection) timer = connection.get_timer() if timer is not None: self.task_group.spawn(self._handle_timer, timer, connection) async def _handle_timer(self, timer: float, connection: QuicConnection) -> None: wait = max(0, timer - self.context.time()) await self.context.sleep(wait) if connection._close_at is not None: connection.handle_timer(now=self.context.time()) await self._handle_events(connection, None) hypercorn-0.14.4/src/hypercorn/protocol/ws_stream.py000066400000000000000000000347641445231714500226540ustar00rootroot00000000000000from __future__ import annotations from enum import auto, Enum from io import BytesIO, StringIO from time import time from typing import Awaitable, Callable, Iterable, List, Optional, Tuple, Union from urllib.parse import unquote from wsproto.connection import Connection, ConnectionState, ConnectionType from wsproto.events import ( BytesMessage, CloseConnection, Event as WSProtoEvent, Message, Ping, TextMessage, ) from wsproto.extensions import Extension, PerMessageDeflate from wsproto.frame_protocol import CloseReason from wsproto.handshake import server_extensions_handshake, WEBSOCKET_VERSION from wsproto.utilities import generate_accept_token, split_comma_header from .events import Body, Data, EndBody, EndData, Event, Request, Response, StreamClosed from ..config import Config from ..typing import ( AppWrapper, ASGISendEvent, TaskGroup, WebsocketAcceptEvent, WebsocketResponseBodyEvent, WebsocketResponseStartEvent, WebsocketScope, WorkerContext, ) from ..utils import ( build_and_validate_headers, suppress_body, UnexpectedMessageError, valid_server_name, ) class ASGIWebsocketState(Enum): # Hypercorn supports the ASGI websocket HTTP response extension, # which allows HTTP responses rather than acceptance. HANDSHAKE = auto() CONNECTED = auto() RESPONSE = auto() CLOSED = auto() HTTPCLOSED = auto() class FrameTooLargeError(Exception): pass class Handshake: def __init__(self, headers: List[Tuple[bytes, bytes]], http_version: str) -> None: self.http_version = http_version self.connection_tokens: Optional[List[str]] = None self.extensions: Optional[List[str]] = None self.key: Optional[bytes] = None self.subprotocols: Optional[List[str]] = None self.upgrade: Optional[bytes] = None self.version: Optional[bytes] = None for name, value in headers: name = name.lower() if name == b"connection": self.connection_tokens = split_comma_header(value) elif name == b"sec-websocket-extensions": self.extensions = split_comma_header(value) elif name == b"sec-websocket-key": self.key = value elif name == b"sec-websocket-protocol": self.subprotocols = split_comma_header(value) elif name == b"sec-websocket-version": self.version = value elif name == b"upgrade": self.upgrade = value def is_valid(self) -> bool: if self.http_version < "1.1": return False elif self.http_version == "1.1": if self.key is None: return False if self.connection_tokens is None or not any( token.lower() == "upgrade" for token in self.connection_tokens ): return False if self.upgrade.lower() != b"websocket": return False if self.version != WEBSOCKET_VERSION: return False return True def accept( self, subprotocol: Optional[str], additional_headers: Iterable[Tuple[bytes, bytes]], ) -> Tuple[int, List[Tuple[bytes, bytes]], Connection]: headers = [] if subprotocol is not None: if self.subprotocols is None or subprotocol not in self.subprotocols: raise Exception("Invalid Subprotocol") else: headers.append((b"sec-websocket-protocol", subprotocol.encode())) extensions: List[Extension] = [PerMessageDeflate()] accepts = None if self.extensions is not None: accepts = server_extensions_handshake(self.extensions, extensions) if accepts: headers.append((b"sec-websocket-extensions", accepts)) if self.key is not None: headers.append((b"sec-websocket-accept", generate_accept_token(self.key))) status_code = 200 if self.http_version == "1.1": headers.extend([(b"upgrade", b"WebSocket"), (b"connection", b"Upgrade")]) status_code = 101 for name, value in additional_headers: if b"sec-websocket-protocol" == name or name.startswith(b":"): raise Exception(f"Invalid additional header, {name.decode()}") headers.append((name, value)) return status_code, headers, Connection(ConnectionType.SERVER, extensions) class WebsocketBuffer: def __init__(self, max_length: int) -> None: self.value: Optional[Union[BytesIO, StringIO]] = None self.length = 0 self.max_length = max_length def extend(self, event: Message) -> None: if self.value is None: if isinstance(event, TextMessage): self.value = StringIO() else: self.value = BytesIO() self.length += self.value.write(event.data) if self.length > self.max_length: raise FrameTooLargeError() def clear(self) -> None: self.value = None self.length = 0 def to_message(self) -> dict: return { "type": "websocket.receive", "bytes": self.value.getvalue() if isinstance(self.value, BytesIO) else None, "text": self.value.getvalue() if isinstance(self.value, StringIO) else None, } class WSStream: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, task_group: TaskGroup, ssl: bool, client: Optional[Tuple[str, int]], server: Optional[Tuple[str, int]], send: Callable[[Event], Awaitable[None]], stream_id: int, ) -> None: self.app = app self.app_put: Optional[Callable] = None self.buffer = WebsocketBuffer(config.websocket_max_message_size) self.client = client self.closed = False self.config = config self.context = context self.task_group = task_group self.response: WebsocketResponseStartEvent self.scope: WebsocketScope self.send = send # RFC 8441 for HTTP/2 says use http or https, ASGI says ws or wss self.scheme = "wss" if ssl else "ws" self.server = server self.start_time: float self.state = ASGIWebsocketState.HANDSHAKE self.stream_id = stream_id self.connection: Connection self.handshake: Handshake @property def idle(self) -> bool: return self.state in {ASGIWebsocketState.CLOSED, ASGIWebsocketState.HTTPCLOSED} async def handle(self, event: Event) -> None: if self.closed: return elif isinstance(event, Request): self.start_time = time() self.handshake = Handshake(event.headers, event.http_version) path, _, query_string = event.raw_path.partition(b"?") self.scope = { "type": "websocket", "asgi": {"spec_version": "2.3", "version": "3.0"}, "scheme": self.scheme, "http_version": event.http_version, "path": unquote(path.decode("ascii")), "raw_path": path, "query_string": query_string, "root_path": self.config.root_path, "headers": event.headers, "client": self.client, "server": self.server, "subprotocols": self.handshake.subprotocols or [], "extensions": {"websocket.http.response": {}}, } if not valid_server_name(self.config, event): await self._send_error_response(404) self.closed = True elif not self.handshake.is_valid(): await self._send_error_response(400) self.closed = True else: self.app_put = await self.task_group.spawn_app( self.app, self.config, self.scope, self.app_send ) await self.app_put({"type": "websocket.connect"}) elif isinstance(event, (Body, Data)): self.connection.receive_data(event.data) await self._handle_events() elif isinstance(event, StreamClosed): self.closed = True if self.app_put is not None: if self.state in {ASGIWebsocketState.HTTPCLOSED, ASGIWebsocketState.CLOSED}: code = CloseReason.NORMAL_CLOSURE.value else: code = CloseReason.ABNORMAL_CLOSURE.value await self.app_put({"type": "websocket.disconnect", "code": code}) async def app_send(self, message: Optional[ASGISendEvent]) -> None: if self.closed: # Allow app to finish after close return if message is None: # ASGI App has finished sending messages # Cleanup if required if self.state == ASGIWebsocketState.HANDSHAKE: await self._send_error_response(500) await self.config.log.access( self.scope, {"status": 500, "headers": []}, time() - self.start_time ) elif self.state == ASGIWebsocketState.CONNECTED: await self._send_wsproto_event(CloseConnection(code=CloseReason.INTERNAL_ERROR)) await self.send(StreamClosed(stream_id=self.stream_id)) else: if message["type"] == "websocket.accept" and self.state == ASGIWebsocketState.HANDSHAKE: await self._accept(message) elif ( message["type"] == "websocket.http.response.start" and self.state == ASGIWebsocketState.HANDSHAKE ): self.response = message elif message["type"] == "websocket.http.response.body" and self.state in { ASGIWebsocketState.HANDSHAKE, ASGIWebsocketState.RESPONSE, }: await self._send_rejection(message) elif message["type"] == "websocket.send" and self.state == ASGIWebsocketState.CONNECTED: event: WSProtoEvent if message.get("bytes") is not None: event = BytesMessage(data=bytes(message["bytes"])) elif not isinstance(message["text"], str): raise TypeError(f"{message['text']} should be a str") else: event = TextMessage(data=message["text"]) await self._send_wsproto_event(event) elif ( message["type"] == "websocket.close" and self.state == ASGIWebsocketState.HANDSHAKE ): self.state = ASGIWebsocketState.HTTPCLOSED await self._send_error_response(403) elif message["type"] == "websocket.close": self.state = ASGIWebsocketState.CLOSED await self._send_wsproto_event( CloseConnection( code=int(message.get("code", CloseReason.NORMAL_CLOSURE)), reason=message.get("reason"), ) ) await self.send(EndData(stream_id=self.stream_id)) else: raise UnexpectedMessageError(self.state, message["type"]) async def _handle_events(self) -> None: for event in self.connection.events(): if isinstance(event, Message): try: self.buffer.extend(event) except FrameTooLargeError: await self._send_wsproto_event( CloseConnection(code=CloseReason.MESSAGE_TOO_BIG) ) break if event.message_finished: await self.app_put(self.buffer.to_message()) self.buffer.clear() elif isinstance(event, Ping): await self._send_wsproto_event(event.response()) elif isinstance(event, CloseConnection): if self.connection.state == ConnectionState.REMOTE_CLOSING: await self._send_wsproto_event(event.response()) await self.send(StreamClosed(stream_id=self.stream_id)) async def _send_error_response(self, status_code: int) -> None: await self.send( Response( stream_id=self.stream_id, status_code=status_code, headers=[(b"content-length", b"0"), (b"connection", b"close")], ) ) await self.send(EndBody(stream_id=self.stream_id)) await self.config.log.access( self.scope, {"status": status_code, "headers": []}, time() - self.start_time ) async def _send_wsproto_event(self, event: WSProtoEvent) -> None: data = self.connection.send(event) await self.send(Data(stream_id=self.stream_id, data=data)) async def _accept(self, message: WebsocketAcceptEvent) -> None: self.state = ASGIWebsocketState.CONNECTED status_code, headers, self.connection = self.handshake.accept( message.get("subprotocol"), message.get("headers", []) ) await self.send( Response(stream_id=self.stream_id, status_code=status_code, headers=headers) ) await self.config.log.access( self.scope, {"status": status_code, "headers": []}, time() - self.start_time ) if self.config.websocket_ping_interval is not None: self.task_group.spawn(self._send_pings) async def _send_rejection(self, message: WebsocketResponseBodyEvent) -> None: body_suppressed = suppress_body("GET", self.response["status"]) if self.state == ASGIWebsocketState.HANDSHAKE: headers = build_and_validate_headers(self.response["headers"]) await self.send( Response( stream_id=self.stream_id, status_code=int(self.response["status"]), headers=headers, ) ) self.state = ASGIWebsocketState.RESPONSE if not body_suppressed: await self.send(Body(stream_id=self.stream_id, data=bytes(message.get("body", b"")))) if not message.get("more_body", False): self.state = ASGIWebsocketState.HTTPCLOSED await self.send(EndBody(stream_id=self.stream_id)) await self.config.log.access(self.scope, self.response, time() - self.start_time) async def _send_pings(self) -> None: while not self.closed: await self._send_wsproto_event(Ping()) await self.context.sleep(self.config.websocket_ping_interval) hypercorn-0.14.4/src/hypercorn/py.typed000066400000000000000000000000071445231714500201130ustar00rootroot00000000000000Marker hypercorn-0.14.4/src/hypercorn/run.py000066400000000000000000000062401445231714500175770ustar00rootroot00000000000000from __future__ import annotations import platform import signal import time from multiprocessing import get_context from multiprocessing.context import BaseContext from multiprocessing.process import BaseProcess from multiprocessing.synchronize import Event as EventType from pickle import PicklingError from typing import Any, List from .config import Config, Sockets from .typing import WorkerFunc from .utils import load_application, wait_for_changes, write_pid_file def run(config: Config) -> None: if config.pid_path is not None: write_pid_file(config.pid_path) worker_func: WorkerFunc if config.worker_class == "asyncio": from .asyncio.run import asyncio_worker worker_func = asyncio_worker elif config.worker_class == "uvloop": from .asyncio.run import uvloop_worker worker_func = uvloop_worker elif config.worker_class == "trio": from .trio.run import trio_worker worker_func = trio_worker else: raise ValueError(f"No worker of class {config.worker_class} exists") sockets = config.create_sockets() # Load the application so that the correct paths are checked for # changes. load_application(config.application_path, config.wsgi_max_body_size) ctx = get_context("spawn") active = True while active: # Ignore SIGINT before creating the processes, so that they # inherit the signal handling. This means that the shutdown # function controls the shutdown. signal.signal(signal.SIGINT, signal.SIG_IGN) shutdown_event = ctx.Event() processes = start_processes(config, worker_func, sockets, shutdown_event, ctx) def shutdown(*args: Any) -> None: nonlocal active, shutdown_event shutdown_event.set() active = False for signal_name in {"SIGINT", "SIGTERM", "SIGBREAK"}: if hasattr(signal, signal_name): signal.signal(getattr(signal, signal_name), shutdown) if config.use_reloader: wait_for_changes(shutdown_event) shutdown_event.set() else: active = False for process in processes: process.join() for process in processes: process.terminate() for sock in sockets.secure_sockets: sock.close() for sock in sockets.insecure_sockets: sock.close() def start_processes( config: Config, worker_func: WorkerFunc, sockets: Sockets, shutdown_event: EventType, ctx: BaseContext, ) -> List[BaseProcess]: processes = [] for _ in range(config.workers): process = ctx.Process( # type: ignore target=worker_func, kwargs={"config": config, "shutdown_event": shutdown_event, "sockets": sockets}, ) process.daemon = True try: process.start() except PicklingError as error: raise RuntimeError( "Cannot pickle the config, see https://docs.python.org/3/library/pickle.html#pickle-picklable" # noqa: E501 ) from error processes.append(process) if platform.system() == "Windows": time.sleep(0.1) return processes hypercorn-0.14.4/src/hypercorn/statsd.py000066400000000000000000000073701445231714500203020ustar00rootroot00000000000000from __future__ import annotations from typing import Any, TYPE_CHECKING from .logging import Logger if TYPE_CHECKING: from .config import Config from .typing import ResponseSummary, WWWScope METRIC_VAR = "metric" VALUE_VAR = "value" MTYPE_VAR = "mtype" GAUGE_TYPE = "gauge" COUNTER_TYPE = "counter" HISTOGRAM_TYPE = "histogram" class StatsdLogger(Logger): def __init__(self, config: "Config") -> None: super().__init__(config) self.dogstatsd_tags = config.dogstatsd_tags self.prefix = config.statsd_prefix if len(self.prefix) and self.prefix[-1] != ".": self.prefix += "." async def critical(self, message: str, *args: Any, **kwargs: Any) -> None: await super().critical(message, *args, **kwargs) await self.increment("hypercorn.log.critical", 1) async def error(self, message: str, *args: Any, **kwargs: Any) -> None: await super().error(message, *args, **kwargs) await self.increment("hypercorn.log.error", 1) async def warning(self, message: str, *args: Any, **kwargs: Any) -> None: await super().warning(message, *args, **kwargs) await self.increment("hypercorn.log.warning", 1) async def info(self, message: str, *args: Any, **kwargs: Any) -> None: await super().info(message, *args, **kwargs) async def debug(self, message: str, *args: Any, **kwargs: Any) -> None: await super().debug(message, *args, **kwargs) async def exception(self, message: str, *args: Any, **kwargs: Any) -> None: await super().exception(message, *args, **kwargs) await self.increment("hypercorn.log.exception", 1) async def log(self, level: int, message: str, *args: Any, **kwargs: Any) -> None: try: extra = kwargs.get("extra", None) if extra is not None: metric = extra.get(METRIC_VAR, None) value = extra.get(VALUE_VAR, None) type_ = extra.get(MTYPE_VAR, None) if metric and value and type_: if type_ == GAUGE_TYPE: await self.gauge(metric, value) elif type_ == COUNTER_TYPE: await self.increment(metric, value) elif type_ == HISTOGRAM_TYPE: await self.histogram(metric, value) if message: await super().log(level, message, *args, **kwargs) except Exception: await super().warning("Failed to log to statsd", exc_info=True) async def access( self, request: "WWWScope", response: "ResponseSummary", request_time: float ) -> None: await super().access(request, response, request_time) await self.histogram("hypercorn.request.duration", request_time * 1_000) await self.increment("hypercorn.requests", 1) await self.increment(f"hypercorn.request.status.{response['status']}", 1) async def gauge(self, name: str, value: int) -> None: await self._send(f"{self.prefix}{name}:{value}|g") async def increment(self, name: str, value: int, sampling_rate: float = 1.0) -> None: await self._send(f"{self.prefix}{name}:{value}|c|@{sampling_rate}") async def decrement(self, name: str, value: int, sampling_rate: float = 1.0) -> None: await self._send(f"{self.prefix}{name}:-{value}|c|@{sampling_rate}") async def histogram(self, name: str, value: float) -> None: await self._send(f"{self.prefix}{name}:{value}|ms") async def _send(self, message: str) -> None: if self.dogstatsd_tags: message = f"{message}|#{self.dogstatsd_tags}" await self._socket_send(message.encode("ascii")) async def _socket_send(self, message: bytes) -> None: raise NotImplementedError() hypercorn-0.14.4/src/hypercorn/trio/000077500000000000000000000000001445231714500173745ustar00rootroot00000000000000hypercorn-0.14.4/src/hypercorn/trio/__init__.py000066400000000000000000000032011445231714500215010ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import Awaitable, Callable, Optional import trio from .run import worker_serve from ..config import Config from ..typing import Framework from ..utils import wrap_app try: from typing import Literal except ImportError: from typing_extensions import Literal # type: ignore async def serve( app: Framework, config: Config, *, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED, mode: Optional[Literal["asgi", "wsgi"]] = None, ) -> None: """Serve an ASGI framework app given the config. This allows for a programmatic way to serve an ASGI framework, it can be used via, .. code-block:: python trio.run(serve, app, config) It is assumed that the event-loop is configured before calling this function, therefore configuration values that relate to loop setup or process setup are ignored. Arguments: app: The ASGI application to serve. config: A Hypercorn configuration object. shutdown_trigger: This should return to trigger a graceful shutdown. mode: Specify if the app is WSGI or ASGI. """ if config.debug: warnings.warn("The config `debug` has no affect when using serve", Warning) if config.workers != 1: warnings.warn("The config `workers` has no affect when using serve", Warning) await worker_serve( wrap_app(app, config.wsgi_max_body_size, mode), config, shutdown_trigger=shutdown_trigger, task_status=task_status, ) hypercorn-0.14.4/src/hypercorn/trio/lifespan.py000066400000000000000000000067041445231714500215560ustar00rootroot00000000000000from __future__ import annotations import trio from ..config import Config from ..typing import AppWrapper, ASGIReceiveEvent, ASGISendEvent, LifespanScope from ..utils import LifespanFailureError, LifespanTimeoutError class UnexpectedMessageError(Exception): pass class Lifespan: def __init__(self, app: AppWrapper, config: Config) -> None: self.app = app self.config = config self.startup = trio.Event() self.shutdown = trio.Event() self.app_send_channel, self.app_receive_channel = trio.open_memory_channel( config.max_app_queue_size ) self.supported = True async def handle_lifespan( self, *, task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED ) -> None: task_status.started() scope: LifespanScope = { "type": "lifespan", "asgi": {"spec_version": "2.0", "version": "3.0"}, } try: await self.app( scope, self.asgi_receive, self.asgi_send, trio.to_thread.run_sync, trio.from_thread.run, ) except LifespanFailureError: # Lifespan failures should crash the server raise except Exception: self.supported = False if not self.startup.is_set(): await self.config.log.warning( "ASGI Framework Lifespan error, continuing without Lifespan support" ) elif not self.shutdown.is_set(): await self.config.log.exception( "ASGI Framework Lifespan error, shutdown without Lifespan support" ) else: await self.config.log.exception("ASGI Framework Lifespan errored after shutdown.") finally: self.startup.set() self.shutdown.set() await self.app_send_channel.aclose() await self.app_receive_channel.aclose() async def wait_for_startup(self) -> None: if not self.supported: return await self.app_send_channel.send({"type": "lifespan.startup"}) try: with trio.fail_after(self.config.startup_timeout): await self.startup.wait() except trio.TooSlowError as error: raise LifespanTimeoutError("startup") from error async def wait_for_shutdown(self) -> None: if not self.supported: return await self.app_send_channel.send({"type": "lifespan.shutdown"}) try: with trio.fail_after(self.config.shutdown_timeout): await self.shutdown.wait() except trio.TooSlowError as error: raise LifespanTimeoutError("startup") from error async def asgi_receive(self) -> ASGIReceiveEvent: return await self.app_receive_channel.receive() async def asgi_send(self, message: ASGISendEvent) -> None: if message["type"] == "lifespan.startup.complete": self.startup.set() elif message["type"] == "lifespan.shutdown.complete": self.shutdown.set() elif message["type"] == "lifespan.startup.failed": raise LifespanFailureError("startup", message.get("message", "")) elif message["type"] == "lifespan.shutdown.failed": raise LifespanFailureError("shutdown", message.get("message", "")) else: raise UnexpectedMessageError(message["type"]) hypercorn-0.14.4/src/hypercorn/trio/run.py000066400000000000000000000110601445231714500205500ustar00rootroot00000000000000from __future__ import annotations import sys from functools import partial from multiprocessing.synchronize import Event as EventType from typing import Awaitable, Callable, Optional import trio from .lifespan import Lifespan from .statsd import StatsdLogger from .tcp_server import TCPServer from .udp_server import UDPServer from .worker_context import WorkerContext from ..config import Config, Sockets from ..typing import AppWrapper from ..utils import ( check_multiprocess_shutdown_event, load_application, raise_shutdown, repr_socket_addr, ShutdownError, ) if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup async def worker_serve( app: AppWrapper, config: Config, *, sockets: Optional[Sockets] = None, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED, ) -> None: config.set_statsd_logger_class(StatsdLogger) lifespan = Lifespan(app, config) context = WorkerContext() async with trio.open_nursery() as lifespan_nursery: await lifespan_nursery.start(lifespan.handle_lifespan) await lifespan.wait_for_startup() async with trio.open_nursery() as server_nursery: if sockets is None: sockets = config.create_sockets() for sock in sockets.secure_sockets: sock.listen(config.backlog) for sock in sockets.insecure_sockets: sock.listen(config.backlog) ssl_context = config.create_ssl_context() listeners = [] binds = [] for sock in sockets.secure_sockets: listeners.append( trio.SSLListener( trio.SocketListener(trio.socket.from_stdlib_socket(sock)), ssl_context, https_compatible=True, ) ) bind = repr_socket_addr(sock.family, sock.getsockname()) binds.append(f"https://{bind}") await config.log.info(f"Running on https://{bind} (CTRL + C to quit)") for sock in sockets.insecure_sockets: listeners.append(trio.SocketListener(trio.socket.from_stdlib_socket(sock))) bind = repr_socket_addr(sock.family, sock.getsockname()) binds.append(f"http://{bind}") await config.log.info(f"Running on http://{bind} (CTRL + C to quit)") for sock in sockets.quic_sockets: await server_nursery.start(UDPServer(app, config, context, sock).run) bind = repr_socket_addr(sock.family, sock.getsockname()) await config.log.info(f"Running on https://{bind} (QUIC) (CTRL + C to quit)") task_status.started(binds) try: async with trio.open_nursery(strict_exception_groups=True) as nursery: if shutdown_trigger is not None: nursery.start_soon(raise_shutdown, shutdown_trigger) nursery.start_soon( partial( trio.serve_listeners, partial(TCPServer, app, config, context), listeners, handler_nursery=server_nursery, ), ) await trio.sleep_forever() except BaseExceptionGroup as error: _, other_errors = error.split((ShutdownError, KeyboardInterrupt)) if other_errors is not None: raise other_errors finally: await context.terminated.set() server_nursery.cancel_scope.deadline = trio.current_time() + config.graceful_timeout await lifespan.wait_for_shutdown() lifespan_nursery.cancel_scope.cancel() def trio_worker( config: Config, sockets: Optional[Sockets] = None, shutdown_event: Optional[EventType] = None ) -> None: if sockets is not None: for sock in sockets.secure_sockets: sock.listen(config.backlog) for sock in sockets.insecure_sockets: sock.listen(config.backlog) app = load_application(config.application_path, config.wsgi_max_body_size) shutdown_trigger = None if shutdown_event is not None: shutdown_trigger = partial(check_multiprocess_shutdown_event, shutdown_event, trio.sleep) trio.run(partial(worker_serve, app, config, sockets=sockets, shutdown_trigger=shutdown_trigger)) hypercorn-0.14.4/src/hypercorn/trio/statsd.py000066400000000000000000000007551445231714500212570ustar00rootroot00000000000000from __future__ import annotations import trio from ..config import Config from ..statsd import StatsdLogger as Base class StatsdLogger(Base): def __init__(self, config: Config) -> None: super().__init__(config) self.address = tuple(config.statsd_host.rsplit(":", 1)) self.socket = trio.socket.socket(trio.socket.AF_INET, trio.socket.SOCK_DGRAM) async def _socket_send(self, message: bytes) -> None: await self.socket.sendto(message, self.address) hypercorn-0.14.4/src/hypercorn/trio/task_group.py000066400000000000000000000045711445231714500221330ustar00rootroot00000000000000from __future__ import annotations import sys from types import TracebackType from typing import Any, Awaitable, Callable, Optional import trio from ..config import Config from ..typing import AppWrapper, ASGIReceiveCallable, ASGIReceiveEvent, ASGISendEvent, Scope if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup async def _handle( app: AppWrapper, config: Config, scope: Scope, receive: ASGIReceiveCallable, send: Callable[[Optional[ASGISendEvent]], Awaitable[None]], sync_spawn: Callable, call_soon: Callable, ) -> None: try: await app(scope, receive, send, sync_spawn, call_soon) except trio.Cancelled: raise except BaseExceptionGroup as error: _, other_errors = error.split(trio.Cancelled) if other_errors is not None: await config.log.exception("Error in ASGI Framework") await send(None) else: raise except Exception: await config.log.exception("Error in ASGI Framework") finally: await send(None) class TaskGroup: def __init__(self) -> None: self._nursery: Optional[trio._core._run.Nursery] = None self._nursery_manager: Optional[trio._core._run.NurseryManager] = None async def spawn_app( self, app: AppWrapper, config: Config, scope: Scope, send: Callable[[Optional[ASGISendEvent]], Awaitable[None]], ) -> Callable[[ASGIReceiveEvent], Awaitable[None]]: app_send_channel, app_receive_channel = trio.open_memory_channel(config.max_app_queue_size) self._nursery.start_soon( _handle, app, config, scope, app_receive_channel.receive, send, trio.to_thread.run_sync, trio.from_thread.run, ) return app_send_channel.send def spawn(self, func: Callable, *args: Any) -> None: self._nursery.start_soon(func, *args) async def __aenter__(self) -> TaskGroup: self._nursery_manager = trio.open_nursery() self._nursery = await self._nursery_manager.__aenter__() return self async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: await self._nursery_manager.__aexit__(exc_type, exc_value, tb) self._nursery_manager = None self._nursery = None hypercorn-0.14.4/src/hypercorn/trio/tcp_server.py000066400000000000000000000117161445231714500221300ustar00rootroot00000000000000from __future__ import annotations from math import inf from typing import Any, Generator, Optional import trio from .task_group import TaskGroup from .worker_context import WorkerContext from ..config import Config from ..events import Closed, Event, RawData, Updated from ..protocol import ProtocolWrapper from ..typing import AppWrapper from ..utils import parse_socket_addr MAX_RECV = 2**16 class TCPServer: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, stream: trio.abc.Stream ) -> None: self.app = app self.config = config self.context = context self.protocol: ProtocolWrapper self.send_lock = trio.Lock() self.idle_lock = trio.Lock() self.stream = stream self._idle_handle: Optional[trio.CancelScope] = None def __await__(self) -> Generator[Any, None, None]: return self.run().__await__() async def run(self) -> None: try: try: with trio.fail_after(self.config.ssl_handshake_timeout): await self.stream.do_handshake() except (trio.BrokenResourceError, trio.TooSlowError): return # Handshake failed alpn_protocol = self.stream.selected_alpn_protocol() socket = self.stream.transport_stream.socket ssl = True except AttributeError: # Not SSL alpn_protocol = "http/1.1" socket = self.stream.socket ssl = False try: client = parse_socket_addr(socket.family, socket.getpeername()) server = parse_socket_addr(socket.family, socket.getsockname()) async with TaskGroup() as task_group: self._task_group = task_group self.protocol = ProtocolWrapper( self.app, self.config, self.context, task_group, ssl, client, server, self.protocol_send, alpn_protocol, ) await self.protocol.initiate() await self._start_idle() await self._read_data() except OSError: pass finally: await self._close() async def protocol_send(self, event: Event) -> None: if isinstance(event, RawData): async with self.send_lock: try: with trio.CancelScope() as cancel_scope: cancel_scope.shield = True await self.stream.send_all(event.data) except (trio.BrokenResourceError, trio.ClosedResourceError): await self.protocol.handle(Closed()) elif isinstance(event, Closed): await self._close() await self.protocol.handle(Closed()) elif isinstance(event, Updated): if event.idle: await self._start_idle() else: await self._stop_idle() async def _read_data(self) -> None: while True: try: with trio.fail_after(self.config.read_timeout or inf): data = await self.stream.receive_some(MAX_RECV) except ( trio.ClosedResourceError, trio.BrokenResourceError, trio.TooSlowError, ): break else: await self.protocol.handle(RawData(data)) if data == b"": break await self.protocol.handle(Closed()) async def _close(self) -> None: try: await self.stream.send_eof() except ( trio.BrokenResourceError, AttributeError, trio.BusyResourceError, trio.ClosedResourceError, ): # They're already gone, nothing to do # Or it is a SSL stream pass await self.stream.aclose() async def _initiate_server_close(self) -> None: await self.protocol.handle(Closed()) await self.stream.aclose() async def _start_idle(self) -> None: async with self.idle_lock: if self._idle_handle is None: self._idle_handle = await self._task_group._nursery.start(self._run_idle) async def _stop_idle(self) -> None: async with self.idle_lock: if self._idle_handle is not None: self._idle_handle.cancel() self._idle_handle = None async def _run_idle( self, task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED, ) -> None: cancel_scope = trio.CancelScope() task_status.started(cancel_scope) with cancel_scope: with trio.move_on_after(self.config.keep_alive_timeout): await self.context.terminated.wait() cancel_scope.shield = True await self._initiate_server_close() hypercorn-0.14.4/src/hypercorn/trio/udp_server.py000066400000000000000000000027521445231714500221320ustar00rootroot00000000000000from __future__ import annotations import trio from .task_group import TaskGroup from .worker_context import WorkerContext from ..config import Config from ..events import Event, RawData from ..typing import AppWrapper from ..utils import parse_socket_addr MAX_RECV = 2**16 class UDPServer: def __init__( self, app: AppWrapper, config: Config, context: WorkerContext, socket: trio.socket.socket, ) -> None: self.app = app self.config = config self.context = context self.socket = trio.socket.from_stdlib_socket(socket) async def run( self, task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED ) -> None: from ..protocol.quic import QuicProtocol # h3/Quic is an optional part of Hypercorn task_status.started() server = parse_socket_addr(self.socket.family, self.socket.getsockname()) async with TaskGroup() as task_group: self.protocol = QuicProtocol( self.app, self.config, self.context, task_group, server, self.protocol_send ) while not self.context.terminated.is_set() or not self.protocol.idle: data, address = await self.socket.recvfrom(MAX_RECV) await self.protocol.handle(RawData(data=data, address=address)) async def protocol_send(self, event: Event) -> None: if isinstance(event, RawData): await self.socket.sendto(event.data, event.address) hypercorn-0.14.4/src/hypercorn/trio/worker_context.py000066400000000000000000000014361445231714500230270ustar00rootroot00000000000000from __future__ import annotations from typing import Type, Union import trio from ..typing import Event class EventWrapper: def __init__(self) -> None: self._event = trio.Event() async def clear(self) -> None: self._event = trio.Event() async def wait(self) -> None: await self._event.wait() async def set(self) -> None: self._event.set() def is_set(self) -> bool: return self._event.is_set() class WorkerContext: event_class: Type[Event] = EventWrapper def __init__(self) -> None: self.terminated = self.event_class() @staticmethod async def sleep(wait: Union[float, int]) -> None: return await trio.sleep(wait) @staticmethod def time() -> float: return trio.current_time() hypercorn-0.14.4/src/hypercorn/typing.py000066400000000000000000000161031445231714500203040ustar00rootroot00000000000000from __future__ import annotations from multiprocessing.synchronize import Event as EventType from types import TracebackType from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union import h2.events import h11 # Till PEP 544 is accepted try: from typing import Literal, Protocol, TypedDict except ImportError: from typing_extensions import Literal, Protocol, TypedDict # type: ignore from .config import Config, Sockets H11SendableEvent = Union[h11.Data, h11.EndOfMessage, h11.InformationalResponse, h11.Response] WorkerFunc = Callable[[Config, Optional[Sockets], Optional[EventType]], None] class ASGIVersions(TypedDict, total=False): spec_version: str version: Union[Literal["2.0"], Literal["3.0"]] class HTTPScope(TypedDict): type: Literal["http"] asgi: ASGIVersions http_version: str method: str scheme: str path: str raw_path: bytes query_string: bytes root_path: str headers: Iterable[Tuple[bytes, bytes]] client: Optional[Tuple[str, int]] server: Optional[Tuple[str, Optional[int]]] extensions: Dict[str, dict] class WebsocketScope(TypedDict): type: Literal["websocket"] asgi: ASGIVersions http_version: str scheme: str path: str raw_path: bytes query_string: bytes root_path: str headers: Iterable[Tuple[bytes, bytes]] client: Optional[Tuple[str, int]] server: Optional[Tuple[str, Optional[int]]] subprotocols: Iterable[str] extensions: Dict[str, dict] class LifespanScope(TypedDict): type: Literal["lifespan"] asgi: ASGIVersions WWWScope = Union[HTTPScope, WebsocketScope] Scope = Union[HTTPScope, WebsocketScope, LifespanScope] class HTTPRequestEvent(TypedDict): type: Literal["http.request"] body: bytes more_body: bool class HTTPResponseStartEvent(TypedDict): type: Literal["http.response.start"] status: int headers: Iterable[Tuple[bytes, bytes]] class HTTPResponseBodyEvent(TypedDict): type: Literal["http.response.body"] body: bytes more_body: bool class HTTPServerPushEvent(TypedDict): type: Literal["http.response.push"] path: str headers: Iterable[Tuple[bytes, bytes]] class HTTPEarlyHintEvent(TypedDict): type: Literal["http.response.early_hint"] links: Iterable[bytes] class HTTPDisconnectEvent(TypedDict): type: Literal["http.disconnect"] class WebsocketConnectEvent(TypedDict): type: Literal["websocket.connect"] class WebsocketAcceptEvent(TypedDict): type: Literal["websocket.accept"] subprotocol: Optional[str] headers: Iterable[Tuple[bytes, bytes]] class WebsocketReceiveEvent(TypedDict): type: Literal["websocket.receive"] bytes: Optional[bytes] text: Optional[str] class WebsocketSendEvent(TypedDict): type: Literal["websocket.send"] bytes: Optional[bytes] text: Optional[str] class WebsocketResponseStartEvent(TypedDict): type: Literal["websocket.http.response.start"] status: int headers: Iterable[Tuple[bytes, bytes]] class WebsocketResponseBodyEvent(TypedDict): type: Literal["websocket.http.response.body"] body: bytes more_body: bool class WebsocketDisconnectEvent(TypedDict): type: Literal["websocket.disconnect"] code: int class WebsocketCloseEvent(TypedDict): type: Literal["websocket.close"] code: int reason: Optional[str] class LifespanStartupEvent(TypedDict): type: Literal["lifespan.startup"] class LifespanShutdownEvent(TypedDict): type: Literal["lifespan.shutdown"] class LifespanStartupCompleteEvent(TypedDict): type: Literal["lifespan.startup.complete"] class LifespanStartupFailedEvent(TypedDict): type: Literal["lifespan.startup.failed"] message: str class LifespanShutdownCompleteEvent(TypedDict): type: Literal["lifespan.shutdown.complete"] class LifespanShutdownFailedEvent(TypedDict): type: Literal["lifespan.shutdown.failed"] message: str ASGIReceiveEvent = Union[ HTTPRequestEvent, HTTPDisconnectEvent, WebsocketConnectEvent, WebsocketReceiveEvent, WebsocketDisconnectEvent, LifespanStartupEvent, LifespanShutdownEvent, ] ASGISendEvent = Union[ HTTPResponseStartEvent, HTTPResponseBodyEvent, HTTPServerPushEvent, HTTPEarlyHintEvent, HTTPDisconnectEvent, WebsocketAcceptEvent, WebsocketSendEvent, WebsocketResponseStartEvent, WebsocketResponseBodyEvent, WebsocketCloseEvent, LifespanStartupCompleteEvent, LifespanStartupFailedEvent, LifespanShutdownCompleteEvent, LifespanShutdownFailedEvent, ] ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]] ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]] ASGIFramework = Callable[ [ Scope, ASGIReceiveCallable, ASGISendCallable, ], Awaitable[None], ] WSGIFramework = Callable[[dict, Callable], Iterable[bytes]] Framework = Union[ASGIFramework, WSGIFramework] class H2SyncStream(Protocol): scope: dict def data_received(self, data: bytes) -> None: ... def ended(self) -> None: ... def reset(self) -> None: ... def close(self) -> None: ... async def handle_request( self, event: h2.events.RequestReceived, scheme: str, client: Tuple[str, int], server: Tuple[str, int], ) -> None: ... class H2AsyncStream(Protocol): scope: dict async def data_received(self, data: bytes) -> None: ... async def ended(self) -> None: ... async def reset(self) -> None: ... async def close(self) -> None: ... async def handle_request( self, event: h2.events.RequestReceived, scheme: str, client: Tuple[str, int], server: Tuple[str, int], ) -> None: ... class Event(Protocol): def __init__(self) -> None: ... async def clear(self) -> None: ... async def set(self) -> None: ... async def wait(self) -> None: ... def is_set(self) -> bool: ... class WorkerContext(Protocol): event_class: Type[Event] terminated: Event @staticmethod async def sleep(wait: Union[float, int]) -> None: ... @staticmethod def time() -> float: ... class TaskGroup(Protocol): async def spawn_app( self, app: AppWrapper, config: Config, scope: Scope, send: Callable[[Optional[ASGISendEvent]], Awaitable[None]], ) -> Callable[[ASGIReceiveEvent], Awaitable[None]]: ... def spawn(self, func: Callable, *args: Any) -> None: ... async def __aenter__(self) -> TaskGroup: ... async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: ... class ResponseSummary(TypedDict): status: int headers: Iterable[Tuple[bytes, bytes]] class AppWrapper(Protocol): async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable, sync_spawn: Callable, call_soon: Callable, ) -> None: ... hypercorn-0.14.4/src/hypercorn/utils.py000066400000000000000000000150111445231714500201270ustar00rootroot00000000000000from __future__ import annotations import inspect import os import socket import sys import time from enum import Enum from importlib import import_module from multiprocessing.synchronize import Event as EventType from pathlib import Path from typing import ( Any, Awaitable, Callable, cast, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING, ) from .app_wrappers import ASGIWrapper, WSGIWrapper from .config import Config from .typing import AppWrapper, ASGIFramework, Framework, WSGIFramework try: from typing import Literal except ImportError: from typing_extensions import Literal # type: ignore if TYPE_CHECKING: from .protocol.events import Request class ShutdownError(Exception): pass class NoAppError(Exception): pass class LifespanTimeoutError(Exception): def __init__(self, stage: str) -> None: super().__init__( f"Timeout whilst awaiting {stage}. Your application may not support the ASGI Lifespan " f"protocol correctly, alternatively the {stage}_timeout configuration is incorrect." ) class LifespanFailureError(Exception): def __init__(self, stage: str, message: str) -> None: super().__init__(f"Lifespan failure in {stage}. '{message}'") class UnexpectedMessageError(Exception): def __init__(self, state: Enum, message_type: str) -> None: super().__init__(f"Unexpected message type, {message_type} given the state {state}") class FrameTooLargeError(Exception): pass def suppress_body(method: str, status_code: int) -> bool: return method == "HEAD" or 100 <= status_code < 200 or status_code in {204, 304} def build_and_validate_headers(headers: Iterable[Tuple[bytes, bytes]]) -> List[Tuple[bytes, bytes]]: # Validates that the header name and value are bytes validated_headers: List[Tuple[bytes, bytes]] = [] for name, value in headers: if name[0] == b":"[0]: raise ValueError("Pseudo headers are not valid") validated_headers.append((bytes(name).strip(), bytes(value).strip())) return validated_headers def filter_pseudo_headers(headers: List[Tuple[bytes, bytes]]) -> List[Tuple[bytes, bytes]]: filtered_headers: List[Tuple[bytes, bytes]] = [(b"host", b"")] # Placeholder authority = None host = b"" for name, value in headers: if name == b":authority": # h2 & h3 libraries validate this is present authority = value elif name == b"host": host = value elif name[0] != b":"[0]: filtered_headers.append((name, value)) filtered_headers[0] = (b"host", authority if authority is not None else host) return filtered_headers def load_application(path: str, wsgi_max_body_size: int) -> AppWrapper: mode: Optional[Literal["asgi", "wsgi"]] = None if ":" not in path: module_name, app_name = path, "app" elif path.count(":") == 2: mode, module_name, app_name = path.split(":", 2) # type: ignore if mode not in {"asgi", "wsgi"}: raise ValueError("Invalid mode, must be 'asgi', or 'wsgi'") else: module_name, app_name = path.split(":", 1) module_path = Path(module_name).resolve() sys.path.insert(0, str(module_path.parent)) if module_path.is_file(): import_name = module_path.with_suffix("").name else: import_name = module_path.name try: module = import_module(import_name) except ModuleNotFoundError as error: if error.name == import_name: raise NoAppError() else: raise try: app = eval(app_name, vars(module)) except NameError: raise NoAppError() else: return wrap_app(app, wsgi_max_body_size, mode) def wrap_app( app: Framework, wsgi_max_body_size: int, mode: Optional[Literal["asgi", "wsgi"]] ) -> AppWrapper: if mode is None: mode = "asgi" if is_asgi(app) else "wsgi" if mode == "asgi": return ASGIWrapper(cast(ASGIFramework, app)) else: return WSGIWrapper(cast(WSGIFramework, app), wsgi_max_body_size) def wait_for_changes(shutdown_event: EventType) -> None: last_updates: Dict[Path, float] = {} for module in list(sys.modules.values()): filename = getattr(module, "__file__", None) if filename is None: continue path = Path(filename) try: last_updates[Path(filename)] = path.stat().st_mtime except (FileNotFoundError, NotADirectoryError): pass while not shutdown_event.is_set(): time.sleep(1) for index, (path, last_mtime) in enumerate(last_updates.items()): if index % 10 == 0: # Yield to the event loop time.sleep(0) try: mtime = path.stat().st_mtime except FileNotFoundError: return else: if mtime > last_mtime: return else: last_updates[path] = mtime async def raise_shutdown(shutdown_event: Callable[..., Awaitable[None]]) -> None: await shutdown_event() raise ShutdownError() async def check_multiprocess_shutdown_event( shutdown_event: EventType, sleep: Callable[[float], Awaitable[Any]] ) -> None: while True: if shutdown_event.is_set(): return await sleep(0.1) def write_pid_file(pid_path: str) -> None: with open(pid_path, "w") as file_: file_.write(f"{os.getpid()}") def parse_socket_addr(family: int, address: tuple) -> Optional[Tuple[str, int]]: if family == socket.AF_INET: return address # type: ignore elif family == socket.AF_INET6: return (address[0], address[1]) else: return None def repr_socket_addr(family: int, address: tuple) -> str: if family == socket.AF_INET: return f"{address[0]}:{address[1]}" elif family == socket.AF_INET6: return f"[{address[0]}]:{address[1]}" elif family == socket.AF_UNIX: return f"unix:{address}" else: return f"{address}" def valid_server_name(config: Config, request: "Request") -> bool: if len(config.server_names) == 0: return True host = "" for name, value in request.headers: if name.lower() == b"host": host = value.decode() break return host in config.server_names def is_asgi(app: Any) -> bool: if inspect.iscoroutinefunction(app): return True elif hasattr(app, "__call__"): return inspect.iscoroutinefunction(app.__call__) return False hypercorn-0.14.4/tests/000077500000000000000000000000001445231714500147615ustar00rootroot00000000000000hypercorn-0.14.4/tests/__init__.py000066400000000000000000000000001445231714500170600ustar00rootroot00000000000000hypercorn-0.14.4/tests/assets/000077500000000000000000000000001445231714500162635ustar00rootroot00000000000000hypercorn-0.14.4/tests/assets/cert.pem000066400000000000000000000031631445231714500177260ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEljCCAn4CCQDieGkyts8ORjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV SzAeFw0xODA3MDcxMjMwMzBaFw0xOTA3MDcxMjMwMzBaMA0xCzAJBgNVBAYTAlVL MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA8yUfZhLb97gXwvoASCei zgzk8mla/No1Kyo15/O9qxkyRtsIEhg1u/oQO0IEegG9/vRj2SpDwGanzE9HGW7n 5ekGy7UDYUWphcqE687BT+nSH2fSD4ZYY+nUlPO3ZE/n0eh/wniiImfZlGqOpwnS po1LwQHnuUXgOEKNCyAXX7GAYbSP7YC93Ytxs49luAJsJ+W+9Pepgz3hN7hMyKsq 3iqGXgGxhJPZXFLbvm+TKDeqw6ye1n5x8a7LX2xNGjZHgy0FWFK+Iu/s0SFp3o0a OPOgMGt90C1zD9Nwk1Ytn7coYgmoe0wAlzT9Nzt2B7iYLsjzBmbdA4Y9vCqqS7W6 WZX4Vsnuh304fKnKDA2ATGZsh/o+aEhfNp386mrHx4IvmmxHex8Ga4WHyjF8MnMJ v50j692g0eiftKTddU1yDYdaQiYKiNYjQJcY+KW0sbZmjvXvmbJLMsfSSll6c0bl IvSj/E2W6J9LXKr73fZLFYQvF8CNwK2VidsoBNqN5OmhaaAOLe3r9YW2cfTvo84L i/lGrm2WrNAz9s+CuldAeidmBaqUi21kcWDegzKB3zA1ru2SMnxXpoZTS0fCQ2rk StlgdvEFeevEiQ2BYZ3meBaYedAxpZS92Eva5Y2glzXPvlzIXm09Keje11osmn/U saXTAPtpYkyF5pfsrnAWXWUCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsOsQk813 Y+H0Sh5T/vdBP6jG+w+svvP1xFlKTyMxzVb0cLrxtQTVgiLOvSTPSuCfgT2nGY13 ZRHCsmH+ymlLtXWl5lWcBnQ1hP9FF6z5ygdJeQS5ZcLnDfqOl8eOXAiFH1xjNZMH BsDFqjA8XvW9SvFPlnwIGmvj+4Nsm44d6AJszvr6TsMfuumEJqWm8pCkf7M5jF8m piemGUsD0S+mihaRSQEC99AM4BFoh/W3TuMd2svaMq923yNcveCByF64wddAbrpm UMPi4GLWVU9LbPTRNgsOlciVUm1kpoB8Vdai+MMiqk6fREYUXyT4VDFAVEKMXSu1 EL6TyTvhSgIja3M4tA579aQsjdBnTreeJRcF3bPlCIJ2dQ3SaR8c6061DCtDoNek B19LVHrI3C1otcWj+IXhw5Tp24wVUv8LYx3ZTpB7yGiHwGck12VKUKkq75eIR5bb b3RH89X1JXhQLGNJ+AJGxDzmqds+YZKWFmK7UNTX+MN3GufZrBKA4EHFgQn8mTUE GnunL6OR2Hj+zUGeA+qiLmul4Aiyh7DB3/sdbd5pDv2+zsLexsx7/DYpA3YazYrg dv31B8O3bcIOBNx31QCCGH9yLY2dwikImwC9nnKsRPC8TftPSUEToMG5O1JE/Q22 q9gqqhDH1dzGIQZAJDremeU/VkxaI4q0PcQ= -----END CERTIFICATE----- hypercorn-0.14.4/tests/assets/config.py000066400000000000000000000002141445231714500200770ustar00rootroot00000000000000from __future__ import annotations import ssl # noqa: F401 access_log_format = "bob" bind = "127.0.0.1:5555" h11_max_incomplete_size = 4 hypercorn-0.14.4/tests/assets/config.toml000066400000000000000000000001161445231714500204230ustar00rootroot00000000000000access_log_format = "bob" bind = "127.0.0.1:5555" h11_max_incomplete_size = 4 hypercorn-0.14.4/tests/assets/config_ssl.py000066400000000000000000000003211445231714500207570ustar00rootroot00000000000000from __future__ import annotations access_log_format = "bob" bind = ["127.0.0.1:5555"] certfile = "tests/assets/cert.pem" ciphers = "ECDHE+AESGCM" h11_max_incomplete_size = 4 keyfile = "tests/assets/key.pem" hypercorn-0.14.4/tests/assets/key.pem000066400000000000000000000063041445231714500175610ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDzJR9mEtv3uBfC +gBIJ6LODOTyaVr82jUrKjXn872rGTJG2wgSGDW7+hA7QgR6Ab3+9GPZKkPAZqfM T0cZbufl6QbLtQNhRamFyoTrzsFP6dIfZ9IPhlhj6dSU87dkT+fR6H/CeKIiZ9mU ao6nCdKmjUvBAee5ReA4Qo0LIBdfsYBhtI/tgL3di3Gzj2W4Amwn5b7096mDPeE3 uEzIqyreKoZeAbGEk9lcUtu+b5MoN6rDrJ7WfnHxrstfbE0aNkeDLQVYUr4i7+zR IWnejRo486Awa33QLXMP03CTVi2ftyhiCah7TACXNP03O3YHuJguyPMGZt0Dhj28 KqpLtbpZlfhWye6HfTh8qcoMDYBMZmyH+j5oSF82nfzqasfHgi+abEd7HwZrhYfK MXwycwm/nSPr3aDR6J+0pN11TXINh1pCJgqI1iNAlxj4pbSxtmaO9e+Zsksyx9JK WXpzRuUi9KP8TZbon0tcqvvd9ksVhC8XwI3ArZWJ2ygE2o3k6aFpoA4t7ev1hbZx 9O+jzguL+UaubZas0DP2z4K6V0B6J2YFqpSLbWRxYN6DMoHfMDWu7ZIyfFemhlNL R8JDauRK2WB28QV568SJDYFhneZ4Fph50DGllL3YS9rljaCXNc++XMhebT0p6N7X Wiyaf9SxpdMA+2liTIXml+yucBZdZQIDAQABAoICACnSw+Dh85ZbwzKVoEDJGJcK 3sLX3n/J5QVkwFsCsShiMCTB/lRmd6+65tnalDyMWislzJsJSxgoUEqzhE5apmcE u1eE7mzn96381Ppe2R+u36bpS9fByyh8i0WH2o7Vs9GGhZtk9ramWGXQInOXG/Xs LhCoDDzxSQ1EXVCBl6OtO6ES1wMKdx5Joyg4zU1mlUYTndIzW6Qom7ni6MpHrxsC A5TeA7QDXosj8YqDVLPBR41a/wN0QpNI9tCWJ3kPxyNINjgoG26VCI48iiJu8QjE 11Qc2Upa1wTs4NtnInfroHWkpad3vk5EHh5HCxlu5jZ9+Fesj+3QRIQ+boaRXtk+ Pg+h0693zeN8clw6py2rjwYzLDQYzGQ+sy/Ru+dz15NhhbGKJfV/z32HNEKHyosu H8A66Rym9eCzhfKQWouEgDXT4dsmFwwfVJr5HASoAkxFgO7d0HnhmqYNvhMrK88K Vqyfz1RK1lqpFYIZHFi6sICi4IV+vP87RciFFOn8s4h0zJlyU7zjNGollPoSr5tQ 6UefyNfvNbS4rG38gxiFB+cYamZMLdTjK5r8c7ibs7nDQOakkIViI3nbelGgifgS qXwwWwIbkAehF62pJ1qHMsiKlB+j7Hd4NHMwz5a6oC37euio0RuYch5IbwmdZONq uZZlWj46Z0xiUuRRj/vBAoIBAQD75cGpbBFywn9iKd5ZkRGaa9qx0U0+HgQ8fhnE AXC4mCdx2adO/uxhZtPWrpeHuj5C2cTR1GSWeQ10cEH+y4fr64iiXnXISfPiEH/r +j/K3TfwntpiSvE90BUz6E3bViAuxAodwXBJvDTQ/l352v80AjeD8iv5aF8go51V vQz+G5hmP8dzHR2Et0ehyaTCiem9lQbHkdFLsnwy10SpCJgYfT4nXog3aWj07Rr9 b6miYNryCuaAblYCNGpIeOv+qMr5hxMxwS23Xhh7VkqlSz68qWEL6SJiZBCKtqOS m+IDvr4jBbfaQ3TrixgnQisPoYXv/z5uJYb7ihgoT6Uzn8V1AoIBAQD3Gt/MEeKf 8m3F7/aKxrthxrqKYxW0D/wx3VGSrveS6EnfW6ifmiMydV1NV2kA3B7wWmd6xTFY LfkdVg7Oi7ph010zG3wVxRVssCBNeV2vhG2qkKpEUhtLi+acFj9HvzHeSygcKzen o1UxE27Nzk+bhNf8Fmf1/7/UoeKyOWULE5wIklQ+9xooKcJ53VuMAHyjR4uxLmYc Nf7Q3+cdLFiY1b8I6T1PdRH7bkfvs45h7VIWOChpNOoVPDJMBEIItR2e0f1mlUpl z+WFK/VR68fQ2scqJCxD22Ui3F/WTYBBdxwqY4Tbd96aUmP2urMaB6/i2/pZeUcx Vn0fiOa1PwoxAoIBADViNs2yAmygvaBPITk4HlPsoZdntQgCEoHDc7BvYbUtQcbG CsgaDHyD70cjDygLl2BRiH2zlnGxS+GuXL4j4jVkYDuQ60M8MPxq5MFc8qIKie1r rPqByWiBLc0nYUCnmwBuOXqe4S4vPb5A+ieWetlJ0vwamaksrmRbaF+gRh2gOYcJ 4zoJJJVYxkyKUGmOEsRDzgEDbSiutdWMe5ebI6ik+kQbq6CarUyi50JopLmt7xi2 qKz1NTMYaqHbRqBco0+Iic/Ukdy3i1awLfej37LZ7qA4kzno3PyYwkey045ZoTAI 6TLPcvrsKn0/b6LLZ3g6Tr/HIjkyxfXdEzTCmnUCggEAZ3Q66kc6qFhpGQvEHonh fagkBThCp+ZhYccVFeJnCHx0IS1QxbFUtxVoAK9t6Mw/r8VJuZ7Bb/efambTQCpD 2B0T0gfZxYuD0sNSYt1DGe7JszVp87ykbNafsA2oZLNpf3Xbzx9Q58B8NFW8eDG+ JpBRlNsUn2t5tt4n+RIKeb61/ui0mL//lX0WTMsePtkdVYbotz+DxJ/elTiInDAq z6H9nw93ecK7ypZ7S6HTJLClQ2QzlwhuUIGpVSYbN2YMhqfH/aDXSxTlNQIYbTnX qFtQMxZ96dL63sOA5EoCPmZNxnlv8CqZaebAr1WvEmDRhJswjzE1WzSoogFBBfTk oQKCAQB7L0lMAtf9W9hyFMqEHvCah8eG7JuX2m2JpCQ5e5xkw/cswGwOX3+JxOYi joTmVSatkHoeKzBtNUCAkHTjXggIi6bKCXe3Lfon35+/QgAV9c5sNjj2m0K6Jw8A VR2gN2/VZG+DVaPSrTnglrGvVhs7gSixaHRtvQh+cP3P1LZnYEoOupIVokau54SU Ur4aIMKo01+r7L38jyvWlPpMqJg8Ev3qopDh0phs+n1n79IJmoswUhGkoKx+tXi2 1W1/OXfTiU6EoHo/iZRcFYFteWq1pWOvReKI/cT1epM6jyo+GwhOc4Uy0lrSVPZ/ zuB7aTwj2sz0b8VigqwV8lYW0ZUh -----END PRIVATE KEY----- hypercorn-0.14.4/tests/asyncio/000077500000000000000000000000001445231714500164265ustar00rootroot00000000000000hypercorn-0.14.4/tests/asyncio/__init__.py000066400000000000000000000000001445231714500205250ustar00rootroot00000000000000hypercorn-0.14.4/tests/asyncio/helpers.py000066400000000000000000000031461445231714500204460ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Optional, Union from ..helpers import MockSocket class MockSSLObject: def selected_alpn_protocol(self) -> str: return "h2" class MemoryReader: def __init__(self) -> None: self.data: asyncio.Queue = asyncio.Queue() self.eof = False async def send(self, data: bytes) -> None: if data != b"": await self.data.put(data) async def read(self, length: int) -> bytes: return await self.data.get() def close(self) -> None: self.data.put_nowait(b"") self.eof = True def at_eof(self) -> bool: return self.eof and self.data.empty() class MemoryWriter: def __init__(self, http2: bool = False) -> None: self.is_closed = False self.data: asyncio.Queue = asyncio.Queue() self.http2 = http2 def get_extra_info(self, name: str) -> Optional[Union[MockSocket, MockSSLObject]]: if name == "socket": return MockSocket() elif self.http2 and name == "ssl_object": return MockSSLObject() else: return None def write_eof(self) -> None: self.data.put_nowait(b"") def write(self, data: bytes) -> None: if self.is_closed: raise ConnectionError() self.data.put_nowait(data) async def drain(self) -> None: pass def close(self) -> None: self.is_closed = True self.data.put_nowait(b"") async def wait_closed(self) -> None: pass async def receive(self) -> bytes: return await self.data.get() hypercorn-0.14.4/tests/asyncio/test_keep_alive.py000066400000000000000000000075301445231714500221500ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import AsyncGenerator import h11 import pytest import pytest_asyncio from hypercorn.app_wrappers import ASGIWrapper from hypercorn.asyncio.tcp_server import TCPServer from hypercorn.asyncio.worker_context import WorkerContext from hypercorn.config import Config from hypercorn.typing import ASGIReceiveCallable, ASGISendCallable, Scope from .helpers import MemoryReader, MemoryWriter KEEP_ALIVE_TIMEOUT = 0.01 REQUEST = h11.Request(method="GET", target="/", headers=[(b"host", b"hypercorn")]) async def slow_framework( scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: while True: event = await receive() if event["type"] == "http.disconnect": break elif event["type"] == "lifespan.startup": await send({"type": "lifspan.startup.complete"}) # type: ignore elif event["type"] == "lifespan.shutdown": await send({"type": "lifspan.shutdown.complete"}) # type: ignore elif event["type"] == "http.request" and not event.get("more_body", False): await asyncio.sleep(2 * KEEP_ALIVE_TIMEOUT) await send( { "type": "http.response.start", "status": 200, "headers": [(b"content-length", b"0")], } ) await send({"type": "http.response.body", "body": b"", "more_body": False}) break @pytest_asyncio.fixture(name="server", scope="function") # type: ignore[misc] async def _server(event_loop: asyncio.AbstractEventLoop) -> AsyncGenerator[TCPServer, None]: config = Config() config.keep_alive_timeout = KEEP_ALIVE_TIMEOUT server = TCPServer( ASGIWrapper(slow_framework), event_loop, config, WorkerContext(), MemoryReader(), # type: ignore MemoryWriter(), # type: ignore ) task = event_loop.create_task(server.run()) yield server server.reader.close() # type: ignore await task @pytest.mark.asyncio async def test_http1_keep_alive_pre_request(server: TCPServer) -> None: await server.reader.send(b"GET") # type: ignore await asyncio.sleep(2 * KEEP_ALIVE_TIMEOUT) assert server.writer.is_closed # type: ignore @pytest.mark.asyncio async def test_http1_keep_alive_during(server: TCPServer) -> None: client = h11.Connection(h11.CLIENT) await server.reader.send(client.send(REQUEST)) # type: ignore await server.reader.send(client.send(h11.EndOfMessage())) # type: ignore await asyncio.sleep(2 * KEEP_ALIVE_TIMEOUT) assert not server.writer.is_closed # type: ignore @pytest.mark.asyncio async def test_http1_keep_alive(server: TCPServer) -> None: client = h11.Connection(h11.CLIENT) await server.reader.send(client.send(REQUEST)) # type: ignore await asyncio.sleep(2 * KEEP_ALIVE_TIMEOUT) assert not server.writer.is_closed # type: ignore await server.reader.send(client.send(h11.EndOfMessage())) # type: ignore while True: event = client.next_event() if event == h11.NEED_DATA: data = await server.writer.receive() # type: ignore client.receive_data(data) elif isinstance(event, h11.EndOfMessage): break client.start_next_cycle() await server.reader.send(client.send(REQUEST)) # type: ignore await asyncio.sleep(2 * KEEP_ALIVE_TIMEOUT) assert not server.writer.is_closed # type: ignore @pytest.mark.asyncio async def test_http1_keep_alive_pipelining(server: TCPServer) -> None: await server.reader.send( # type: ignore b"GET / HTTP/1.1\r\nHost: hypercorn\r\n\r\nGET / HTTP/1.1\r\nHost: hypercorn\r\n\r\n" ) await server.writer.receive() # type: ignore await asyncio.sleep(2 * KEEP_ALIVE_TIMEOUT) assert not server.writer.is_closed # type: ignore hypercorn-0.14.4/tests/asyncio/test_lifespan.py000066400000000000000000000046101445231714500216410ustar00rootroot00000000000000from __future__ import annotations import asyncio from time import sleep from typing import Callable import pytest from hypercorn.app_wrappers import ASGIWrapper from hypercorn.asyncio.lifespan import Lifespan from hypercorn.config import Config from hypercorn.typing import Scope from hypercorn.utils import LifespanFailureError, LifespanTimeoutError from ..helpers import lifespan_failure, SlowLifespanFramework async def no_lifespan_app(scope: Scope, receive: Callable, send: Callable) -> None: sleep(0.1) # Block purposefully raise Exception() @pytest.mark.asyncio async def test_ensure_no_race_condition(event_loop: asyncio.AbstractEventLoop) -> None: config = Config() config.startup_timeout = 0.2 lifespan = Lifespan(ASGIWrapper(no_lifespan_app), config, event_loop) task = event_loop.create_task(lifespan.handle_lifespan()) await lifespan.wait_for_startup() # Raises if there is a race condition await task @pytest.mark.asyncio async def test_startup_timeout_error(event_loop: asyncio.AbstractEventLoop) -> None: config = Config() config.startup_timeout = 0.01 lifespan = Lifespan(ASGIWrapper(SlowLifespanFramework(0.02, asyncio.sleep)), config, event_loop) task = event_loop.create_task(lifespan.handle_lifespan()) with pytest.raises(LifespanTimeoutError) as exc_info: await lifespan.wait_for_startup() assert str(exc_info.value).startswith("Timeout whilst awaiting startup") await task @pytest.mark.asyncio async def test_startup_failure(event_loop: asyncio.AbstractEventLoop) -> None: lifespan = Lifespan(ASGIWrapper(lifespan_failure), Config(), event_loop) lifespan_task = event_loop.create_task(lifespan.handle_lifespan()) await lifespan.wait_for_startup() assert lifespan_task.done() exception = lifespan_task.exception() assert isinstance(exception, LifespanFailureError) assert str(exception) == "Lifespan failure in startup. 'Failure'" async def return_app(scope: Scope, receive: Callable, send: Callable) -> None: return @pytest.mark.asyncio async def test_lifespan_return(event_loop: asyncio.AbstractEventLoop) -> None: lifespan = Lifespan(ASGIWrapper(return_app), Config(), event_loop) lifespan_task = event_loop.create_task(lifespan.handle_lifespan()) await lifespan.wait_for_startup() await lifespan.wait_for_shutdown() # Should complete (not hang) assert lifespan_task.done() hypercorn-0.14.4/tests/asyncio/test_sanity.py000066400000000000000000000203101445231714500213420ustar00rootroot00000000000000from __future__ import annotations import asyncio import h2 import h11 import pytest import wsproto from hypercorn.app_wrappers import ASGIWrapper from hypercorn.asyncio.tcp_server import TCPServer from hypercorn.asyncio.worker_context import WorkerContext from hypercorn.config import Config from .helpers import MemoryReader, MemoryWriter from ..helpers import SANITY_BODY, sanity_framework @pytest.mark.asyncio async def test_http1_request(event_loop: asyncio.AbstractEventLoop) -> None: server = TCPServer( ASGIWrapper(sanity_framework), event_loop, Config(), WorkerContext(), MemoryReader(), # type: ignore MemoryWriter(), # type: ignore ) task = event_loop.create_task(server.run()) client = h11.Connection(h11.CLIENT) await server.reader.send( # type: ignore client.send( h11.Request( method="POST", target="/", headers=[ (b"host", b"hypercorn"), (b"connection", b"close"), (b"content-length", b"%d" % len(SANITY_BODY)), ], ) ) ) await server.reader.send(client.send(h11.Data(data=SANITY_BODY))) # type: ignore await server.reader.send(client.send(h11.EndOfMessage())) # type: ignore events = [] while True: event = client.next_event() if event == h11.NEED_DATA: data = await server.writer.receive() # type: ignore client.receive_data(data) elif isinstance(event, h11.ConnectionClosed): break else: events.append(event) assert events == [ h11.Response( status_code=200, headers=[ (b"content-length", b"15"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h11"), (b"connection", b"close"), ], http_version=b"1.1", reason=b"", ), h11.Data(data=b"Hello & Goodbye"), h11.EndOfMessage(headers=[]), # type: ignore ] server.reader.close() # type: ignore await task @pytest.mark.asyncio async def test_http1_websocket(event_loop: asyncio.AbstractEventLoop) -> None: server = TCPServer( ASGIWrapper(sanity_framework), event_loop, Config(), WorkerContext(), MemoryReader(), # type: ignore MemoryWriter(), # type: ignore ) task = event_loop.create_task(server.run()) client = wsproto.WSConnection(wsproto.ConnectionType.CLIENT) await server.reader.send( # type: ignore client.send(wsproto.events.Request(host="hypercorn", target="/")) ) client.receive_data(await server.writer.receive()) # type: ignore assert list(client.events()) == [ wsproto.events.AcceptConnection( extra_headers=[ (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h11"), ] ) ] await server.reader.send( # type: ignore client.send(wsproto.events.BytesMessage(data=SANITY_BODY)) ) client.receive_data(await server.writer.receive()) # type: ignore assert list(client.events()) == [wsproto.events.TextMessage(data="Hello & Goodbye")] await server.reader.send(client.send(wsproto.events.CloseConnection(code=1000))) # type: ignore client.receive_data(await server.writer.receive()) # type: ignore assert list(client.events()) == [wsproto.events.CloseConnection(code=1000, reason="")] assert server.writer.is_closed # type: ignore server.reader.close() # type: ignore await task @pytest.mark.asyncio async def test_http2_request(event_loop: asyncio.AbstractEventLoop) -> None: server = TCPServer( ASGIWrapper(sanity_framework), event_loop, Config(), WorkerContext(), MemoryReader(), # type: ignore MemoryWriter(http2=True), # type: ignore ) task = event_loop.create_task(server.run()) client = h2.connection.H2Connection() client.initiate_connection() await server.reader.send(client.data_to_send()) # type: ignore stream_id = client.get_next_available_stream_id() client.send_headers( stream_id, [ (":method", "POST"), (":path", "/"), (":authority", "hypercorn"), (":scheme", "https"), ("content-length", "%d" % len(SANITY_BODY)), ], ) client.send_data(stream_id, SANITY_BODY) client.end_stream(stream_id) await server.reader.send(client.data_to_send()) # type: ignore events = [] open_ = True while open_: data = await server.writer.receive() # type: ignore if data == b"": open_ = False h2_events = client.receive_data(data) for event in h2_events: if isinstance(event, h2.events.DataReceived): client.acknowledge_received_data(event.flow_controlled_length, event.stream_id) elif isinstance( event, (h2.events.ConnectionTerminated, h2.events.StreamEnded, h2.events.StreamReset), ): open_ = False break else: events.append(event) await server.reader.send(client.data_to_send()) # type: ignore assert isinstance(events[2], h2.events.ResponseReceived) assert events[2].headers == [ (b":status", b"200"), (b"content-length", b"15"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h2"), ] client.close_connection() await server.reader.send(client.data_to_send()) # type: ignore await server.writer.receive() # type: ignore assert server.writer.is_closed # type: ignore server.reader.close() # type: ignore await task @pytest.mark.asyncio async def test_http2_websocket(event_loop: asyncio.AbstractEventLoop) -> None: server = TCPServer( ASGIWrapper(sanity_framework), event_loop, Config(), WorkerContext(), MemoryReader(), # type: ignore MemoryWriter(http2=True), # type: ignore ) task = event_loop.create_task(server.run()) h2_client = h2.connection.H2Connection() h2_client.initiate_connection() await server.reader.send(h2_client.data_to_send()) # type: ignore stream_id = h2_client.get_next_available_stream_id() h2_client.send_headers( stream_id, [ (":method", "CONNECT"), (":path", "/"), (":authority", "hypercorn"), (":scheme", "https"), ("sec-websocket-version", "13"), ], ) await server.reader.send(h2_client.data_to_send()) # type: ignore events = h2_client.receive_data(await server.writer.receive()) # type: ignore await server.reader.send(h2_client.data_to_send()) # type: ignore events = h2_client.receive_data(await server.writer.receive()) # type: ignore events = h2_client.receive_data(await server.writer.receive()) # type: ignore assert isinstance(events[0], h2.events.ResponseReceived) assert events[0].headers == [ (b":status", b"200"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h2"), ] client = wsproto.connection.Connection(wsproto.ConnectionType.CLIENT) h2_client.send_data(stream_id, client.send(wsproto.events.BytesMessage(data=SANITY_BODY))) await server.reader.send(h2_client.data_to_send()) # type: ignore events = h2_client.receive_data(await server.writer.receive()) # type: ignore client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.TextMessage(data="Hello & Goodbye")] h2_client.send_data(stream_id, client.send(wsproto.events.CloseConnection(code=1000))) await server.reader.send(h2_client.data_to_send()) # type: ignore events = h2_client.receive_data(await server.writer.receive()) # type: ignore client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.CloseConnection(code=1000, reason="")] h2_client.close_connection() await server.reader.send(h2_client.data_to_send()) # type: ignore server.reader.close() # type: ignore await task hypercorn-0.14.4/tests/asyncio/test_task_group.py000066400000000000000000000037431445231714500222240ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Callable import pytest from hypercorn.app_wrappers import ASGIWrapper from hypercorn.asyncio.task_group import TaskGroup from hypercorn.config import Config from hypercorn.typing import HTTPScope, Scope @pytest.mark.asyncio async def test_spawn_app(event_loop: asyncio.AbstractEventLoop, http_scope: HTTPScope) -> None: async def _echo_app(scope: Scope, receive: Callable, send: Callable) -> None: while True: message = await receive() if message is None: return await send(message) app_queue: asyncio.Queue = asyncio.Queue() async with TaskGroup(event_loop) as task_group: put = await task_group.spawn_app( ASGIWrapper(_echo_app), Config(), http_scope, app_queue.put ) await put({"type": "http.disconnect"}) assert (await app_queue.get()) == {"type": "http.disconnect"} await put(None) @pytest.mark.asyncio async def test_spawn_app_error( event_loop: asyncio.AbstractEventLoop, http_scope: HTTPScope ) -> None: async def _error_app(scope: Scope, receive: Callable, send: Callable) -> None: raise Exception() app_queue: asyncio.Queue = asyncio.Queue() async with TaskGroup(event_loop) as task_group: await task_group.spawn_app(ASGIWrapper(_error_app), Config(), http_scope, app_queue.put) assert (await app_queue.get()) is None @pytest.mark.asyncio async def test_spawn_app_cancelled( event_loop: asyncio.AbstractEventLoop, http_scope: HTTPScope ) -> None: async def _error_app(scope: Scope, receive: Callable, send: Callable) -> None: raise asyncio.CancelledError() app_queue: asyncio.Queue = asyncio.Queue() with pytest.raises(asyncio.CancelledError): async with TaskGroup(event_loop) as task_group: await task_group.spawn_app(ASGIWrapper(_error_app), Config(), http_scope, app_queue.put) assert (await app_queue.get()) is None hypercorn-0.14.4/tests/asyncio/test_tcp_server.py000066400000000000000000000030151445231714500222120ustar00rootroot00000000000000from __future__ import annotations import asyncio import pytest from hypercorn.app_wrappers import ASGIWrapper from hypercorn.asyncio.tcp_server import TCPServer from hypercorn.asyncio.worker_context import WorkerContext from hypercorn.config import Config from .helpers import MemoryReader, MemoryWriter from ..helpers import echo_framework @pytest.mark.asyncio async def test_completes_on_closed(event_loop: asyncio.AbstractEventLoop) -> None: server = TCPServer( ASGIWrapper(echo_framework), event_loop, Config(), WorkerContext(), MemoryReader(), # type: ignore MemoryWriter(), # type: ignore ) server.reader.close() # type: ignore await server.run() # Key is that this line is reached, rather than the above line # hanging. @pytest.mark.asyncio async def test_complets_on_half_close(event_loop: asyncio.AbstractEventLoop) -> None: server = TCPServer( ASGIWrapper(echo_framework), event_loop, Config(), WorkerContext(), MemoryReader(), # type: ignore MemoryWriter(), # type: ignore ) task = event_loop.create_task(server.run()) await server.reader.send(b"GET / HTTP/1.1\r\nHost: hypercorn\r\n\r\n") # type: ignore server.reader.close() # type: ignore await task data = await server.writer.receive() # type: ignore assert ( data == b"HTTP/1.1 200 \r\ncontent-length: 335\r\ndate: Thu, 01 Jan 1970 01:23:20 GMT\r\nserver: hypercorn-h11\r\n\r\n" # noqa: E501 ) hypercorn-0.14.4/tests/conftest.py000066400000000000000000000015361445231714500171650ustar00rootroot00000000000000from __future__ import annotations import pytest from _pytest.monkeypatch import MonkeyPatch import hypercorn.config from hypercorn.typing import HTTPScope @pytest.fixture(autouse=True) def _time(monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr(hypercorn.config, "time", lambda: 5000) @pytest.fixture(name="http_scope") def _http_scope() -> HTTPScope: return { "type": "http", "asgi": {}, "http_version": "2", "method": "GET", "scheme": "https", "path": "/", "raw_path": b"/", "query_string": b"a=b", "root_path": "", "headers": [ (b"User-Agent", b"Hypercorn"), (b"X-Hypercorn", b"Hypercorn"), (b"Referer", b"hypercorn"), ], "client": ("127.0.0.1", 80), "server": None, "extensions": {}, } hypercorn-0.14.4/tests/helpers.py000066400000000000000000000102671445231714500170030ustar00rootroot00000000000000from __future__ import annotations from copy import deepcopy from json import dumps from socket import AF_INET from typing import Callable, cast, Tuple from hypercorn.typing import ASGIReceiveCallable, ASGISendCallable, Scope, WWWScope SANITY_BODY = b"Hello Hypercorn" class MockSocket: family = AF_INET def getsockname(self) -> Tuple[str, int]: return ("162.1.1.1", 80) def getpeername(self) -> Tuple[str, int]: return ("127.0.0.1", 80) async def empty_framework(scope: Scope, receive: Callable, send: Callable) -> None: pass class SlowLifespanFramework: def __init__(self, delay: float, sleep: Callable) -> None: self.delay = delay self.sleep = sleep async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: await self.sleep(self.delay) async def echo_framework( input_scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: input_scope = cast(WWWScope, input_scope) scope = deepcopy(input_scope) scope["query_string"] = scope["query_string"].decode() # type: ignore scope["raw_path"] = scope["raw_path"].decode() # type: ignore scope["headers"] = [ (name.decode(), value.decode()) for name, value in scope["headers"] # type: ignore ] body = bytearray() while True: event = await receive() if event["type"] in {"http.disconnect", "websocket.disconnect"}: break elif event["type"] == "http.request": body.extend(event.get("body", b"")) if not event.get("more_body", False): response = dumps({"scope": scope, "request_body": body.decode()}).encode() content_length = len(response) await send( { "type": "http.response.start", "status": 200, "headers": [(b"content-length", str(content_length).encode())], } ) await send({"type": "http.response.body", "body": response, "more_body": False}) break elif event["type"] == "websocket.connect": await send({"type": "websocket.accept"}) # type: ignore elif event["type"] == "websocket.receive": await send({"type": "websocket.send", "text": event["text"], "bytes": event["bytes"]}) async def lifespan_failure( scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: while True: message = await receive() if message["type"] == "lifespan.startup": await send({"type": "lifespan.startup.failed", "message": "Failure"}) break async def sanity_framework( scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: body = b"" if scope["type"] == "websocket": await send({"type": "websocket.accept"}) # type: ignore while True: event = await receive() if event["type"] in {"http.disconnect", "websocket.disconnect"}: break elif event["type"] == "lifespan.startup": await send({"type": "lifspan.startup.complete"}) # type: ignore elif event["type"] == "lifespan.shutdown": await send({"type": "lifspan.shutdown.complete"}) # type: ignore elif event["type"] == "http.request" and event.get("more_body", False): body += event["body"] elif event["type"] == "http.request" and not event.get("more_body", False): body += event["body"] assert body == SANITY_BODY response = b"Hello & Goodbye" content_length = len(response) await send( { "type": "http.response.start", "status": 200, "headers": [(b"content-length", str(content_length).encode())], } ) await send({"type": "http.response.body", "body": response, "more_body": False}) break elif event["type"] == "websocket.receive": assert event["bytes"] == SANITY_BODY await send({"type": "websocket.send", "text": "Hello & Goodbye"}) # type: ignore hypercorn-0.14.4/tests/middleware/000077500000000000000000000000001445231714500170765ustar00rootroot00000000000000hypercorn-0.14.4/tests/middleware/__init__.py000066400000000000000000000000001445231714500211750ustar00rootroot00000000000000hypercorn-0.14.4/tests/middleware/test_dispatcher.py000066400000000000000000000062201445231714500226350ustar00rootroot00000000000000from __future__ import annotations from typing import Callable, cast import pytest from hypercorn.middleware.dispatcher import AsyncioDispatcherMiddleware, TrioDispatcherMiddleware from hypercorn.typing import HTTPScope, Scope @pytest.mark.asyncio async def test_dispatcher_middleware(http_scope: HTTPScope) -> None: class EchoFramework: def __init__(self, name: str) -> None: self.name = name async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> None: scope = cast(HTTPScope, scope) response = f"{self.name}-{scope['path']}" await send( { "type": "http.response.start", "status": 200, "headers": [(b"content-length", b"%d" % len(response))], } ) await send({"type": "http.response.body", "body": response.encode()}) app = AsyncioDispatcherMiddleware( {"/api/x": EchoFramework("apix"), "/api": EchoFramework("api")} ) sent_events = [] async def send(message: dict) -> None: nonlocal sent_events sent_events.append(message) await app({**http_scope, **{"path": "/api/x/b"}}, None, send) # type: ignore await app({**http_scope, **{"path": "/api/b"}}, None, send) # type: ignore await app({**http_scope, **{"path": "/"}}, None, send) # type: ignore assert sent_events == [ {"type": "http.response.start", "status": 200, "headers": [(b"content-length", b"7")]}, {"type": "http.response.body", "body": b"apix-/b"}, {"type": "http.response.start", "status": 200, "headers": [(b"content-length", b"6")]}, {"type": "http.response.body", "body": b"api-/b"}, {"type": "http.response.start", "status": 404, "headers": [(b"content-length", b"0")]}, {"type": "http.response.body"}, ] class ScopeFramework: def __init__(self, name: str) -> None: self.name = name async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> None: await send({"type": "lifespan.startup.complete"}) @pytest.mark.asyncio async def test_asyncio_dispatcher_lifespan() -> None: app = AsyncioDispatcherMiddleware( {"/apix": ScopeFramework("apix"), "/api": ScopeFramework("api")} ) sent_events = [] async def send(message: dict) -> None: nonlocal sent_events sent_events.append(message) async def receive() -> dict: return {"type": "lifespan.shutdown"} await app({"type": "lifespan", "asgi": {"version": "3.0"}}, receive, send) assert sent_events == [{"type": "lifespan.startup.complete"}] @pytest.mark.trio async def test_trio_dispatcher_lifespan() -> None: app = TrioDispatcherMiddleware({"/apix": ScopeFramework("apix"), "/api": ScopeFramework("api")}) sent_events = [] async def send(message: dict) -> None: nonlocal sent_events sent_events.append(message) async def receive() -> dict: return {"type": "lifespan.shutdown"} await app({"type": "lifespan", "asgi": {"version": "3.0"}}, receive, send) assert sent_events == [{"type": "lifespan.startup.complete"}] hypercorn-0.14.4/tests/middleware/test_http_to_https.py000066400000000000000000000111541445231714500234140ustar00rootroot00000000000000from __future__ import annotations import pytest from hypercorn.middleware import HTTPToHTTPSRedirectMiddleware from hypercorn.typing import HTTPScope, WebsocketScope from ..helpers import empty_framework @pytest.mark.asyncio @pytest.mark.parametrize("raw_path", [b"/abc", b"/abc%3C"]) async def test_http_to_https_redirect_middleware_http(raw_path: bytes) -> None: app = HTTPToHTTPSRedirectMiddleware(empty_framework, "localhost") sent_events = [] async def send(message: dict) -> None: nonlocal sent_events sent_events.append(message) scope: HTTPScope = { "type": "http", "asgi": {}, "http_version": "2", "method": "GET", "scheme": "http", "path": raw_path.decode(), "raw_path": raw_path, "query_string": b"a=b", "root_path": "", "headers": [], "client": ("127.0.0.1", 80), "server": None, "extensions": {}, } await app(scope, None, send) assert sent_events == [ { "type": "http.response.start", "status": 307, "headers": [(b"location", b"https://localhost%s?a=b" % raw_path)], }, {"type": "http.response.body"}, ] @pytest.mark.asyncio @pytest.mark.parametrize("raw_path", [b"/abc", b"/abc%3C"]) async def test_http_to_https_redirect_middleware_websocket(raw_path: bytes) -> None: app = HTTPToHTTPSRedirectMiddleware(empty_framework, "localhost") sent_events = [] async def send(message: dict) -> None: nonlocal sent_events sent_events.append(message) scope: WebsocketScope = { "type": "websocket", "asgi": {}, "http_version": "1.1", "scheme": "ws", "path": raw_path.decode(), "raw_path": raw_path, "query_string": b"a=b", "root_path": "", "headers": [], "client": None, "server": None, "subprotocols": [], "extensions": {"websocket.http.response": {}}, } await app(scope, None, send) assert sent_events == [ { "type": "websocket.http.response.start", "status": 307, "headers": [(b"location", b"wss://localhost%s?a=b" % raw_path)], }, {"type": "websocket.http.response.body"}, ] @pytest.mark.asyncio async def test_http_to_https_redirect_middleware_websocket_http2() -> None: app = HTTPToHTTPSRedirectMiddleware(empty_framework, "localhost") sent_events = [] async def send(message: dict) -> None: nonlocal sent_events sent_events.append(message) scope: WebsocketScope = { "type": "websocket", "asgi": {}, "http_version": "2", "scheme": "ws", "path": "/abc", "raw_path": b"/abc", "query_string": b"a=b", "root_path": "", "headers": [], "client": None, "server": None, "subprotocols": [], "extensions": {"websocket.http.response": {}}, } await app(scope, None, send) assert sent_events == [ { "type": "websocket.http.response.start", "status": 307, "headers": [(b"location", b"https://localhost/abc?a=b")], }, {"type": "websocket.http.response.body"}, ] @pytest.mark.asyncio async def test_http_to_https_redirect_middleware_websocket_no_rejection() -> None: app = HTTPToHTTPSRedirectMiddleware(empty_framework, "localhost") sent_events = [] async def send(message: dict) -> None: nonlocal sent_events sent_events.append(message) scope: WebsocketScope = { "type": "websocket", "asgi": {}, "http_version": "2", "scheme": "ws", "path": "/abc", "raw_path": b"/abc", "query_string": b"a=b", "root_path": "", "headers": [], "client": None, "server": None, "subprotocols": [], "extensions": {}, } await app(scope, None, send) assert sent_events == [{"type": "websocket.close"}] def test_http_to_https_redirect_new_url_header() -> None: app = HTTPToHTTPSRedirectMiddleware(empty_framework, None) new_url = app._new_url( "https", { "http_version": "1.1", "asgi": {}, "method": "GET", "headers": [(b"host", b"localhost")], "path": "/", "root_path": "", "query_string": b"", "raw_path": b"/", "scheme": "http", "type": "http", "client": None, "server": None, "extensions": {}, }, ) assert new_url == "https://localhost/" hypercorn-0.14.4/tests/protocol/000077500000000000000000000000001445231714500166225ustar00rootroot00000000000000hypercorn-0.14.4/tests/protocol/test_h11.py000077500000000000000000000335571445231714500206440ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Any from unittest.mock import call, Mock import h11 import pytest import pytest_asyncio from _pytest.monkeypatch import MonkeyPatch import hypercorn.protocol.h11 from hypercorn.asyncio.worker_context import EventWrapper from hypercorn.config import Config from hypercorn.events import Closed, RawData, Updated from hypercorn.protocol.events import Body, Data, EndBody, EndData, Request, Response, StreamClosed from hypercorn.protocol.h11 import H2CProtocolRequiredError, H2ProtocolAssumedError, H11Protocol from hypercorn.protocol.http_stream import HTTPStream from hypercorn.typing import Event as IOEvent try: from unittest.mock import AsyncMock except ImportError: # Python < 3.8 from mock import AsyncMock # type: ignore BASIC_HEADERS = [("Host", "hypercorn"), ("Connection", "close")] @pytest_asyncio.fixture(name="protocol") # type: ignore[misc] async def _protocol(monkeypatch: MonkeyPatch) -> H11Protocol: MockHTTPStream = Mock() # noqa: N806 MockHTTPStream.return_value = AsyncMock(spec=HTTPStream) monkeypatch.setattr(hypercorn.protocol.h11, "HTTPStream", MockHTTPStream) context = Mock() context.event_class.return_value = AsyncMock(spec=IOEvent) context.terminated = context.event_class() context.terminated.is_set.return_value = False return H11Protocol(AsyncMock(), Config(), context, AsyncMock(), False, None, None, AsyncMock()) @pytest.mark.asyncio async def test_protocol_send_response(protocol: H11Protocol) -> None: await protocol.stream_send(Response(stream_id=1, status_code=201, headers=[])) protocol.send.assert_called() # type: ignore assert protocol.send.call_args_list == [ # type: ignore call( RawData( data=( b"HTTP/1.1 201 \r\ndate: Thu, 01 Jan 1970 01:23:20 GMT\r\n" b"server: hypercorn-h11\r\nConnection: close\r\n\r\n" ) ) ) ] @pytest.mark.asyncio async def test_protocol_preserve_headers(protocol: H11Protocol) -> None: await protocol.stream_send( Response(stream_id=1, status_code=201, headers=[(b"X-Special", b"Value")]) ) protocol.send.assert_called() # type: ignore assert protocol.send.call_args_list == [ # type: ignore call( RawData( data=( b"HTTP/1.1 201 \r\nX-Special: Value\r\n" b"date: Thu, 01 Jan 1970 01:23:20 GMT\r\n" b"server: hypercorn-h11\r\nConnection: close\r\n\r\n" ) ) ) ] @pytest.mark.asyncio async def test_protocol_send_data(protocol: H11Protocol) -> None: await protocol.stream_send(Data(stream_id=1, data=b"hello")) protocol.send.assert_called() # type: ignore assert protocol.send.call_args_list == [call(RawData(data=b"hello"))] # type: ignore @pytest.mark.asyncio async def test_protocol_send_body(protocol: H11Protocol) -> None: await protocol.handle( RawData(data=b"GET / HTTP/1.1\r\nHost: hypercorn\r\nConnection: close\r\n\r\n") ) await protocol.stream_send( Response(stream_id=1, status_code=200, headers=[(b"content-length", b"5")]) ) await protocol.stream_send(Body(stream_id=1, data=b"hello")) protocol.send.assert_called() # type: ignore assert protocol.send.call_args_list == [ # type: ignore call(Updated(idle=False)), call( RawData( data=b"HTTP/1.1 200 \r\ncontent-length: 5\r\ndate: Thu, 01 Jan 1970 01:23:20 GMT\r\nserver: hypercorn-h11\r\nConnection: close\r\n\r\n" # noqa: E501 ) ), call(RawData(data=b"hello")), ] @pytest.mark.asyncio @pytest.mark.parametrize("keep_alive, expected", [(True, Updated(idle=True)), (False, Closed())]) async def test_protocol_send_stream_closed( keep_alive: bool, expected: Any, protocol: H11Protocol ) -> None: data = b"GET / HTTP/1.1\r\nHost: hypercorn\r\n" if keep_alive: data += b"\r\n" else: data += b"Connection: close\r\n\r\n" await protocol.handle(RawData(data=data)) await protocol.stream_send(Response(stream_id=1, status_code=200, headers=[])) await protocol.stream_send(EndBody(stream_id=1)) await protocol.stream_send(StreamClosed(stream_id=1)) protocol.send.assert_called() # type: ignore assert protocol.send.call_args_list[3] == call(expected) # type: ignore @pytest.mark.asyncio async def test_protocol_instant_recycle( protocol: H11Protocol, event_loop: asyncio.AbstractEventLoop ) -> None: # This test task acts as the asgi app, spawned tasks act as the # server. data = b"GET / HTTP/1.1\r\nHost: hypercorn\r\n\r\n" # This test requires a real event as the handling should pause on # the instant receipt protocol.can_read = EventWrapper() task = event_loop.create_task(protocol.handle(RawData(data=data))) await asyncio.sleep(0) # Switch to task assert protocol.stream is not None assert task.done() await protocol.stream_send(Response(stream_id=1, status_code=200, headers=[])) await protocol.stream_send(EndBody(stream_id=1)) task = event_loop.create_task(protocol.handle(RawData(data=data))) await asyncio.sleep(0) # Switch to task await protocol.stream_send(StreamClosed(stream_id=1)) await asyncio.sleep(0) # Switch to task # Should have recycled, i.e. a stream should exist assert protocol.stream is not None assert task.done() @pytest.mark.asyncio async def test_protocol_send_end_data(protocol: H11Protocol) -> None: protocol.stream = AsyncMock() await protocol.stream_send(EndData(stream_id=1)) assert protocol.stream is not None @pytest.mark.asyncio async def test_protocol_handle_closed(protocol: H11Protocol) -> None: await protocol.handle( RawData(data=b"GET / HTTP/1.1\r\nHost: hypercorn\r\nConnection: close\r\n\r\n") ) stream = protocol.stream await protocol.handle(Closed()) stream.handle.assert_called() # type: ignore assert stream.handle.call_args_list == [ # type: ignore call( Request( stream_id=1, headers=[(b"host", b"hypercorn"), (b"connection", b"close")], http_version="1.1", method="GET", raw_path=b"/", ) ), call(EndBody(stream_id=1)), call(StreamClosed(stream_id=1)), ] @pytest.mark.asyncio async def test_protocol_handle_request(protocol: H11Protocol) -> None: client = h11.Connection(h11.CLIENT) await protocol.handle( RawData(data=client.send(h11.Request(method="GET", target="/?a=b", headers=BASIC_HEADERS))) ) protocol.stream.handle.assert_called() # type: ignore assert protocol.stream.handle.call_args_list == [ # type: ignore call( Request( stream_id=1, headers=[(b"host", b"hypercorn"), (b"connection", b"close")], http_version="1.1", method="GET", raw_path=b"/?a=b", ) ), call(EndBody(stream_id=1)), ] @pytest.mark.asyncio async def test_protocol_handle_request_with_raw_headers(protocol: H11Protocol) -> None: protocol.config.h11_pass_raw_headers = True client = h11.Connection(h11.CLIENT) headers = BASIC_HEADERS + [("FOO_BAR", "foobar")] await protocol.handle( RawData(data=client.send(h11.Request(method="GET", target="/?a=b", headers=headers))) ) protocol.stream.handle.assert_called() # type: ignore assert protocol.stream.handle.call_args_list == [ # type: ignore call( Request( stream_id=1, headers=[ (b"Host", b"hypercorn"), (b"Connection", b"close"), (b"FOO_BAR", b"foobar"), ], http_version="1.1", method="GET", raw_path=b"/?a=b", ) ), call(EndBody(stream_id=1)), ] @pytest.mark.asyncio async def test_protocol_handle_protocol_error(protocol: H11Protocol) -> None: await protocol.handle(RawData(data=b"broken nonsense\r\n\r\n")) protocol.send.assert_called() # type: ignore assert protocol.send.call_args_list == [ # type: ignore call( RawData( data=b"HTTP/1.1 400 \r\ncontent-length: 0\r\nconnection: close\r\n" b"date: Thu, 01 Jan 1970 01:23:20 GMT\r\nserver: hypercorn-h11\r\n\r\n" ) ), call(RawData(data=b"")), call(Closed()), ] @pytest.mark.asyncio async def test_protocol_handle_send_client_error(protocol: H11Protocol) -> None: client = h11.Connection(h11.CLIENT) await protocol.handle( RawData(data=client.send(h11.Request(method="GET", target="/?a=b", headers=BASIC_HEADERS))) ) await protocol.handle(RawData(data=b"some body")) # This next line should not cause an error await protocol.stream_send(Response(stream_id=1, status_code=200, headers=[])) @pytest.mark.asyncio async def test_protocol_handle_pipelining(protocol: H11Protocol) -> None: protocol.can_read.wait.side_effect = Exception() # type: ignore with pytest.raises(Exception): await protocol.handle( RawData( data=b"GET / HTTP/1.1\r\nHost: hypercorn\r\nConnection: keep-alive\r\n\r\n" b"GET / HTTP/1.1\r\nHost: hypercorn\r\nConnection: close\r\n\r\n" ) ) protocol.can_read.clear.assert_called() # type: ignore protocol.can_read.wait.assert_called() # type: ignore @pytest.mark.asyncio async def test_protocol_handle_continue_request(protocol: H11Protocol) -> None: client = h11.Connection(h11.CLIENT) await protocol.handle( RawData( data=client.send( h11.Request( method="POST", target="/?a=b", headers=BASIC_HEADERS + [("transfer-encoding", "chunked"), ("expect", "100-continue")], ) ) ) ) assert protocol.send.call_args[0][0] == RawData( # type: ignore data=b"HTTP/1.1 100 \r\ndate: Thu, 01 Jan 1970 01:23:20 GMT\r\nserver: hypercorn-h11\r\n\r\n" # noqa: E501 ) @pytest.mark.asyncio async def test_protocol_handle_max_incomplete(monkeypatch: MonkeyPatch) -> None: config = Config() config.h11_max_incomplete_size = 5 MockHTTPStream = AsyncMock() # noqa: N806 MockHTTPStream.return_value = AsyncMock(spec=HTTPStream) monkeypatch.setattr(hypercorn.protocol.h11, "HTTPStream", MockHTTPStream) context = Mock() context.event_class.return_value = AsyncMock(spec=IOEvent) protocol = H11Protocol( AsyncMock(), config, context, AsyncMock(), False, None, None, AsyncMock() ) await protocol.handle(RawData(data=b"GET / HTTP/1.1\r\nHost: hypercorn\r\n")) protocol.send.assert_called() # type: ignore assert protocol.send.call_args_list == [ # type: ignore call( RawData( data=b"HTTP/1.1 400 \r\ncontent-length: 0\r\nconnection: close\r\n" b"date: Thu, 01 Jan 1970 01:23:20 GMT\r\nserver: hypercorn-h11\r\n\r\n" ) ), call(RawData(data=b"")), call(Closed()), ] @pytest.mark.asyncio async def test_protocol_handle_h2c_upgrade(protocol: H11Protocol) -> None: with pytest.raises(H2CProtocolRequiredError) as exc_info: await protocol.handle( RawData( data=( b"GET / HTTP/1.1\r\nHost: hypercorn\r\n" b"upgrade: h2c\r\nhttp2-settings: abcd\r\n\r\nbbb" ) ) ) assert protocol.send.call_args_list == [ # type: ignore call(Updated(idle=False)), call( RawData( b"HTTP/1.1 101 \r\n" b"date: Thu, 01 Jan 1970 01:23:20 GMT\r\n" b"server: hypercorn-h11\r\n" b"connection: upgrade\r\n" b"upgrade: h2c\r\n" b"\r\n" ) ), ] assert exc_info.value.data == b"bbb" assert exc_info.value.headers == [ (b":method", b"GET"), (b":path", b"/"), (b":authority", b"hypercorn"), (b"host", b"hypercorn"), (b"upgrade", b"h2c"), (b"http2-settings", b"abcd"), ] assert exc_info.value.settings == "abcd" @pytest.mark.asyncio async def test_protocol_handle_h2_prior(protocol: H11Protocol) -> None: with pytest.raises(H2ProtocolAssumedError) as exc_info: await protocol.handle(RawData(data=b"PRI * HTTP/2.0\r\n\r\nbbb")) assert exc_info.value.data == b"PRI * HTTP/2.0\r\n\r\nbbb" @pytest.mark.asyncio async def test_protocol_handle_data_post_response(protocol: H11Protocol) -> None: await protocol.handle( RawData(data=b"POST / HTTP/1.1\r\nHost: hypercorn\r\nContent-Length: 4\r\n\r\n") ) await protocol.stream_send(Response(stream_id=1, status_code=201, headers=[])) await protocol.stream_send(EndBody(stream_id=1)) await protocol.handle(RawData(data=b"abcd")) @pytest.mark.asyncio async def test_protocol_handle_data_post_end(protocol: H11Protocol) -> None: await protocol.handle( RawData(data=b"POST / HTTP/1.1\r\nHost: hypercorn\r\nContent-Length: 10\r\n\r\n") ) await protocol.stream_send(Response(stream_id=1, status_code=201, headers=[])) await protocol.stream_send(EndBody(stream_id=1)) # Key is that this doesn't error await protocol.handle(RawData(data=b"abcdefghij")) @pytest.mark.asyncio async def test_protocol_handle_data_post_close(protocol: H11Protocol) -> None: await protocol.handle( RawData(data=b"POST / HTTP/1.1\r\nHost: hypercorn\r\nContent-Length: 10\r\n\r\n") ) await protocol.stream_send(StreamClosed(stream_id=1)) assert protocol.stream is None # Key is that this doesn't error await protocol.handle(RawData(data=b"abcdefghij")) hypercorn-0.14.4/tests/protocol/test_h2.py000066400000000000000000000051221445231714500205440ustar00rootroot00000000000000from __future__ import annotations import asyncio from unittest.mock import call, Mock import pytest from hypercorn.asyncio.worker_context import EventWrapper, WorkerContext from hypercorn.config import Config from hypercorn.events import Closed, RawData from hypercorn.protocol.h2 import BUFFER_HIGH_WATER, BufferCompleteError, H2Protocol, StreamBuffer try: from unittest.mock import AsyncMock except ImportError: # Python < 3.8 from mock import AsyncMock # type: ignore @pytest.mark.asyncio async def test_stream_buffer_push_and_pop(event_loop: asyncio.AbstractEventLoop) -> None: stream_buffer = StreamBuffer(EventWrapper) async def _push_over_limit() -> bool: await stream_buffer.push(b"a" * (BUFFER_HIGH_WATER + 1)) return True task = event_loop.create_task(_push_over_limit()) assert not task.done() # Blocked as over high water await stream_buffer.pop(BUFFER_HIGH_WATER // 4) assert not task.done() # Blocked as over low water await stream_buffer.pop(BUFFER_HIGH_WATER // 4) assert (await task) is True @pytest.mark.asyncio async def test_stream_buffer_drain(event_loop: asyncio.AbstractEventLoop) -> None: stream_buffer = StreamBuffer(EventWrapper) await stream_buffer.push(b"a" * 10) async def _drain() -> bool: await stream_buffer.drain() return True task = event_loop.create_task(_drain()) assert not task.done() # Blocked await stream_buffer.pop(20) assert (await task) is True @pytest.mark.asyncio async def test_stream_buffer_closed(event_loop: asyncio.AbstractEventLoop) -> None: stream_buffer = StreamBuffer(EventWrapper) await stream_buffer.close() await stream_buffer._is_empty.wait() await stream_buffer._paused.wait() assert True with pytest.raises(BufferCompleteError): await stream_buffer.push(b"a") @pytest.mark.asyncio async def test_stream_buffer_complete(event_loop: asyncio.AbstractEventLoop) -> None: stream_buffer = StreamBuffer(EventWrapper) await stream_buffer.push(b"a" * 10) assert not stream_buffer.complete stream_buffer.set_complete() assert not stream_buffer.complete await stream_buffer.pop(20) assert stream_buffer.complete @pytest.mark.asyncio async def test_protocol_handle_protocol_error() -> None: protocol = H2Protocol( Mock(), Config(), WorkerContext(), AsyncMock(), False, None, None, AsyncMock() ) await protocol.handle(RawData(data=b"broken nonsense\r\n\r\n")) protocol.send.assert_awaited() # type: ignore assert protocol.send.call_args_list == [call(Closed())] # type: ignore hypercorn-0.14.4/tests/protocol/test_http_stream.py000066400000000000000000000223521445231714500225710ustar00rootroot00000000000000from __future__ import annotations from typing import Any, cast from unittest.mock import call import pytest import pytest_asyncio from hypercorn.asyncio.worker_context import WorkerContext from hypercorn.config import Config from hypercorn.logging import Logger from hypercorn.protocol.events import ( Body, EndBody, InformationalResponse, Request, Response, StreamClosed, ) from hypercorn.protocol.http_stream import ASGIHTTPState, HTTPStream from hypercorn.typing import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope from hypercorn.utils import UnexpectedMessageError try: from unittest.mock import AsyncMock except ImportError: # Python < 3.8 from mock import AsyncMock # type: ignore @pytest_asyncio.fixture(name="stream") # type: ignore[misc] async def _stream() -> HTTPStream: stream = HTTPStream( AsyncMock(), Config(), WorkerContext(), AsyncMock(), False, None, None, AsyncMock(), 1 ) stream.app_put = AsyncMock() stream.config._log = AsyncMock(spec=Logger) return stream @pytest.mark.parametrize("http_version", ["1.0", "1.1"]) @pytest.mark.asyncio async def test_handle_request_http_1(stream: HTTPStream, http_version: str) -> None: await stream.handle( Request(stream_id=1, http_version=http_version, headers=[], raw_path=b"/?a=b", method="GET") ) stream.task_group.spawn_app.assert_called() # type: ignore scope = stream.task_group.spawn_app.call_args[0][2] # type: ignore assert scope == { "type": "http", "http_version": http_version, "asgi": {"spec_version": "2.1", "version": "3.0"}, "method": "GET", "scheme": "http", "path": "/", "raw_path": b"/", "query_string": b"a=b", "root_path": stream.config.root_path, "headers": [], "client": None, "server": None, "extensions": {}, } @pytest.mark.asyncio async def test_handle_request_http_2(stream: HTTPStream) -> None: await stream.handle( Request(stream_id=1, http_version="2", headers=[], raw_path=b"/?a=b", method="GET") ) stream.task_group.spawn_app.assert_called() # type: ignore scope = stream.task_group.spawn_app.call_args[0][2] # type: ignore assert scope == { "type": "http", "http_version": "2", "asgi": {"spec_version": "2.1", "version": "3.0"}, "method": "GET", "scheme": "http", "path": "/", "raw_path": b"/", "query_string": b"a=b", "root_path": stream.config.root_path, "headers": [], "client": None, "server": None, "extensions": {"http.response.early_hint": {}, "http.response.push": {}}, } @pytest.mark.asyncio async def test_handle_body(stream: HTTPStream) -> None: await stream.handle(Body(stream_id=1, data=b"data")) stream.app_put.assert_called() # type: ignore assert stream.app_put.call_args_list == [ # type: ignore call({"type": "http.request", "body": b"data", "more_body": True}) ] @pytest.mark.asyncio async def test_handle_end_body(stream: HTTPStream) -> None: stream.app_put = AsyncMock() await stream.handle(EndBody(stream_id=1)) stream.app_put.assert_called() assert stream.app_put.call_args_list == [ call({"type": "http.request", "body": b"", "more_body": False}) ] @pytest.mark.asyncio async def test_handle_closed(stream: HTTPStream) -> None: await stream.handle(StreamClosed(stream_id=1)) stream.app_put.assert_called() # type: ignore assert stream.app_put.call_args_list == [call({"type": "http.disconnect"})] # type: ignore @pytest.mark.asyncio async def test_send_response(stream: HTTPStream) -> None: await stream.handle( Request(stream_id=1, http_version="2", headers=[], raw_path=b"/?a=b", method="GET") ) await stream.app_send( cast(HTTPResponseStartEvent, {"type": "http.response.start", "status": 200, "headers": []}) ) assert stream.state == ASGIHTTPState.REQUEST # Must wait for response before sending anything stream.send.assert_not_called() # type: ignore await stream.app_send( cast(HTTPResponseBodyEvent, {"type": "http.response.body", "body": b"Body"}) ) assert stream.state == ASGIHTTPState.CLOSED # type: ignore stream.send.assert_called() assert stream.send.call_args_list == [ call(Response(stream_id=1, headers=[], status_code=200)), call(Body(stream_id=1, data=b"Body")), call(EndBody(stream_id=1)), call(StreamClosed(stream_id=1)), ] stream.config._log.access.assert_called() @pytest.mark.asyncio async def test_invalid_server_name(stream: HTTPStream) -> None: stream.config.server_names = ["hypercorn"] await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"host", b"example.com")], raw_path=b"/", method="GET", ) ) assert stream.send.call_args_list == [ # type: ignore call( Response( stream_id=1, headers=[(b"content-length", b"0"), (b"connection", b"close")], status_code=404, ) ), call(EndBody(stream_id=1)), ] # This shouldn't error await stream.handle(Body(stream_id=1, data=b"Body")) @pytest.mark.asyncio async def test_send_push(stream: HTTPStream, http_scope: HTTPScope) -> None: stream.scope = http_scope stream.stream_id = 1 await stream.app_send({"type": "http.response.push", "path": "/push", "headers": []}) assert stream.send.call_args_list == [ # type: ignore call( Request( stream_id=1, headers=[(b":scheme", b"https")], http_version="2", method="GET", raw_path=b"/push", ) ) ] @pytest.mark.asyncio async def test_send_early_hint(stream: HTTPStream, http_scope: HTTPScope) -> None: stream.scope = http_scope stream.stream_id = 1 await stream.app_send( {"type": "http.response.early_hint", "links": [b'; rel="preload"; as="style"']} ) assert stream.send.call_args_list == [ # type: ignore call( InformationalResponse( stream_id=1, headers=[(b"link", b'; rel="preload"; as="style"')], status_code=103, ) ) ] @pytest.mark.asyncio async def test_send_app_error(stream: HTTPStream) -> None: await stream.handle( Request(stream_id=1, http_version="2", headers=[], raw_path=b"/?a=b", method="GET") ) await stream.app_send(None) stream.send.assert_called() # type: ignore assert stream.send.call_args_list == [ # type: ignore call( Response( stream_id=1, headers=[(b"content-length", b"0"), (b"connection", b"close")], status_code=500, ) ), call(EndBody(stream_id=1)), call(StreamClosed(stream_id=1)), ] stream.config._log.access.assert_called() # type: ignore @pytest.mark.parametrize( "state, message_type", [ (ASGIHTTPState.REQUEST, "not_a_real_type"), (ASGIHTTPState.RESPONSE, "http.response.start"), (ASGIHTTPState.CLOSED, "http.response.start"), (ASGIHTTPState.CLOSED, "http.response.body"), ], ) @pytest.mark.asyncio async def test_send_invalid_message_given_state( stream: HTTPStream, state: ASGIHTTPState, message_type: str ) -> None: stream.state = state with pytest.raises(UnexpectedMessageError): await stream.app_send({"type": message_type}) # type: ignore @pytest.mark.parametrize( "status, headers, body", [ ("201 NO CONTENT", [], b""), # Status should be int (200, [("X-Foo", "foo")], b""), # Headers should be bytes (200, [], "Body"), # Body should be bytes ], ) @pytest.mark.asyncio async def test_send_invalid_message( stream: HTTPStream, http_scope: HTTPScope, status: Any, headers: Any, body: Any, ) -> None: stream.scope = http_scope stream.state = ASGIHTTPState.REQUEST with pytest.raises((TypeError, ValueError)): await stream.app_send( cast( HTTPResponseStartEvent, {"type": "http.response.start", "headers": headers, "status": status}, ) ) await stream.app_send( cast(HTTPResponseBodyEvent, {"type": "http.response.body", "body": body}) ) def test_stream_idle(stream: HTTPStream) -> None: assert stream.idle is False @pytest.mark.asyncio async def test_closure(stream: HTTPStream) -> None: assert not stream.closed await stream.handle(StreamClosed(stream_id=1)) assert stream.closed await stream.handle(StreamClosed(stream_id=1)) assert stream.closed # It is important that the disconnect message has only been sent # once. assert stream.app_put.call_args_list == [call({"type": "http.disconnect"})] @pytest.mark.asyncio async def test_closed_app_send_noop(stream: HTTPStream) -> None: stream.closed = True await stream.app_send( cast(HTTPResponseStartEvent, {"type": "http.response.start", "status": 200, "headers": []}) ) stream.send.assert_not_called() # type: ignore hypercorn-0.14.4/tests/protocol/test_ws_stream.py000066400000000000000000000410551445231714500222440ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Any, cast, List, Tuple from unittest.mock import call, Mock import pytest import pytest_asyncio from wsproto.events import BytesMessage, TextMessage from hypercorn.asyncio.task_group import TaskGroup from hypercorn.asyncio.worker_context import WorkerContext from hypercorn.config import Config from hypercorn.logging import Logger from hypercorn.protocol.events import Body, Data, EndBody, EndData, Request, Response, StreamClosed from hypercorn.protocol.ws_stream import ( ASGIWebsocketState, FrameTooLargeError, Handshake, WebsocketBuffer, WSStream, ) from hypercorn.typing import ( WebsocketAcceptEvent, WebsocketCloseEvent, WebsocketResponseBodyEvent, WebsocketResponseStartEvent, WebsocketSendEvent, ) from hypercorn.utils import UnexpectedMessageError try: from unittest.mock import AsyncMock except ImportError: # Python < 3.8 from mock import AsyncMock # type: ignore def test_buffer() -> None: buffer_ = WebsocketBuffer(10) buffer_.extend(TextMessage(data="abc", frame_finished=False, message_finished=True)) assert buffer_.to_message() == {"type": "websocket.receive", "bytes": None, "text": "abc"} buffer_.clear() buffer_.extend(BytesMessage(data=b"abc", frame_finished=False, message_finished=True)) assert buffer_.to_message() == {"type": "websocket.receive", "bytes": b"abc", "text": None} def test_buffer_frame_too_large() -> None: buffer_ = WebsocketBuffer(2) with pytest.raises(FrameTooLargeError): buffer_.extend(TextMessage(data="abc", frame_finished=False, message_finished=True)) @pytest.mark.parametrize( "data", [ ( TextMessage(data="abc", frame_finished=False, message_finished=True), BytesMessage(data=b"abc", frame_finished=False, message_finished=True), ), ( BytesMessage(data=b"abc", frame_finished=False, message_finished=True), TextMessage(data="abc", frame_finished=False, message_finished=True), ), ], ) def test_buffer_mixed_types(data: list) -> None: buffer_ = WebsocketBuffer(10) buffer_.extend(data[0]) with pytest.raises(TypeError): buffer_.extend(data[1]) @pytest.mark.parametrize( "headers, http_version, valid", [ ([], "1.0", False), ( [ (b"connection", b"upgrade, keep-alive"), (b"sec-websocket-version", b"13"), (b"upgrade", b"websocket"), (b"sec-websocket-key", b"UnQ3lpJAH6j2PslA993iKQ=="), ], "1.1", True, ), ( [ (b"connection", b"keep-alive"), (b"sec-websocket-version", b"13"), (b"upgrade", b"websocket"), (b"sec-websocket-key", b"UnQ3lpJAH6j2PslA993iKQ=="), ], "1.1", False, ), ( [ (b"connection", b"upgrade, keep-alive"), (b"sec-websocket-version", b"13"), (b"upgrade", b"h2c"), (b"sec-websocket-key", b"UnQ3lpJAH6j2PslA993iKQ=="), ], "1.1", False, ), ([(b"sec-websocket-version", b"13")], "2", True), ([(b"sec-websocket-version", b"12")], "2", False), ], ) def test_handshake_validity( headers: List[Tuple[bytes, bytes]], http_version: str, valid: bool ) -> None: handshake = Handshake(headers, http_version) assert handshake.is_valid() is valid def test_handshake_accept_http1() -> None: handshake = Handshake( [ (b"connection", b"upgrade, keep-alive"), (b"sec-websocket-version", b"13"), (b"upgrade", b"websocket"), (b"sec-websocket-key", b"UnQ3lpJAH6j2PslA993iKQ=="), ], "1.1", ) status_code, headers, _ = handshake.accept(None, []) assert status_code == 101 assert headers == [ (b"sec-websocket-accept", b"1BpNk/3ah1huDGgcuMJBcjcMbEA="), (b"upgrade", b"WebSocket"), (b"connection", b"Upgrade"), ] def test_handshake_accept_http2() -> None: handshake = Handshake([(b"sec-websocket-version", b"13")], "2") status_code, headers, _ = handshake.accept(None, []) assert status_code == 200 assert headers == [] def test_handshake_accept_additional_headers() -> None: handshake = Handshake( [ (b"connection", b"upgrade, keep-alive"), (b"sec-websocket-version", b"13"), (b"upgrade", b"websocket"), (b"sec-websocket-key", b"UnQ3lpJAH6j2PslA993iKQ=="), ], "1.1", ) status_code, headers, _ = handshake.accept(None, [(b"additional", b"header")]) assert status_code == 101 assert headers == [ (b"sec-websocket-accept", b"1BpNk/3ah1huDGgcuMJBcjcMbEA="), (b"upgrade", b"WebSocket"), (b"connection", b"Upgrade"), (b"additional", b"header"), ] @pytest_asyncio.fixture(name="stream") # type: ignore[misc] async def _stream() -> WSStream: stream = WSStream( AsyncMock(), Config(), WorkerContext(), AsyncMock(), False, None, None, AsyncMock(), 1 ) stream.task_group.spawn_app.return_value = AsyncMock() # type: ignore stream.app_put = AsyncMock() stream.config._log = AsyncMock(spec=Logger) return stream @pytest.mark.asyncio async def test_handle_request(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/?a=b", method="GET", ) ) stream.task_group.spawn_app.assert_called() # type: ignore scope = stream.task_group.spawn_app.call_args[0][2] # type: ignore assert scope == { "type": "websocket", "asgi": {"spec_version": "2.3", "version": "3.0"}, "scheme": "ws", "http_version": "2", "path": "/", "raw_path": b"/", "query_string": b"a=b", "root_path": "", "headers": [(b"sec-websocket-version", b"13")], "client": None, "server": None, "subprotocols": [], "extensions": {"websocket.http.response": {}}, } @pytest.mark.asyncio async def test_handle_connection(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/?a=b", method="GET", ) ) await stream.app_send(cast(WebsocketAcceptEvent, {"type": "websocket.accept"})) stream.app_put = AsyncMock() await stream.handle(Data(stream_id=1, data=b"\x81\x85&`\x13\x0eN\x05\x7fbI")) stream.app_put.assert_called() assert stream.app_put.call_args_list == [ call({"type": "websocket.receive", "bytes": None, "text": "hello"}) ] @pytest.mark.asyncio async def test_handle_closed(stream: WSStream) -> None: await stream.handle(StreamClosed(stream_id=1)) stream.app_put.assert_called() # type: ignore assert stream.app_put.call_args_list == [ # type: ignore call({"type": "websocket.disconnect", "code": 1006}) ] @pytest.mark.asyncio async def test_send_accept(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/", method="GET", ) ) await stream.app_send(cast(WebsocketAcceptEvent, {"type": "websocket.accept"})) assert stream.state == ASGIWebsocketState.CONNECTED stream.send.assert_called() # type: ignore assert stream.send.call_args_list == [ # type: ignore call(Response(stream_id=1, headers=[], status_code=200)) ] @pytest.mark.asyncio async def test_send_accept_with_additional_headers(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/", method="GET", ) ) await stream.app_send( cast( WebsocketAcceptEvent, {"type": "websocket.accept", "headers": [(b"additional", b"header")]}, ) ) assert stream.state == ASGIWebsocketState.CONNECTED stream.send.assert_called() # type: ignore assert stream.send.call_args_list == [ # type: ignore call(Response(stream_id=1, headers=[(b"additional", b"header")], status_code=200)) ] @pytest.mark.asyncio async def test_send_reject(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/", method="GET", ) ) await stream.app_send( cast( WebsocketResponseStartEvent, {"type": "websocket.http.response.start", "status": 200, "headers": []}, ), ) assert stream.state == ASGIWebsocketState.HANDSHAKE # Must wait for response before sending anything stream.send.assert_not_called() # type: ignore await stream.app_send( cast(WebsocketResponseBodyEvent, {"type": "websocket.http.response.body", "body": b"Body"}) ) assert stream.state == ASGIWebsocketState.HTTPCLOSED # type: ignore stream.send.assert_called() assert stream.send.call_args_list == [ call(Response(stream_id=1, headers=[], status_code=200)), call(Body(stream_id=1, data=b"Body")), call(EndBody(stream_id=1)), ] stream.config._log.access.assert_called() @pytest.mark.asyncio async def test_invalid_server_name(stream: WSStream) -> None: stream.config.server_names = ["hypercorn"] await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"host", b"example.com"), (b"sec-websocket-version", b"13")], raw_path=b"/", method="GET", ) ) assert stream.send.call_args_list == [ # type: ignore call( Response( stream_id=1, headers=[(b"content-length", b"0"), (b"connection", b"close")], status_code=404, ) ), call(EndBody(stream_id=1)), ] # This shouldn't error await stream.handle(Body(stream_id=1, data=b"Body")) @pytest.mark.asyncio async def test_send_app_error_handshake(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/", method="GET", ) ) await stream.app_send(None) stream.send.assert_called() # type: ignore assert stream.send.call_args_list == [ # type: ignore call( Response( stream_id=1, headers=[(b"content-length", b"0"), (b"connection", b"close")], status_code=500, ) ), call(EndBody(stream_id=1)), call(StreamClosed(stream_id=1)), ] stream.config._log.access.assert_called() # type: ignore @pytest.mark.asyncio async def test_send_app_error_connected(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/", method="GET", ) ) await stream.app_send(cast(WebsocketAcceptEvent, {"type": "websocket.accept"})) await stream.app_send(None) stream.send.assert_called() # type: ignore assert stream.send.call_args_list == [ # type: ignore call(Response(stream_id=1, headers=[], status_code=200)), call(Data(stream_id=1, data=b"\x88\x02\x03\xf3")), call(StreamClosed(stream_id=1)), ] stream.config._log.access.assert_called() # type: ignore @pytest.mark.asyncio async def test_send_connection(stream: WSStream) -> None: await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/", method="GET", ) ) await stream.app_send(cast(WebsocketAcceptEvent, {"type": "websocket.accept"})) await stream.app_send(cast(WebsocketSendEvent, {"type": "websocket.send", "text": "hello"})) await stream.app_send(cast(WebsocketCloseEvent, {"type": "websocket.close"})) stream.send.assert_called() # type: ignore assert stream.send.call_args_list == [ # type: ignore call(Response(stream_id=1, headers=[], status_code=200)), call(Data(stream_id=1, data=b"\x81\x05hello")), call(Data(stream_id=1, data=b"\x88\x02\x03\xe8")), call(EndData(stream_id=1)), ] @pytest.mark.asyncio async def test_pings(stream: WSStream, event_loop: asyncio.AbstractEventLoop) -> None: stream.config.websocket_ping_interval = 0.1 await stream.handle( Request( stream_id=1, http_version="2", headers=[(b"sec-websocket-version", b"13")], raw_path=b"/?a=b", method="GET", ) ) async with TaskGroup(event_loop) as task_group: stream.task_group = task_group await stream.app_send(cast(WebsocketAcceptEvent, {"type": "websocket.accept"})) stream.app_put = AsyncMock() await asyncio.sleep(0.15) assert stream.send.call_args_list == [ # type: ignore call(Response(stream_id=1, headers=[], status_code=200)), call(Data(stream_id=1, data=b"\x89\x00")), call(Data(stream_id=1, data=b"\x89\x00")), ] await stream.handle(StreamClosed(stream_id=1)) @pytest.mark.asyncio @pytest.mark.parametrize( "state, message_type", [ (ASGIWebsocketState.HANDSHAKE, "websocket.send"), (ASGIWebsocketState.RESPONSE, "websocket.accept"), (ASGIWebsocketState.RESPONSE, "websocket.send"), (ASGIWebsocketState.CONNECTED, "websocket.http.response.start"), (ASGIWebsocketState.CONNECTED, "websocket.http.response.body"), (ASGIWebsocketState.CLOSED, "websocket.send"), (ASGIWebsocketState.CLOSED, "websocket.http.response.start"), (ASGIWebsocketState.CLOSED, "websocket.http.response.body"), ], ) async def test_send_invalid_message_given_state( stream: WSStream, state: ASGIWebsocketState, message_type: str ) -> None: stream.state = state with pytest.raises(UnexpectedMessageError): await stream.app_send({"type": message_type}) # type: ignore @pytest.mark.asyncio @pytest.mark.parametrize( "status, headers, body", [ ("201 NO CONTENT", [], b""), # Status should be int (200, [("X-Foo", "foo")], b""), # Headers should be bytes (200, [], "Body"), # Body should be bytes ], ) async def test_send_invalid_http_message( stream: WSStream, status: Any, headers: Any, body: Any ) -> None: stream.connection = Mock() stream.state = ASGIWebsocketState.HANDSHAKE stream.scope = {"method": "GET"} # type: ignore with pytest.raises((TypeError, ValueError)): await stream.app_send( cast( WebsocketResponseStartEvent, {"type": "websocket.http.response.start", "headers": headers, "status": status}, ), ) await stream.app_send( cast(WebsocketResponseBodyEvent, {"type": "websocket.http.response.body", "body": body}) ) @pytest.mark.parametrize( "state, idle", [ (state, False) for state in ASGIWebsocketState if state not in {ASGIWebsocketState.CLOSED, ASGIWebsocketState.HTTPCLOSED} ] + [(ASGIWebsocketState.CLOSED, True), (ASGIWebsocketState.HTTPCLOSED, True)], ) def test_stream_idle(stream: WSStream, state: ASGIWebsocketState, idle: bool) -> None: stream.state = state assert stream.idle is idle @pytest.mark.asyncio async def test_closure(stream: WSStream) -> None: assert not stream.closed await stream.handle(StreamClosed(stream_id=1)) assert stream.closed await stream.handle(StreamClosed(stream_id=1)) assert stream.closed # It is important that the disconnect message has only been sent # once. assert stream.app_put.call_args_list == [call({"type": "websocket.disconnect", "code": 1006})] @pytest.mark.asyncio async def test_closed_app_send_noop(stream: WSStream) -> None: stream.closed = True await stream.app_send(cast(WebsocketAcceptEvent, {"type": "websocket.accept"})) stream.send.assert_not_called() # type: ignore hypercorn-0.14.4/tests/test___main__.py000066400000000000000000000055221445231714500201160ustar00rootroot00000000000000from __future__ import annotations import inspect import os from unittest.mock import Mock import pytest from _pytest.monkeypatch import MonkeyPatch import hypercorn.__main__ from hypercorn.config import Config def test_load_config_none() -> None: assert isinstance(hypercorn.__main__._load_config(None), Config) def test_load_config_pyfile(monkeypatch: MonkeyPatch) -> None: mock_config = Mock() monkeypatch.setattr(hypercorn.__main__, "Config", mock_config) hypercorn.__main__._load_config("file:assets/config.py") mock_config.from_pyfile.assert_called() def test_load_config_pymodule(monkeypatch: MonkeyPatch) -> None: mock_config = Mock() monkeypatch.setattr(hypercorn.__main__, "Config", mock_config) hypercorn.__main__._load_config("python:assets.config") mock_config.from_object.assert_called() def test_load_config(monkeypatch: MonkeyPatch) -> None: mock_config = Mock() monkeypatch.setattr(hypercorn.__main__, "Config", mock_config) hypercorn.__main__._load_config("assets/config") mock_config.from_toml.assert_called() @pytest.mark.parametrize( "flag, set_value, config_key", [ ("--access-logformat", "jeff", "access_log_format"), ("--backlog", 5, "backlog"), ("--ca-certs", "/path", "ca_certs"), ("--certfile", "/path", "certfile"), ("--ciphers", "DHE-RSA-AES128-SHA", "ciphers"), ("--worker-class", "trio", "worker_class"), ("--keep-alive", 20, "keep_alive_timeout"), ("--keyfile", "/path", "keyfile"), ("--pid", "/path", "pid_path"), ("--root-path", "/path", "root_path"), ("--workers", 2, "workers"), ], ) def test_main_cli_override( flag: str, set_value: str, config_key: str, monkeypatch: MonkeyPatch ) -> None: run_multiple = Mock() monkeypatch.setattr(hypercorn.__main__, "run", run_multiple) path = os.path.join(os.path.dirname(__file__), "assets/config_ssl.py") raw_config = Config.from_pyfile(path) hypercorn.__main__.main(["--config", f"file:{path}", flag, str(set_value), "asgi:App"]) run_multiple.assert_called() config = run_multiple.call_args_list[0][0][0] for name, value in inspect.getmembers(raw_config): if ( not inspect.ismethod(value) and not name.startswith("_") and name not in {"log", config_key} ): assert getattr(raw_config, name) == getattr(config, name) assert getattr(config, config_key) == set_value def test_verify_mode_conversion(monkeypatch: MonkeyPatch) -> None: run_multiple = Mock() monkeypatch.setattr(hypercorn.__main__, "run", run_multiple) with pytest.raises(SystemExit): hypercorn.__main__.main(["--verify-mode", "CERT_UNKNOWN", "asgi:App"]) hypercorn.__main__.main(["--verify-mode", "CERT_REQUIRED", "asgi:App"]) run_multiple.assert_called() hypercorn-0.14.4/tests/test_app_wrappers.py000066400000000000000000000126441445231714500211040ustar00rootroot00000000000000from __future__ import annotations import asyncio from functools import partial from typing import Any, Callable, List import pytest import trio from hypercorn.app_wrappers import _build_environ, InvalidPathError, WSGIWrapper from hypercorn.typing import ASGISendEvent, HTTPScope def echo_body(environ: dict, start_response: Callable) -> List[bytes]: status = "200 OK" output = environ["wsgi.input"].read() headers = [ ("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(output))), ] start_response(status, headers) return [output] @pytest.mark.trio async def test_wsgi_trio() -> None: app = WSGIWrapper(echo_body, 2**16) scope: HTTPScope = { "http_version": "1.1", "asgi": {}, "method": "GET", "headers": [], "path": "/", "root_path": "/", "query_string": b"a=b", "raw_path": b"/", "scheme": "http", "type": "http", "client": ("localhost", 80), "server": None, "extensions": {}, } send_channel, receive_channel = trio.open_memory_channel(1) await send_channel.send({"type": "http.request"}) messages = [] async def _send(message: ASGISendEvent) -> None: nonlocal messages messages.append(message) await app(scope, receive_channel.receive, _send, trio.to_thread.run_sync, trio.from_thread.run) assert messages == [ { "headers": [(b"content-type", b"text/plain; charset=utf-8"), (b"content-length", b"0")], "status": 200, "type": "http.response.start", }, {"body": bytearray(b""), "type": "http.response.body", "more_body": True}, {"body": bytearray(b""), "type": "http.response.body", "more_body": False}, ] @pytest.mark.asyncio async def test_wsgi_asyncio(event_loop: asyncio.AbstractEventLoop) -> None: app = WSGIWrapper(echo_body, 2**16) scope: HTTPScope = { "http_version": "1.1", "asgi": {}, "method": "GET", "headers": [], "path": "/", "root_path": "/", "query_string": b"a=b", "raw_path": b"/", "scheme": "http", "type": "http", "client": ("localhost", 80), "server": None, "extensions": {}, } queue: asyncio.Queue = asyncio.Queue() await queue.put({"type": "http.request"}) messages = [] async def _send(message: ASGISendEvent) -> None: nonlocal messages messages.append(message) def _call_soon(func: Callable, *args: Any) -> Any: future = asyncio.run_coroutine_threadsafe(func(*args), event_loop) return future.result() await app(scope, queue.get, _send, partial(event_loop.run_in_executor, None), _call_soon) assert messages == [ { "headers": [(b"content-type", b"text/plain; charset=utf-8"), (b"content-length", b"0")], "status": 200, "type": "http.response.start", }, {"body": bytearray(b""), "type": "http.response.body", "more_body": True}, {"body": bytearray(b""), "type": "http.response.body", "more_body": False}, ] @pytest.mark.asyncio async def test_max_body_size(event_loop: asyncio.AbstractEventLoop) -> None: app = WSGIWrapper(echo_body, 4) scope: HTTPScope = { "http_version": "1.1", "asgi": {}, "method": "GET", "headers": [], "path": "/", "root_path": "/", "query_string": b"a=b", "raw_path": b"/", "scheme": "http", "type": "http", "client": ("localhost", 80), "server": None, "extensions": {}, } queue: asyncio.Queue = asyncio.Queue() await queue.put({"type": "http.request", "body": b"abcde"}) messages = [] async def _send(message: ASGISendEvent) -> None: nonlocal messages messages.append(message) def _call_soon(func: Callable, *args: Any) -> Any: future = asyncio.run_coroutine_threadsafe(func(*args), event_loop) return future.result() await app(scope, queue.get, _send, partial(event_loop.run_in_executor, None), _call_soon) assert messages == [ {"headers": [], "status": 400, "type": "http.response.start"}, {"body": bytearray(b""), "type": "http.response.body", "more_body": False}, ] def test_build_environ_encoding() -> None: scope: HTTPScope = { "http_version": "1.0", "asgi": {}, "method": "GET", "headers": [], "path": "/中/文", "root_path": "/中", "query_string": b"bar=baz", "raw_path": "/中/文".encode(), "scheme": "http", "type": "http", "client": ("localhost", 80), "server": None, "extensions": {}, } environ = _build_environ(scope, b"") assert environ["SCRIPT_NAME"] == "/中".encode("utf8").decode("latin-1") assert environ["PATH_INFO"] == "/文".encode("utf8").decode("latin-1") def test_build_environ_root_path() -> None: scope: HTTPScope = { "http_version": "1.0", "asgi": {}, "method": "GET", "headers": [], "path": "/中文", "root_path": "/中国", "query_string": b"bar=baz", "raw_path": "/中文".encode(), "scheme": "http", "type": "http", "client": ("localhost", 80), "server": None, "extensions": {}, } with pytest.raises(InvalidPathError): _build_environ(scope, b"") hypercorn-0.14.4/tests/test_config.py000066400000000000000000000116571445231714500176510ustar00rootroot00000000000000from __future__ import annotations import os import socket import ssl import sys from typing import Tuple from unittest.mock import Mock, NonCallableMock import pytest from _pytest.monkeypatch import MonkeyPatch import hypercorn.config from hypercorn.config import Config access_log_format = "bob" h11_max_incomplete_size = 4 def _check_standard_config(config: Config) -> None: assert config.access_log_format == access_log_format assert config.h11_max_incomplete_size == h11_max_incomplete_size assert config.bind == ["127.0.0.1:5555"] def test_config_from_pyfile() -> None: path = os.path.join(os.path.dirname(__file__), "assets/config.py") config = Config.from_pyfile(path) _check_standard_config(config) def test_config_from_object() -> None: sys.path.append(os.path.join(os.path.dirname(__file__))) config = Config.from_object("assets.config") _check_standard_config(config) def test_ssl_config_from_pyfile() -> None: path = os.path.join(os.path.dirname(__file__), "assets/config_ssl.py") config = Config.from_pyfile(path) _check_standard_config(config) assert config.ssl_enabled def test_config_from_toml() -> None: path = os.path.join(os.path.dirname(__file__), "assets/config.toml") config = Config.from_toml(path) _check_standard_config(config) def test_create_ssl_context() -> None: path = os.path.join(os.path.dirname(__file__), "assets/config_ssl.py") config = Config.from_pyfile(path) context = config.create_ssl_context() assert context.options & ( ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION ) @pytest.mark.parametrize( "bind, expected_family, expected_binding", [ ("127.0.0.1:5000", socket.AF_INET, ("127.0.0.1", 5000)), ("127.0.0.1", socket.AF_INET, ("127.0.0.1", 8000)), ("[::]:5000", socket.AF_INET6, ("::", 5000)), ("[::]", socket.AF_INET6, ("::", 8000)), ], ) def test_create_sockets_ip( bind: str, expected_family: socket.AddressFamily, expected_binding: Tuple[str, int], monkeypatch: MonkeyPatch, ) -> None: mock_socket = Mock() monkeypatch.setattr(socket, "socket", mock_socket) config = Config() config.bind = [bind] sockets = config.create_sockets() sock = sockets.insecure_sockets[0] mock_socket.assert_called_with(expected_family, socket.SOCK_STREAM) sock.setsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # type: ignore sock.bind.assert_called_with(expected_binding) # type: ignore sock.setblocking.assert_called_with(False) # type: ignore sock.set_inheritable.assert_called_with(True) # type: ignore def test_create_sockets_unix(monkeypatch: MonkeyPatch) -> None: mock_socket = Mock() monkeypatch.setattr(socket, "socket", mock_socket) monkeypatch.setattr(os, "chown", Mock()) config = Config() config.bind = ["unix:/tmp/hypercorn.sock"] sockets = config.create_sockets() sock = sockets.insecure_sockets[0] mock_socket.assert_called_with(socket.AF_UNIX, socket.SOCK_STREAM) sock.setsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # type: ignore sock.bind.assert_called_with("/tmp/hypercorn.sock") # type: ignore sock.setblocking.assert_called_with(False) # type: ignore sock.set_inheritable.assert_called_with(True) # type: ignore def test_create_sockets_fd(monkeypatch: MonkeyPatch) -> None: mock_sock_class = Mock( return_value=NonCallableMock( **{"getsockopt.return_value": socket.SOCK_STREAM} # type: ignore ) ) monkeypatch.setattr(socket, "socket", mock_sock_class) config = Config() config.bind = ["fd://2"] sockets = config.create_sockets() sock = sockets.insecure_sockets[0] mock_sock_class.assert_called_with(fileno=2) sock.getsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_TYPE) # type: ignore sock.setsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # type: ignore sock.setblocking.assert_called_with(False) # type: ignore sock.set_inheritable.assert_called_with(True) # type: ignore def test_create_sockets_multiple(monkeypatch: MonkeyPatch) -> None: mock_socket = Mock() monkeypatch.setattr(socket, "socket", mock_socket) monkeypatch.setattr(os, "chown", Mock()) config = Config() config.bind = ["127.0.0.1", "unix:/tmp/hypercorn.sock"] sockets = config.create_sockets() assert len(sockets.insecure_sockets) == 2 def test_response_headers(monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr(hypercorn.config, "time", lambda: 1_512_229_395) config = Config() assert config.response_headers("test") == [ (b"date", b"Sat, 02 Dec 2017 15:43:15 GMT"), (b"server", b"hypercorn-test"), ] config.include_server_header = False assert config.response_headers("test") == [(b"date", b"Sat, 02 Dec 2017 15:43:15 GMT")] hypercorn-0.14.4/tests/test_logging.py000066400000000000000000000076771445231714500200410ustar00rootroot00000000000000from __future__ import annotations import logging import os import time from typing import Optional, Type, Union import pytest from hypercorn.config import Config from hypercorn.logging import AccessLogAtoms, Logger from hypercorn.typing import HTTPScope, ResponseSummary @pytest.mark.parametrize( "target, expected_name, expected_handler_type", [ ("-", "hypercorn.access", logging.StreamHandler), ("/tmp/path", "hypercorn.access", logging.FileHandler), (logging.getLogger("test_special"), "test_special", None), (None, None, None), ], ) def test_access_logger_init( target: Union[logging.Logger, str, None], expected_name: Optional[str], expected_handler_type: Optional[Type[logging.Handler]], ) -> None: config = Config() config.accesslog = target config.access_log_format = "%h" logger = Logger(config) assert logger.access_log_format == "%h" assert logger.getEffectiveLevel() == logging.INFO if target is None: assert logger.access_logger is None elif expected_name is None: assert logger.access_logger.handlers == [] else: assert logger.access_logger.name == expected_name if expected_handler_type is None: assert logger.access_logger.handlers == [] else: assert isinstance(logger.access_logger.handlers[0], expected_handler_type) @pytest.mark.parametrize( "level, expected", [ (logging.getLevelName(level_name), level_name) for level_name in range(logging.DEBUG, logging.CRITICAL + 1, 10) ], ) def test_loglevel_option(level: Optional[str], expected: int) -> None: config = Config() config.loglevel = level logger = Logger(config) assert logger.error_logger.getEffectiveLevel() == expected @pytest.fixture(name="response") def _response_scope() -> dict: return {"status": 200, "headers": [(b"Content-Length", b"5"), (b"X-Hypercorn", b"Hypercorn")]} def test_access_log_standard_atoms(http_scope: HTTPScope, response: ResponseSummary) -> None: atoms = AccessLogAtoms(http_scope, response, 0.000_023) assert atoms["h"] == "127.0.0.1:80" assert atoms["l"] == "-" assert time.strptime(atoms["t"], "[%d/%b/%Y:%H:%M:%S %z]") assert int(atoms["s"]) == 200 assert atoms["m"] == "GET" assert atoms["U"] == "/" assert atoms["q"] == "a=b" assert atoms["H"] == "2" assert int(atoms["b"]) == 5 assert int(atoms["B"]) == 5 assert atoms["f"] == "hypercorn" assert atoms["a"] == "Hypercorn" assert atoms["p"] == f"<{os.getpid()}>" assert atoms["not-atom"] == "-" assert int(atoms["T"]) == 0 assert int(atoms["D"]) == 23 assert atoms["L"] == "0.000023" assert atoms["r"] == "GET / 2" assert atoms["R"] == "GET /?a=b 2" assert atoms["Uq"] == "/?a=b" assert atoms["st"] == "OK" def test_access_log_header_atoms(http_scope: HTTPScope, response: ResponseSummary) -> None: atoms = AccessLogAtoms(http_scope, response, 0) assert atoms["{X-Hypercorn}i"] == "Hypercorn" assert atoms["{X-HYPERCORN}i"] == "Hypercorn" assert atoms["{not-atom}i"] == "-" assert atoms["{X-Hypercorn}o"] == "Hypercorn" assert atoms["{X-HYPERCORN}o"] == "Hypercorn" def test_access_no_log_header_atoms(http_scope: HTTPScope) -> None: atoms = AccessLogAtoms(http_scope, {"status": 200, "headers": []}, 0) assert atoms["{X-Hypercorn}i"] == "Hypercorn" assert atoms["{X-HYPERCORN}i"] == "Hypercorn" assert atoms["{not-atom}i"] == "-" assert not any(key.startswith("{") and key.endswith("}o") for key in atoms.keys()) def test_access_log_environ_atoms(http_scope: HTTPScope, response: ResponseSummary) -> None: os.environ["Random"] = "Environ" atoms = AccessLogAtoms(http_scope, response, 0) assert atoms["{random}e"] == "Environ" def test_nonstandard_status_code(http_scope: HTTPScope) -> None: atoms = AccessLogAtoms(http_scope, {"status": 441, "headers": []}, 0) assert atoms["st"] == "" hypercorn-0.14.4/tests/test_utils.py000066400000000000000000000041361445231714500175360ustar00rootroot00000000000000from __future__ import annotations from typing import Any, Callable, Iterable import pytest from hypercorn.typing import Scope from hypercorn.utils import ( build_and_validate_headers, filter_pseudo_headers, is_asgi, suppress_body, ) @pytest.mark.parametrize( "method, status, expected", [("HEAD", 200, True), ("GET", 200, False), ("GET", 101, True)] ) def test_suppress_body(method: str, status: int, expected: bool) -> None: assert suppress_body(method, status) is expected class ASGIClassInstance: def __init__(self) -> None: pass async def __call__(self, scope: Scope, receive: Callable, send: Callable) -> None: pass async def asgi_callable(scope: Scope, receive: Callable, send: Callable) -> None: pass class WSGIClassInstance: def __init__(self) -> None: pass def __call__(self, environ: dict, start_response: Callable) -> Iterable[bytes]: pass def wsgi_callable(environ: dict, start_response: Callable) -> Iterable[bytes]: pass @pytest.mark.parametrize( "app, expected", [ (WSGIClassInstance(), False), (ASGIClassInstance(), True), (wsgi_callable, False), (asgi_callable, True), ], ) def test_is_asgi(app: Any, expected: bool) -> None: assert is_asgi(app) == expected def test_build_and_validate_headers_validate() -> None: with pytest.raises(TypeError): build_and_validate_headers([("string", "string")]) # type: ignore def test_build_and_validate_headers_pseudo() -> None: with pytest.raises(ValueError): build_and_validate_headers([(b":authority", b"quart")]) def test_filter_pseudo_headers() -> None: result = filter_pseudo_headers( [(b":authority", b"quart"), (b":path", b"/"), (b"user-agent", b"something")] ) assert result == [(b"host", b"quart"), (b"user-agent", b"something")] def test_filter_pseudo_headers_no_authority() -> None: result = filter_pseudo_headers( [(b"host", b"quart"), (b":path", b"/"), (b"user-agent", b"something")] ) assert result == [(b"host", b"quart"), (b"user-agent", b"something")] hypercorn-0.14.4/tests/trio/000077500000000000000000000000001445231714500157365ustar00rootroot00000000000000hypercorn-0.14.4/tests/trio/__init__.py000066400000000000000000000000001445231714500200350ustar00rootroot00000000000000hypercorn-0.14.4/tests/trio/test_keep_alive.py000066400000000000000000000074341445231714500214630ustar00rootroot00000000000000from __future__ import annotations from typing import Callable, Generator import h11 import pytest import trio from hypercorn.app_wrappers import ASGIWrapper from hypercorn.config import Config from hypercorn.trio.tcp_server import TCPServer from hypercorn.trio.worker_context import WorkerContext from hypercorn.typing import Scope from ..helpers import MockSocket KEEP_ALIVE_TIMEOUT = 0.01 REQUEST = h11.Request(method="GET", target="/", headers=[(b"host", b"hypercorn")]) async def slow_framework(scope: Scope, receive: Callable, send: Callable) -> None: while True: event = await receive() if event["type"] == "http.disconnect": break elif event["type"] == "lifespan.startup": await send({"type": "lifspan.startup.complete"}) elif event["type"] == "lifespan.shutdown": await send({"type": "lifspan.shutdown.complete"}) elif event["type"] == "http.request" and not event.get("more_body", False): await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) await send( { "type": "http.response.start", "status": 200, "headers": [(b"content-length", b"0")], } ) await send({"type": "http.response.body", "body": b"", "more_body": False}) break @pytest.fixture(name="client_stream", scope="function") def _client_stream( nursery: trio._core._run.Nursery, ) -> Generator[trio.testing._memory_streams.MemorySendStream, None, None]: config = Config() config.keep_alive_timeout = KEEP_ALIVE_TIMEOUT client_stream, server_stream = trio.testing.memory_stream_pair() server_stream.socket = MockSocket() server = TCPServer(ASGIWrapper(slow_framework), config, WorkerContext(), server_stream) nursery.start_soon(server.run) yield client_stream @pytest.mark.trio async def test_http1_keep_alive_pre_request( client_stream: trio.testing._memory_streams.MemorySendStream, ) -> None: await client_stream.send_all(b"GET") await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) # Only way to confirm closure is to invoke an error with pytest.raises(trio.BrokenResourceError): await client_stream.send_all(b"a") @pytest.mark.trio async def test_http1_keep_alive_during( client_stream: trio.testing._memory_streams.MemorySendStream, ) -> None: client = h11.Connection(h11.CLIENT) await client_stream.send_all(client.send(REQUEST)) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) # Key is that this doesn't error await client_stream.send_all(client.send(h11.EndOfMessage())) @pytest.mark.trio async def test_http1_keep_alive( client_stream: trio.testing._memory_streams.MemorySendStream, ) -> None: client = h11.Connection(h11.CLIENT) await client_stream.send_all(client.send(REQUEST)) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) await client_stream.send_all(client.send(h11.EndOfMessage())) while True: event = client.next_event() if event == h11.NEED_DATA: data = await client_stream.receive_some(2**16) client.receive_data(data) elif isinstance(event, h11.EndOfMessage): break client.start_next_cycle() await client_stream.send_all(client.send(REQUEST)) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) # Key is that this doesn't error await client_stream.send_all(client.send(h11.EndOfMessage())) @pytest.mark.trio async def test_http1_keep_alive_pipelining( client_stream: trio.testing._memory_streams.MemorySendStream, ) -> None: await client_stream.send_all( b"GET / HTTP/1.1\r\nHost: hypercorn\r\n\r\nGET / HTTP/1.1\r\nHost: hypercorn\r\n\r\n" ) await client_stream.receive_some(2**16) await trio.sleep(2 * KEEP_ALIVE_TIMEOUT) await client_stream.send_all(b"") hypercorn-0.14.4/tests/trio/test_lifespan.py000066400000000000000000000023121445231714500211460ustar00rootroot00000000000000from __future__ import annotations import pytest import trio from hypercorn.app_wrappers import ASGIWrapper from hypercorn.config import Config from hypercorn.trio.lifespan import Lifespan from hypercorn.utils import LifespanFailureError, LifespanTimeoutError from ..helpers import lifespan_failure, SlowLifespanFramework @pytest.mark.trio async def test_startup_timeout_error(nursery: trio._core._run.Nursery) -> None: config = Config() config.startup_timeout = 0.01 lifespan = Lifespan(ASGIWrapper(SlowLifespanFramework(0.02, trio.sleep)), config) nursery.start_soon(lifespan.handle_lifespan) with pytest.raises(LifespanTimeoutError) as exc_info: await lifespan.wait_for_startup() assert str(exc_info.value).startswith("Timeout whilst awaiting startup") @pytest.mark.trio async def test_startup_failure() -> None: lifespan = Lifespan(ASGIWrapper(lifespan_failure), Config()) with pytest.raises(LifespanFailureError) as exc_info: async with trio.open_nursery() as lifespan_nursery: await lifespan_nursery.start(lifespan.handle_lifespan) await lifespan.wait_for_startup() assert str(exc_info.value) == "Lifespan failure in startup. 'Failure'" hypercorn-0.14.4/tests/trio/test_sanity.py000066400000000000000000000175521445231714500206700ustar00rootroot00000000000000from __future__ import annotations from unittest.mock import Mock, PropertyMock import h2 import h11 import pytest import trio import wsproto from hypercorn.app_wrappers import ASGIWrapper from hypercorn.config import Config from hypercorn.trio.tcp_server import TCPServer from hypercorn.trio.worker_context import WorkerContext from ..helpers import MockSocket, SANITY_BODY, sanity_framework try: from unittest.mock import AsyncMock except ImportError: # Python < 3.8 from mock import AsyncMock # type: ignore @pytest.mark.trio async def test_http1_request(nursery: trio._core._run.Nursery) -> None: client_stream, server_stream = trio.testing.memory_stream_pair() server_stream.socket = MockSocket() server = TCPServer(ASGIWrapper(sanity_framework), Config(), WorkerContext(), server_stream) nursery.start_soon(server.run) client = h11.Connection(h11.CLIENT) await client_stream.send_all( client.send( h11.Request( method="POST", target="/", headers=[ (b"host", b"hypercorn"), (b"connection", b"close"), (b"content-length", b"%d" % len(SANITY_BODY)), ], ) ) ) await client_stream.send_all(client.send(h11.Data(data=SANITY_BODY))) await client_stream.send_all(client.send(h11.EndOfMessage())) events = [] while True: event = client.next_event() if event == h11.NEED_DATA: # bytes cast is key otherwise b"" is lost data = bytes(await client_stream.receive_some(1024)) client.receive_data(data) elif isinstance(event, h11.ConnectionClosed): break else: events.append(event) assert events == [ h11.Response( status_code=200, headers=[ (b"content-length", b"15"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h11"), (b"connection", b"close"), ], http_version=b"1.1", reason=b"", ), h11.Data(data=b"Hello & Goodbye"), h11.EndOfMessage(headers=[]), # type: ignore ] @pytest.mark.trio async def test_http1_websocket(nursery: trio._core._run.Nursery) -> None: client_stream, server_stream = trio.testing.memory_stream_pair() server_stream.socket = MockSocket() server = TCPServer(ASGIWrapper(sanity_framework), Config(), WorkerContext(), server_stream) nursery.start_soon(server.run) client = wsproto.WSConnection(wsproto.ConnectionType.CLIENT) await client_stream.send_all(client.send(wsproto.events.Request(host="hypercorn", target="/"))) client.receive_data(await client_stream.receive_some(1024)) assert list(client.events()) == [ wsproto.events.AcceptConnection( extra_headers=[ (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h11"), ] ) ] await client_stream.send_all(client.send(wsproto.events.BytesMessage(data=SANITY_BODY))) client.receive_data(await client_stream.receive_some(1024)) assert list(client.events()) == [wsproto.events.TextMessage(data="Hello & Goodbye")] await client_stream.send_all(client.send(wsproto.events.CloseConnection(code=1000))) client.receive_data(await client_stream.receive_some(1024)) assert list(client.events()) == [wsproto.events.CloseConnection(code=1000, reason="")] @pytest.mark.trio async def test_http2_request(nursery: trio._core._run.Nursery) -> None: client_stream, server_stream = trio.testing.memory_stream_pair() server_stream.transport_stream = Mock(return_value=PropertyMock(return_value=MockSocket())) server_stream.do_handshake = AsyncMock() server_stream.selected_alpn_protocol = Mock(return_value="h2") server = TCPServer(ASGIWrapper(sanity_framework), Config(), WorkerContext(), server_stream) nursery.start_soon(server.run) client = h2.connection.H2Connection() client.initiate_connection() await client_stream.send_all(client.data_to_send()) stream_id = client.get_next_available_stream_id() client.send_headers( stream_id, [ (":method", "GET"), (":path", "/"), (":authority", "hypercorn"), (":scheme", "https"), ("content-length", "%d" % len(SANITY_BODY)), ], ) client.send_data(stream_id, SANITY_BODY) client.end_stream(stream_id) await client_stream.send_all(client.data_to_send()) events = [] open_ = True while open_: # bytes cast is key otherwise b"" is lost data = bytes(await client_stream.receive_some(1024)) if data == b"": open_ = False h2_events = client.receive_data(data) for event in h2_events: if isinstance(event, h2.events.DataReceived): client.acknowledge_received_data(event.flow_controlled_length, event.stream_id) elif isinstance( event, (h2.events.ConnectionTerminated, h2.events.StreamEnded, h2.events.StreamReset), ): open_ = False break else: events.append(event) await client_stream.send_all(client.data_to_send()) assert isinstance(events[2], h2.events.ResponseReceived) assert events[2].headers == [ (b":status", b"200"), (b"content-length", b"15"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h2"), ] @pytest.mark.trio async def test_http2_websocket(nursery: trio._core._run.Nursery) -> None: client_stream, server_stream = trio.testing.memory_stream_pair() server_stream.transport_stream = Mock(return_value=PropertyMock(return_value=MockSocket())) server_stream.do_handshake = AsyncMock() server_stream.selected_alpn_protocol = Mock(return_value="h2") server = TCPServer(ASGIWrapper(sanity_framework), Config(), WorkerContext(), server_stream) nursery.start_soon(server.run) h2_client = h2.connection.H2Connection() h2_client.initiate_connection() await client_stream.send_all(h2_client.data_to_send()) stream_id = h2_client.get_next_available_stream_id() h2_client.send_headers( stream_id, [ (":method", "CONNECT"), (":path", "/"), (":authority", "hypercorn"), (":scheme", "https"), ("sec-websocket-version", "13"), ], ) await client_stream.send_all(h2_client.data_to_send()) events = h2_client.receive_data(await client_stream.receive_some(1024)) await client_stream.send_all(h2_client.data_to_send()) events = h2_client.receive_data(await client_stream.receive_some(1024)) if not isinstance(events[-1], h2.events.ResponseReceived): events = h2_client.receive_data(await client_stream.receive_some(1024)) assert events[-1].headers == [ (b":status", b"200"), (b"date", b"Thu, 01 Jan 1970 01:23:20 GMT"), (b"server", b"hypercorn-h2"), ] client = wsproto.connection.Connection(wsproto.ConnectionType.CLIENT) h2_client.send_data(stream_id, client.send(wsproto.events.BytesMessage(data=SANITY_BODY))) await client_stream.send_all(h2_client.data_to_send()) events = h2_client.receive_data(await client_stream.receive_some(1024)) client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.TextMessage(data="Hello & Goodbye")] h2_client.send_data(stream_id, client.send(wsproto.events.CloseConnection(code=1000))) await client_stream.send_all(h2_client.data_to_send()) events = h2_client.receive_data(await client_stream.receive_some(1024)) client.receive_data(events[0].data) assert list(client.events()) == [wsproto.events.CloseConnection(code=1000, reason="")] await client_stream.send_all(b"") hypercorn-0.14.4/tox.ini000066400000000000000000000022051445231714500151310ustar00rootroot00000000000000[tox] envlist = docs,format,mypy,py37,py38,py39,py310,py311,package,pep8 minversion = 3.3 isolated_build = true [testenv] deps = py37: mock hypothesis pytest pytest-asyncio pytest-cov pytest-sugar pytest-trio commands = pytest --cov=hypercorn {posargs} [testenv:docs] basepython = python3.11 deps = pydata-sphinx-theme sphinx trio commands = sphinx-apidoc -e -f -o docs/reference/source/ src/hypercorn/ src/hypercorn/protocol/quic.py src/hypercorn/protocol/h3.py sphinx-build -W --keep-going -b html -d {envtmpdir}/doctrees docs/ docs/_build/html/ [testenv:format] basepython = python3.11 deps = black isort commands = black --check --diff src/hypercorn/ tests/ isort --check --diff src/hypercorn tests [testenv:pep8] basepython = python3.11 deps = flake8 pep8-naming flake8-future-import flake8-print commands = flake8 src/hypercorn/ tests/ [testenv:mypy] basepython = python3.11 deps = mypy pytest commands = mypy src/hypercorn/ tests/ [testenv:package] basepython = python3.11 deps = poetry twine commands = poetry build twine check dist/*