mirror of
https://github.com/django/daphne.git
synced 2025-08-04 02:20:08 +03:00
Compare commits
No commits in common. "main" and "2.4.0" have entirely different histories.
11
.flake8
11
.flake8
|
@ -1,11 +0,0 @@
|
||||||
[flake8]
|
|
||||||
exclude =
|
|
||||||
.venv,
|
|
||||||
.tox,
|
|
||||||
docs,
|
|
||||||
testproject,
|
|
||||||
js_client,
|
|
||||||
.eggs
|
|
||||||
|
|
||||||
extend-ignore = E123, E128, E266, E402, W503, E731, W601, B036
|
|
||||||
max-line-length = 120
|
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
|
@ -1,6 +0,0 @@
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
43
.github/workflows/tests.yml
vendored
43
.github/workflows/tests.yml
vendored
|
@ -1,43 +0,0 @@
|
||||||
name: Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tests:
|
|
||||||
runs-on: ${{ matrix.os }}-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- ubuntu
|
|
||||||
- windows
|
|
||||||
python-version:
|
|
||||||
- "3.9"
|
|
||||||
- "3.10"
|
|
||||||
- "3.11"
|
|
||||||
- "3.12"
|
|
||||||
- "3.13"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip setuptools wheel
|
|
||||||
python -m pip install --upgrade tox
|
|
||||||
|
|
||||||
- name: Run tox targets for ${{ matrix.python-version }}
|
|
||||||
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -10,7 +10,3 @@ build/
|
||||||
.eggs
|
.eggs
|
||||||
test_layer*
|
test_layer*
|
||||||
test_consumer*
|
test_consumer*
|
||||||
.python-version
|
|
||||||
.pytest_cache/
|
|
||||||
.vscode
|
|
||||||
.coverage
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
repos:
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
|
||||||
rev: v3.20.0
|
|
||||||
hooks:
|
|
||||||
- id: pyupgrade
|
|
||||||
args: [--py39-plus]
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 25.1.0
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
language_version: python3
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
rev: 6.0.1
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 7.3.0
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies:
|
|
||||||
- flake8-bugbear
|
|
||||||
ci:
|
|
||||||
autoupdate_schedule: quarterly
|
|
57
.travis.yml
Normal file
57
.travis.yml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
language: python
|
||||||
|
|
||||||
|
python:
|
||||||
|
- '3.6'
|
||||||
|
- '3.5'
|
||||||
|
|
||||||
|
env:
|
||||||
|
- TWISTED="twisted"
|
||||||
|
- TWISTED="twisted==18.7.0"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- pip install $TWISTED -e .[tests]
|
||||||
|
- pip freeze
|
||||||
|
|
||||||
|
script:
|
||||||
|
- pytest
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- test
|
||||||
|
- name: release
|
||||||
|
if: tag IS present
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
include:
|
||||||
|
- python: '3.7'
|
||||||
|
env: TWISTED="twisted==18.7.0"
|
||||||
|
dist: xenial
|
||||||
|
sudo: required
|
||||||
|
- python: '3.7'
|
||||||
|
env: TWISTED="twisted"
|
||||||
|
dist: xenial
|
||||||
|
sudo: required
|
||||||
|
- python: '3.8'
|
||||||
|
env: TWISTED="twisted"
|
||||||
|
dist: xenial
|
||||||
|
sudo: required
|
||||||
|
|
||||||
|
- stage: lint
|
||||||
|
install: pip install -U -e .[tests] black pyflakes isort
|
||||||
|
script:
|
||||||
|
- pyflakes daphne tests
|
||||||
|
- black --check daphne tests
|
||||||
|
- isort --check-only --diff --recursive daphne tests
|
||||||
|
|
||||||
|
- stage: release
|
||||||
|
script: skip
|
||||||
|
deploy:
|
||||||
|
provider: pypi
|
||||||
|
user: andrewgodwin_bot
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
distributions: sdist bdist_wheel
|
||||||
|
password:
|
||||||
|
secure: IA+dvSmMKN+fT47rgRb6zdmrExhK5QCVEDH8kheC6kAacw80ORBZKo6sMX9GQBJ3BlfhTqrzAhItHkDUxonb579rJDvmlJ7FPg7axZpsY9Fmls6q1rJC/La8iGWx20+ctberejKSH3wSwa0LH0imJXGDoKKzf1DLmk5pEEWjG2QqhKdEtyAcnzOPnDWcRCs+DKfQcMzETH7lMFN8oe3aBhHLLtcg4yA78cN5CeyyH92lmbaVp7k/b1FqXXFgf16bi5tlgLrb6DhmcnNjwLMSHRafNoPCXkWQOwh6gEHeHRR3OsHsBueyJHIikuHNrpmgpAqjYlVQ5WqmfgMlhCfRm9xL+G4G+KK9n8AJNGAszUfxVlPvMTw+nkOSd/bmxKrdCqqYnDIvDLucXJ86TstNzklfAwr3FL+wBlucRtOMLhQlHIaPTXYcNpOuh6B4ELjC+WjDGh8EdRKvcsZz7+5AS5ZaDDccuviMzQFsXVcE2d4HiosbARVrkxJ7j3MWp0OGgWVxXgRO2EQIksbgGSIjI8PqFjBqht2WT6MhVZPCc9XHUlP2CiAR5+QY8JgTIztbEDuhpgr0cRAtiHwJEAxDR9tJR/j/v4X/Pau2ZdR0C0yW77lVgD75spLL0khAnU7q+qgiF0hyQ7gRRVy0tElT0HBenVbzjzHowdJX8lSPjRg=
|
160
CHANGELOG.txt
160
CHANGELOG.txt
|
@ -1,161 +1,3 @@
|
||||||
4.2.1 (2025-07-02)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fixed a packaging error in 4.2.0.
|
|
||||||
|
|
||||||
* Removed --nostatic and --insecure args to runserver command when staticfiles
|
|
||||||
app is not installed.
|
|
||||||
|
|
||||||
4.2.0 (2025-05-16)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Daphne 4.2 is a maintenance release in the 4.x series.
|
|
||||||
|
|
||||||
* Added support for Python 3.13.
|
|
||||||
|
|
||||||
* Dropped support for EOL Python 3.8.
|
|
||||||
|
|
||||||
* Updated pyupgrade configuration to target Python 3.9.
|
|
||||||
|
|
||||||
* Added a `load_asgi_app` hook to CLI class, useful for compiled or frozen
|
|
||||||
applications.
|
|
||||||
|
|
||||||
* Allowed assigning a port in the DaphneProcess test helper, useful for live
|
|
||||||
server test cases, such as that provided by Channels.
|
|
||||||
|
|
||||||
* Added --nostatic and --insecure args to runserver command to match Django's
|
|
||||||
built-in command.
|
|
||||||
|
|
||||||
* Moved metadata to use pyproject.toml.
|
|
||||||
|
|
||||||
* Updated sdist file to include tests and changelog.
|
|
||||||
|
|
||||||
* Removed unused pytest-runner.
|
|
||||||
|
|
||||||
4.1.2 (2024-04-11)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fixed a setuptools configuration error in 4.1.1.
|
|
||||||
|
|
||||||
4.1.1 (2024-04-10)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fixed a twisted.plugin packaging error in 4.1.0.
|
|
||||||
|
|
||||||
Thanks to sdc50.
|
|
||||||
|
|
||||||
4.1.0 (2024-02-10)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Added support for Python 3.12.
|
|
||||||
|
|
||||||
* Dropped support for EOL Python 3.7.
|
|
||||||
|
|
||||||
* Handled root path for websocket scopes.
|
|
||||||
|
|
||||||
* Validate HTTP header names as per RFC 9110.
|
|
||||||
|
|
||||||
4.0.0 (2022-10-07)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Major versioning targeting use with Channels 4.0 and beyond. Except where
|
|
||||||
noted should remain usable with Channels v3 projects, but updating Channels to the latest version is recommended.
|
|
||||||
|
|
||||||
* Added a ``runserver`` command to run an ASGI Django development server.
|
|
||||||
|
|
||||||
Added ``"daphne"`` to the ``INSTALLED_APPS`` setting, before
|
|
||||||
``"django.contrib.staticfiles"`` to enable:
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
"daphne",
|
|
||||||
...
|
|
||||||
]
|
|
||||||
|
|
||||||
This replaces the Channels implementation of ``runserver``, which is removed
|
|
||||||
in Channels 4.0.
|
|
||||||
|
|
||||||
* Made the ``DaphneProcess`` tests helper class compatible with the ``spawn``
|
|
||||||
process start method, which is used on macOS and Windows.
|
|
||||||
|
|
||||||
Note that requires Channels v4 if using with ``ChannelsLiveServerTestCase``.
|
|
||||||
|
|
||||||
* Dropped support for Python 3.6.
|
|
||||||
|
|
||||||
* Updated dependencies to the latest versions.
|
|
||||||
|
|
||||||
Previously a range of Twisted versions have been supported. Recent Twisted
|
|
||||||
releases (22.2, 22.4) have issued security fixes, so those are now the
|
|
||||||
minimum supported version. Given the stability of Twisted, supporting a
|
|
||||||
range of versions does not represent a good use of maintainer time. Going
|
|
||||||
forward the latest Twisted version will be required.
|
|
||||||
|
|
||||||
* Set ``daphne`` as default ``Server`` header.
|
|
||||||
|
|
||||||
This can be configured with the ``--server-name`` CLI argument.
|
|
||||||
|
|
||||||
Added the new ``--no-server-name`` CLI argument to disable the ``Server``
|
|
||||||
header, which is equivalent to ``--server-name=` (an empty name).
|
|
||||||
|
|
||||||
* Added ``--log-fmt`` CLI argument.
|
|
||||||
|
|
||||||
* Added support for ``ASGI_THREADS`` environment variable, setting the maximum
|
|
||||||
number of workers used by a ``SyncToAsync`` thread-pool executor.
|
|
||||||
|
|
||||||
Set e.g. ``ASGI_THREADS=4 daphne ...`` when running to limit the number of
|
|
||||||
workers.
|
|
||||||
|
|
||||||
* Removed deprecated ``--ws_protocols`` CLI option.
|
|
||||||
|
|
||||||
3.0.2 (2021-04-07)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fixed a bug where ``send`` passed to applications wasn't a true async
|
|
||||||
function but a lambda wrapper, preventing it from being used with
|
|
||||||
``asgiref.sync.async_to_sync()``.
|
|
||||||
|
|
||||||
|
|
||||||
3.0.1 (2020-11-12)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fixed a bug where ``asyncio.CancelledError`` was not correctly handled on
|
|
||||||
Python 3.8+, resulting in incorrect protocol application cleanup.
|
|
||||||
|
|
||||||
|
|
||||||
3.0.0 (2020-10-28)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Updates internals to use ASGI v3 throughout. ``asgiref.compatibility`` is
|
|
||||||
used for older applications.
|
|
||||||
|
|
||||||
* Consequently, the `--asgi-protocol` command-line option is removed.
|
|
||||||
|
|
||||||
* HTTP request bodies are now read, and passed to the application, in chunks.
|
|
||||||
|
|
||||||
* Added support for Python 3.9.
|
|
||||||
|
|
||||||
* Dropped support for Python 3.5.
|
|
||||||
|
|
||||||
|
|
||||||
2.5.0 (2020-04-15)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Fixes compatability for twisted when running Python 3.8+ on Windows, by
|
|
||||||
setting ``asyncio.WindowsSelectorEventLoopPolicy`` as the event loop policy
|
|
||||||
in this case.
|
|
||||||
|
|
||||||
* The internal ``daphne.testing.TestApplication`` now requires an addition
|
|
||||||
``lock`` argument to ``__init__()``. This is expected to be an instance of
|
|
||||||
``multiprocessing.Lock``.
|
|
||||||
|
|
||||||
|
|
||||||
2.4.1 (2019-12-18)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
* Avoids Twisted using the default event loop, for compatibility with Django
|
|
||||||
3.0's ``async_unsafe()`` decorator in threaded contexts, such as using the
|
|
||||||
auto-reloader.
|
|
||||||
|
|
||||||
|
|
||||||
2.4.0 (2019-11-20)
|
2.4.0 (2019-11-20)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
@ -173,13 +15,11 @@ noted should remain usable with Channels v3 projects, but updating Channels to t
|
||||||
|
|
||||||
* Adds missing LICENSE to distribution.
|
* Adds missing LICENSE to distribution.
|
||||||
|
|
||||||
|
|
||||||
2.3.0 (2019-04-09)
|
2.3.0 (2019-04-09)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
* Added support for ASGI v3.
|
* Added support for ASGI v3.
|
||||||
|
|
||||||
|
|
||||||
2.2.5 (2019-01-31)
|
2.2.5 (2019-01-31)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include CHANGELOG.txt
|
|
||||||
recursive-include tests *.py
|
|
||||||
|
|
21
README.rst
21
README.rst
|
@ -1,17 +1,24 @@
|
||||||
daphne
|
daphne
|
||||||
======
|
======
|
||||||
|
|
||||||
|
.. image:: https://api.travis-ci.org/django/daphne.svg
|
||||||
|
:target: https://travis-ci.org/django/daphne
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/daphne.svg
|
.. image:: https://img.shields.io/pypi/v/daphne.svg
|
||||||
:target: https://pypi.python.org/pypi/daphne
|
:target: https://pypi.python.org/pypi/daphne
|
||||||
|
|
||||||
Daphne is a HTTP, HTTP2 and WebSocket protocol server for
|
Daphne is a HTTP, HTTP2 and WebSocket protocol server for
|
||||||
`ASGI <https://github.com/django/asgiref/blob/main/specs/asgi.rst>`_ and
|
`ASGI <https://github.com/django/asgiref/blob/master/specs/asgi.rst>`_ and
|
||||||
`ASGI-HTTP <https://github.com/django/asgiref/blob/main/specs/www.rst>`_,
|
`ASGI-HTTP <https://github.com/django/asgiref/blob/master/specs/www.rst>`_,
|
||||||
developed to power Django Channels.
|
developed to power Django Channels.
|
||||||
|
|
||||||
It supports automatic negotiation of protocols; there's no need for URL
|
It supports automatic negotiation of protocols; there's no need for URL
|
||||||
prefixing to determine WebSocket endpoints versus HTTP endpoints.
|
prefixing to determine WebSocket endpoints versus HTTP endpoints.
|
||||||
|
|
||||||
|
*Note:* Daphne 2 is not compatible with Channels 1.x applications, only with
|
||||||
|
Channels 2.x and other ASGI applications. Install a 1.x version of Daphne
|
||||||
|
for Channels 1.x support.
|
||||||
|
|
||||||
|
|
||||||
Running
|
Running
|
||||||
-------
|
-------
|
||||||
|
@ -54,7 +61,7 @@ Daphne supports terminating HTTP/2 connections natively. You'll
|
||||||
need to do a couple of things to get it working, though. First, you need to
|
need to do a couple of things to get it working, though. First, you need to
|
||||||
make sure you install the Twisted ``http2`` and ``tls`` extras::
|
make sure you install the Twisted ``http2`` and ``tls`` extras::
|
||||||
|
|
||||||
pip install -U "Twisted[tls,http2]"
|
pip install -U Twisted[tls,http2]
|
||||||
|
|
||||||
Next, because all current browsers only support HTTP/2 when using TLS, you will
|
Next, because all current browsers only support HTTP/2 when using TLS, you will
|
||||||
need to start Daphne with TLS turned on, which can be done using the Twisted endpoint syntax::
|
need to start Daphne with TLS turned on, which can be done using the Twisted endpoint syntax::
|
||||||
|
@ -108,19 +115,19 @@ should start with a slash, but not end with one; for example::
|
||||||
Python Support
|
Python Support
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Daphne requires Python 3.9 or later.
|
Daphne requires Python 3.5 or later.
|
||||||
|
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Please refer to the
|
Please refer to the
|
||||||
`main Channels contributing docs <https://github.com/django/channels/blob/main/CONTRIBUTING.rst>`_.
|
`main Channels contributing docs <https://github.com/django/channels/blob/master/CONTRIBUTING.rst>`_.
|
||||||
|
|
||||||
To run tests, make sure you have installed the ``tests`` extra with the package::
|
To run tests, make sure you have installed the ``tests`` extra with the package::
|
||||||
|
|
||||||
cd daphne/
|
cd daphne/
|
||||||
pip install -e '.[tests]'
|
pip install -e .[tests]
|
||||||
pytest
|
pytest
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,4 +141,4 @@ https://docs.djangoproject.com/en/dev/internals/security/.
|
||||||
To report bugs or request new features, please open a new GitHub issue.
|
To report bugs or request new features, please open a new GitHub issue.
|
||||||
|
|
||||||
This repository is part of the Channels project. For the shepherd and maintenance team, please see the
|
This repository is part of the Channels project. For the shepherd and maintenance team, please see the
|
||||||
`main Channels readme <https://github.com/django/channels/blob/main/README.rst>`_.
|
`main Channels readme <https://github.com/django/channels/blob/master/README.rst>`_.
|
||||||
|
|
|
@ -1,14 +1 @@
|
||||||
import sys
|
__version__ = "2.4.0"
|
||||||
|
|
||||||
__version__ = "4.2.1"
|
|
||||||
|
|
||||||
|
|
||||||
# Windows on Python 3.8+ uses ProactorEventLoop, which is not compatible with
|
|
||||||
# Twisted. Does not implement add_writer/add_reader.
|
|
||||||
# See https://bugs.python.org/issue37373
|
|
||||||
# and https://twistedmatrix.com/trac/ticket/9766
|
|
||||||
PY38_WIN = sys.version_info >= (3, 8) and sys.platform == "win32"
|
|
||||||
if PY38_WIN:
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from daphne.cli import CommandLineInterface
|
|
||||||
|
|
||||||
CommandLineInterface.entrypoint()
|
|
|
@ -1,7 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
class AccessLogGenerator:
|
class AccessLogGenerator(object):
|
||||||
"""
|
"""
|
||||||
Object that implements the Daphne "action logger" internal interface in
|
Object that implements the Daphne "action logger" internal interface in
|
||||||
order to provide an access log in something resembling NCSA format.
|
order to provide an access log in something resembling NCSA format.
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Import the server here to ensure the reactor is installed very early on in case other
|
|
||||||
# packages import twisted.internet.reactor (e.g. raven does this).
|
|
||||||
from django.apps import AppConfig
|
|
||||||
from django.core import checks
|
|
||||||
|
|
||||||
import daphne.server # noqa: F401
|
|
||||||
|
|
||||||
from .checks import check_daphne_installed
|
|
||||||
|
|
||||||
|
|
||||||
class DaphneConfig(AppConfig):
|
|
||||||
name = "daphne"
|
|
||||||
verbose_name = "Daphne"
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
checks.register(check_daphne_installed, checks.Tags.staticfiles)
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Django system check to ensure daphne app is listed in INSTALLED_APPS before django.contrib.staticfiles.
|
|
||||||
from django.core.checks import Error, register
|
|
||||||
|
|
||||||
|
|
||||||
@register()
|
|
||||||
def check_daphne_installed(app_configs, **kwargs):
|
|
||||||
from django.apps import apps
|
|
||||||
from django.contrib.staticfiles.apps import StaticFilesConfig
|
|
||||||
|
|
||||||
from daphne.apps import DaphneConfig
|
|
||||||
|
|
||||||
for app in apps.get_app_configs():
|
|
||||||
if isinstance(app, DaphneConfig):
|
|
||||||
return []
|
|
||||||
if isinstance(app, StaticFilesConfig):
|
|
||||||
return [
|
|
||||||
Error(
|
|
||||||
"Daphne must be listed before django.contrib.staticfiles in INSTALLED_APPS.",
|
|
||||||
id="daphne.E001",
|
|
||||||
)
|
|
||||||
]
|
|
|
@ -1,9 +1,10 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentError, Namespace
|
from argparse import ArgumentError, Namespace
|
||||||
|
|
||||||
from asgiref.compatibility import guarantee_single_callable
|
from asgiref.compatibility import is_double_callable
|
||||||
|
|
||||||
from .access import AccessLogGenerator
|
from .access import AccessLogGenerator
|
||||||
from .endpoints import build_endpoint_description_strings
|
from .endpoints import build_endpoint_description_strings
|
||||||
|
@ -16,7 +17,20 @@ DEFAULT_HOST = "127.0.0.1"
|
||||||
DEFAULT_PORT = 8000
|
DEFAULT_PORT = 8000
|
||||||
|
|
||||||
|
|
||||||
class CommandLineInterface:
|
class ASGI3Middleware:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def __call__(self, scope):
|
||||||
|
scope.setdefault("asgi", {})
|
||||||
|
scope["asgi"]["version"] = "3.0"
|
||||||
|
return functools.partial(self.asgi, scope=scope)
|
||||||
|
|
||||||
|
async def asgi(self, receive, send, scope):
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandLineInterface(object):
|
||||||
"""
|
"""
|
||||||
Acts as the main CLI entry point for running the server.
|
Acts as the main CLI entry point for running the server.
|
||||||
"""
|
"""
|
||||||
|
@ -90,11 +104,6 @@ class CommandLineInterface:
|
||||||
help="Where to write the access log (- for stdout, the default for verbosity=1)",
|
help="Where to write the access log (- for stdout, the default for verbosity=1)",
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
|
||||||
"--log-fmt",
|
|
||||||
help="Log format to use",
|
|
||||||
default="%(asctime)-15s %(levelname)-8s %(message)s",
|
|
||||||
)
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
"--ping-interval",
|
"--ping-interval",
|
||||||
type=int,
|
type=int,
|
||||||
|
@ -113,6 +122,20 @@ class CommandLineInterface:
|
||||||
help="The number of seconds an ASGI application has to exit after client disconnect before it is killed",
|
help="The number of seconds an ASGI application has to exit after client disconnect before it is killed",
|
||||||
default=10,
|
default=10,
|
||||||
)
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
"--ws-protocol",
|
||||||
|
nargs="*",
|
||||||
|
dest="ws_protocols",
|
||||||
|
help="The WebSocket protocols you wish to support",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
"--asgi-protocol",
|
||||||
|
dest="asgi_protocol",
|
||||||
|
help="The version of the ASGI protocol to use",
|
||||||
|
default="auto",
|
||||||
|
choices=["asgi2", "asgi3", "auto"],
|
||||||
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
"--root-path",
|
"--root-path",
|
||||||
dest="root_path",
|
dest="root_path",
|
||||||
|
@ -155,10 +178,7 @@ class CommandLineInterface:
|
||||||
"--server-name",
|
"--server-name",
|
||||||
dest="server_name",
|
dest="server_name",
|
||||||
help="specify which value should be passed to response header Server attribute",
|
help="specify which value should be passed to response header Server attribute",
|
||||||
default="daphne",
|
default="Daphne",
|
||||||
)
|
|
||||||
self.parser.add_argument(
|
|
||||||
"--no-server-name", dest="server_name", action="store_const", const=""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.server = None
|
self.server = None
|
||||||
|
@ -201,12 +221,6 @@ class CommandLineInterface:
|
||||||
if args.proxy_headers:
|
if args.proxy_headers:
|
||||||
return "X-Forwarded-Port"
|
return "X-Forwarded-Port"
|
||||||
|
|
||||||
def load_asgi_app(self, asgi_app_path: str):
|
|
||||||
"""
|
|
||||||
Return the imported application.
|
|
||||||
"""
|
|
||||||
return import_by_path(asgi_app_path)
|
|
||||||
|
|
||||||
def run(self, args):
|
def run(self, args):
|
||||||
"""
|
"""
|
||||||
Pass in raw argument list and it will decode them
|
Pass in raw argument list and it will decode them
|
||||||
|
@ -222,7 +236,7 @@ class CommandLineInterface:
|
||||||
2: logging.DEBUG,
|
2: logging.DEBUG,
|
||||||
3: logging.DEBUG, # Also turns on asyncio debug
|
3: logging.DEBUG, # Also turns on asyncio debug
|
||||||
}[args.verbosity],
|
}[args.verbosity],
|
||||||
format=args.log_fmt,
|
format="%(asctime)-15s %(levelname)-8s %(message)s",
|
||||||
)
|
)
|
||||||
# If verbosity is 1 or greater, or they told us explicitly, set up access log
|
# If verbosity is 1 or greater, or they told us explicitly, set up access log
|
||||||
access_log_stream = None
|
access_log_stream = None
|
||||||
|
@ -233,11 +247,16 @@ class CommandLineInterface:
|
||||||
access_log_stream = open(args.access_log, "a", 1)
|
access_log_stream = open(args.access_log, "a", 1)
|
||||||
elif args.verbosity >= 1:
|
elif args.verbosity >= 1:
|
||||||
access_log_stream = sys.stdout
|
access_log_stream = sys.stdout
|
||||||
|
|
||||||
# Import application
|
# Import application
|
||||||
sys.path.insert(0, ".")
|
sys.path.insert(0, ".")
|
||||||
application = self.load_asgi_app(args.application)
|
application = import_by_path(args.application)
|
||||||
application = guarantee_single_callable(application)
|
|
||||||
|
asgi_protocol = args.asgi_protocol
|
||||||
|
if asgi_protocol == "auto":
|
||||||
|
asgi_protocol = "asgi2" if is_double_callable(application) else "asgi3"
|
||||||
|
|
||||||
|
if asgi_protocol == "asgi3":
|
||||||
|
application = ASGI3Middleware(application)
|
||||||
|
|
||||||
# Set up port/host bindings
|
# Set up port/host bindings
|
||||||
if not any(
|
if not any(
|
||||||
|
@ -265,7 +284,7 @@ class CommandLineInterface:
|
||||||
)
|
)
|
||||||
endpoints = sorted(args.socket_strings + endpoints)
|
endpoints = sorted(args.socket_strings + endpoints)
|
||||||
# Start the server
|
# Start the server
|
||||||
logger.info("Starting server at {}".format(", ".join(endpoints)))
|
logger.info("Starting server at %s" % (", ".join(endpoints),))
|
||||||
self.server = self.server_class(
|
self.server = self.server_class(
|
||||||
application=application,
|
application=application,
|
||||||
endpoints=endpoints,
|
endpoints=endpoints,
|
||||||
|
@ -276,18 +295,17 @@ class CommandLineInterface:
|
||||||
websocket_connect_timeout=args.websocket_connect_timeout,
|
websocket_connect_timeout=args.websocket_connect_timeout,
|
||||||
websocket_handshake_timeout=args.websocket_connect_timeout,
|
websocket_handshake_timeout=args.websocket_connect_timeout,
|
||||||
application_close_timeout=args.application_close_timeout,
|
application_close_timeout=args.application_close_timeout,
|
||||||
action_logger=(
|
action_logger=AccessLogGenerator(access_log_stream)
|
||||||
AccessLogGenerator(access_log_stream) if access_log_stream else None
|
if access_log_stream
|
||||||
),
|
else None,
|
||||||
|
ws_protocols=args.ws_protocols,
|
||||||
root_path=args.root_path,
|
root_path=args.root_path,
|
||||||
verbosity=args.verbosity,
|
verbosity=args.verbosity,
|
||||||
proxy_forwarded_address_header=self._get_forwarded_host(args=args),
|
proxy_forwarded_address_header=self._get_forwarded_host(args=args),
|
||||||
proxy_forwarded_port_header=self._get_forwarded_port(args=args),
|
proxy_forwarded_port_header=self._get_forwarded_port(args=args),
|
||||||
proxy_forwarded_proto_header=(
|
proxy_forwarded_proto_header="X-Forwarded-Proto"
|
||||||
"X-Forwarded-Proto" if args.proxy_headers else None
|
if args.proxy_headers
|
||||||
),
|
else None,
|
||||||
server_name=args.server_name,
|
server_name=args.server_name,
|
||||||
)
|
)
|
||||||
self.server.run()
|
self.server.run()
|
||||||
if self.server.abort_start:
|
|
||||||
exit(1)
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from twisted.protocols.policies import ProtocolWrapper
|
||||||
from twisted.web import http
|
from twisted.web import http
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
|
|
||||||
from .utils import HEADER_NAME_RE, parse_x_forwarded_for
|
from .utils import parse_x_forwarded_for
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -50,8 +50,6 @@ class WebRequest(http.Request):
|
||||||
) # Shorten it a bit, bytes wise
|
) # Shorten it a bit, bytes wise
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.client_addr = None
|
|
||||||
self.server_addr = None
|
|
||||||
try:
|
try:
|
||||||
http.Request.__init__(self, *args, **kwargs)
|
http.Request.__init__(self, *args, **kwargs)
|
||||||
# Easy server link
|
# Easy server link
|
||||||
|
@ -69,13 +67,6 @@ class WebRequest(http.Request):
|
||||||
def process(self):
|
def process(self):
|
||||||
try:
|
try:
|
||||||
self.request_start = time.time()
|
self.request_start = time.time()
|
||||||
|
|
||||||
# Validate header names.
|
|
||||||
for name, _ in self.requestHeaders.getAllRawHeaders():
|
|
||||||
if not HEADER_NAME_RE.fullmatch(name):
|
|
||||||
self.basic_error(400, b"Bad Request", "Invalid header name")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get upgrade header
|
# Get upgrade header
|
||||||
upgrade_header = None
|
upgrade_header = None
|
||||||
if self.requestHeaders.hasHeader(b"Upgrade"):
|
if self.requestHeaders.hasHeader(b"Upgrade"):
|
||||||
|
@ -86,6 +77,9 @@ class WebRequest(http.Request):
|
||||||
# requires unicode string.
|
# requires unicode string.
|
||||||
self.client_addr = [str(self.client.host), self.client.port]
|
self.client_addr = [str(self.client.host), self.client.port]
|
||||||
self.server_addr = [str(self.host.host), self.host.port]
|
self.server_addr = [str(self.host.host), self.host.port]
|
||||||
|
else:
|
||||||
|
self.client_addr = None
|
||||||
|
self.server_addr = None
|
||||||
|
|
||||||
self.client_scheme = "https" if self.isSecure() else "http"
|
self.client_scheme = "https" if self.isSecure() else "http"
|
||||||
|
|
||||||
|
@ -191,19 +185,9 @@ class WebRequest(http.Request):
|
||||||
# Not much we can do, the request is prematurely abandoned.
|
# Not much we can do, the request is prematurely abandoned.
|
||||||
return
|
return
|
||||||
# Run application against request
|
# Run application against request
|
||||||
buffer_size = self.server.request_buffer_size
|
self.application_queue.put_nowait(
|
||||||
while True:
|
{"type": "http.request", "body": self.content.read()}
|
||||||
chunk = self.content.read(buffer_size)
|
)
|
||||||
more_body = not (len(chunk) < buffer_size)
|
|
||||||
payload = {
|
|
||||||
"type": "http.request",
|
|
||||||
"body": chunk,
|
|
||||||
"more_body": more_body,
|
|
||||||
}
|
|
||||||
self.application_queue.put_nowait(payload)
|
|
||||||
if not more_body:
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
self.basic_error(
|
self.basic_error(
|
||||||
|
@ -256,8 +240,8 @@ class WebRequest(http.Request):
|
||||||
# Write headers
|
# Write headers
|
||||||
for header, value in message.get("headers", {}):
|
for header, value in message.get("headers", {}):
|
||||||
self.responseHeaders.addRawHeader(header, value)
|
self.responseHeaders.addRawHeader(header, value)
|
||||||
if self.server.server_name and not self.responseHeaders.hasHeader("server"):
|
if self.server.server_name and self.server.server_name.lower() != "daphne":
|
||||||
self.setHeader(b"server", self.server.server_name.encode())
|
self.setHeader(b"server", self.server.server_name.encode("utf-8"))
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"HTTP %s response started for %s", message["status"], self.client_addr
|
"HTTP %s response started for %s", message["status"], self.client_addr
|
||||||
)
|
)
|
||||||
|
@ -286,11 +270,9 @@ class WebRequest(http.Request):
|
||||||
"path": uri,
|
"path": uri,
|
||||||
"status": self.code,
|
"status": self.code,
|
||||||
"method": self.method.decode("ascii", "replace"),
|
"method": self.method.decode("ascii", "replace"),
|
||||||
"client": (
|
"client": "%s:%s" % tuple(self.client_addr)
|
||||||
"%s:%s" % tuple(self.client_addr)
|
if self.client_addr
|
||||||
if self.client_addr
|
else None,
|
||||||
else None
|
|
||||||
),
|
|
||||||
"time_taken": self.duration(),
|
"time_taken": self.duration(),
|
||||||
"size": self.sentLength,
|
"size": self.sentLength,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,204 +0,0 @@
|
||||||
import datetime
|
|
||||||
import importlib
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.core.management import CommandError
|
|
||||||
from django.core.management.commands.runserver import Command as RunserverCommand
|
|
||||||
|
|
||||||
from daphne import __version__
|
|
||||||
from daphne.endpoints import build_endpoint_description_strings
|
|
||||||
from daphne.server import Server
|
|
||||||
|
|
||||||
logger = logging.getLogger("django.channels.server")
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_application():
|
|
||||||
"""
|
|
||||||
Gets the default application, set in the ASGI_APPLICATION setting.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
path, name = settings.ASGI_APPLICATION.rsplit(".", 1)
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
raise ImproperlyConfigured("Cannot find ASGI_APPLICATION setting.")
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(path)
|
|
||||||
except ImportError:
|
|
||||||
raise ImproperlyConfigured("Cannot import ASGI_APPLICATION module %r" % path)
|
|
||||||
try:
|
|
||||||
value = getattr(module, name)
|
|
||||||
except AttributeError:
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
f"Cannot find {name!r} in ASGI_APPLICATION module {path}"
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class Command(RunserverCommand):
|
|
||||||
protocol = "http"
|
|
||||||
server_cls = Server
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
super().add_arguments(parser)
|
|
||||||
parser.add_argument(
|
|
||||||
"--noasgi",
|
|
||||||
action="store_false",
|
|
||||||
dest="use_asgi",
|
|
||||||
default=True,
|
|
||||||
help="Run the old WSGI-based runserver rather than the ASGI-based one",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--http_timeout",
|
|
||||||
action="store",
|
|
||||||
dest="http_timeout",
|
|
||||||
type=int,
|
|
||||||
default=None,
|
|
||||||
help=(
|
|
||||||
"Specify the daphne http_timeout interval in seconds "
|
|
||||||
"(default: no timeout)"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--websocket_handshake_timeout",
|
|
||||||
action="store",
|
|
||||||
dest="websocket_handshake_timeout",
|
|
||||||
type=int,
|
|
||||||
default=5,
|
|
||||||
help=(
|
|
||||||
"Specify the daphne websocket_handshake_timeout interval in "
|
|
||||||
"seconds (default: 5)"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if apps.is_installed("django.contrib.staticfiles"):
|
|
||||||
parser.add_argument(
|
|
||||||
"--nostatic",
|
|
||||||
action="store_false",
|
|
||||||
dest="use_static_handler",
|
|
||||||
help="Tells Django to NOT automatically serve static files at STATIC_URL.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--insecure",
|
|
||||||
action="store_true",
|
|
||||||
dest="insecure_serving",
|
|
||||||
help="Allows serving static files even if DEBUG is False.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
self.http_timeout = options.get("http_timeout", None)
|
|
||||||
self.websocket_handshake_timeout = options.get("websocket_handshake_timeout", 5)
|
|
||||||
# Check Channels is installed right
|
|
||||||
if options["use_asgi"] and not hasattr(settings, "ASGI_APPLICATION"):
|
|
||||||
raise CommandError(
|
|
||||||
"You have not set ASGI_APPLICATION, which is needed to run the server."
|
|
||||||
)
|
|
||||||
# Dispatch upward
|
|
||||||
super().handle(*args, **options)
|
|
||||||
|
|
||||||
def inner_run(self, *args, **options):
|
|
||||||
# Maybe they want the wsgi one?
|
|
||||||
if not options.get("use_asgi", True):
|
|
||||||
if hasattr(RunserverCommand, "server_cls"):
|
|
||||||
self.server_cls = RunserverCommand.server_cls
|
|
||||||
return RunserverCommand.inner_run(self, *args, **options)
|
|
||||||
# Run checks
|
|
||||||
self.stdout.write("Performing system checks...\n\n")
|
|
||||||
self.check(display_num_errors=True)
|
|
||||||
self.check_migrations()
|
|
||||||
# Print helpful text
|
|
||||||
quit_command = "CTRL-BREAK" if sys.platform == "win32" else "CONTROL-C"
|
|
||||||
now = datetime.datetime.now().strftime("%B %d, %Y - %X")
|
|
||||||
self.stdout.write(now)
|
|
||||||
self.stdout.write(
|
|
||||||
(
|
|
||||||
"Django version %(version)s, using settings %(settings)r\n"
|
|
||||||
"Starting ASGI/Daphne version %(daphne_version)s development server"
|
|
||||||
" at %(protocol)s://%(addr)s:%(port)s/\n"
|
|
||||||
"Quit the server with %(quit_command)s.\n"
|
|
||||||
)
|
|
||||||
% {
|
|
||||||
"version": self.get_version(),
|
|
||||||
"daphne_version": __version__,
|
|
||||||
"settings": settings.SETTINGS_MODULE,
|
|
||||||
"protocol": self.protocol,
|
|
||||||
"addr": "[%s]" % self.addr if self._raw_ipv6 else self.addr,
|
|
||||||
"port": self.port,
|
|
||||||
"quit_command": quit_command,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Launch server in 'main' thread. Signals are disabled as it's still
|
|
||||||
# actually a subthread under the autoreloader.
|
|
||||||
logger.debug("Daphne running, listening on %s:%s", self.addr, self.port)
|
|
||||||
|
|
||||||
# build the endpoint description string from host/port options
|
|
||||||
endpoints = build_endpoint_description_strings(host=self.addr, port=self.port)
|
|
||||||
try:
|
|
||||||
self.server_cls(
|
|
||||||
application=self.get_application(options),
|
|
||||||
endpoints=endpoints,
|
|
||||||
signal_handlers=not options["use_reloader"],
|
|
||||||
action_logger=self.log_action,
|
|
||||||
http_timeout=self.http_timeout,
|
|
||||||
root_path=getattr(settings, "FORCE_SCRIPT_NAME", "") or "",
|
|
||||||
websocket_handshake_timeout=self.websocket_handshake_timeout,
|
|
||||||
).run()
|
|
||||||
logger.debug("Daphne exited")
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
shutdown_message = options.get("shutdown_message", "")
|
|
||||||
if shutdown_message:
|
|
||||||
self.stdout.write(shutdown_message)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_application(self, options):
|
|
||||||
"""
|
|
||||||
Returns the static files serving application wrapping the default application,
|
|
||||||
if static files should be served. Otherwise just returns the default
|
|
||||||
handler.
|
|
||||||
"""
|
|
||||||
staticfiles_installed = apps.is_installed("django.contrib.staticfiles")
|
|
||||||
use_static_handler = options.get("use_static_handler", staticfiles_installed)
|
|
||||||
insecure_serving = options.get("insecure_serving", False)
|
|
||||||
if use_static_handler and (settings.DEBUG or insecure_serving):
|
|
||||||
return ASGIStaticFilesHandler(get_default_application())
|
|
||||||
else:
|
|
||||||
return get_default_application()
|
|
||||||
|
|
||||||
def log_action(self, protocol, action, details):
|
|
||||||
"""
|
|
||||||
Logs various different kinds of requests to the console.
|
|
||||||
"""
|
|
||||||
# HTTP requests
|
|
||||||
if protocol == "http" and action == "complete":
|
|
||||||
msg = "HTTP %(method)s %(path)s %(status)s [%(time_taken).2f, %(client)s]"
|
|
||||||
|
|
||||||
# Utilize terminal colors, if available
|
|
||||||
if 200 <= details["status"] < 300:
|
|
||||||
# Put 2XX first, since it should be the common case
|
|
||||||
logger.info(self.style.HTTP_SUCCESS(msg), details)
|
|
||||||
elif 100 <= details["status"] < 200:
|
|
||||||
logger.info(self.style.HTTP_INFO(msg), details)
|
|
||||||
elif details["status"] == 304:
|
|
||||||
logger.info(self.style.HTTP_NOT_MODIFIED(msg), details)
|
|
||||||
elif 300 <= details["status"] < 400:
|
|
||||||
logger.info(self.style.HTTP_REDIRECT(msg), details)
|
|
||||||
elif details["status"] == 404:
|
|
||||||
logger.warning(self.style.HTTP_NOT_FOUND(msg), details)
|
|
||||||
elif 400 <= details["status"] < 500:
|
|
||||||
logger.warning(self.style.HTTP_BAD_REQUEST(msg), details)
|
|
||||||
else:
|
|
||||||
# Any 5XX, or any other response
|
|
||||||
logger.error(self.style.HTTP_SERVER_ERROR(msg), details)
|
|
||||||
|
|
||||||
# Websocket requests
|
|
||||||
elif protocol == "websocket" and action == "connected":
|
|
||||||
logger.info("WebSocket CONNECT %(path)s [%(client)s]", details)
|
|
||||||
elif protocol == "websocket" and action == "disconnected":
|
|
||||||
logger.info("WebSocket DISCONNECT %(path)s [%(client)s]", details)
|
|
||||||
elif protocol == "websocket" and action == "connecting":
|
|
||||||
logger.info("WebSocket HANDSHAKING %(path)s [%(client)s]", details)
|
|
||||||
elif protocol == "websocket" and action == "rejected":
|
|
||||||
logger.info("WebSocket REJECT %(path)s [%(client)s]", details)
|
|
|
@ -1,18 +1,8 @@
|
||||||
# This has to be done first as Twisted is import-order-sensitive with reactors
|
# This has to be done first as Twisted is import-order-sensitive with reactors
|
||||||
import asyncio # isort:skip
|
|
||||||
import os # isort:skip
|
|
||||||
import sys # isort:skip
|
import sys # isort:skip
|
||||||
import warnings # isort:skip
|
import warnings # isort:skip
|
||||||
from concurrent.futures import ThreadPoolExecutor # isort:skip
|
|
||||||
from twisted.internet import asyncioreactor # isort:skip
|
from twisted.internet import asyncioreactor # isort:skip
|
||||||
|
|
||||||
|
|
||||||
twisted_loop = asyncio.new_event_loop()
|
|
||||||
if "ASGI_THREADS" in os.environ:
|
|
||||||
twisted_loop.set_default_executor(
|
|
||||||
ThreadPoolExecutor(max_workers=int(os.environ["ASGI_THREADS"]))
|
|
||||||
)
|
|
||||||
|
|
||||||
current_reactor = sys.modules.get("twisted.internet.reactor", None)
|
current_reactor = sys.modules.get("twisted.internet.reactor", None)
|
||||||
if current_reactor is not None:
|
if current_reactor is not None:
|
||||||
if not isinstance(current_reactor, asyncioreactor.AsyncioSelectorReactor):
|
if not isinstance(current_reactor, asyncioreactor.AsyncioSelectorReactor):
|
||||||
|
@ -21,17 +11,17 @@ if current_reactor is not None:
|
||||||
+ "you can fix this warning by importing daphne.server early in your codebase or "
|
+ "you can fix this warning by importing daphne.server early in your codebase or "
|
||||||
+ "finding the package that imports Twisted and importing it later on.",
|
+ "finding the package that imports Twisted and importing it later on.",
|
||||||
UserWarning,
|
UserWarning,
|
||||||
stacklevel=2,
|
|
||||||
)
|
)
|
||||||
del sys.modules["twisted.internet.reactor"]
|
del sys.modules["twisted.internet.reactor"]
|
||||||
asyncioreactor.install(twisted_loop)
|
asyncioreactor.install()
|
||||||
else:
|
else:
|
||||||
asyncioreactor.install(twisted_loop)
|
asyncioreactor.install()
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
from concurrent.futures import CancelledError
|
from concurrent.futures import CancelledError
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from twisted.internet.endpoints import serverFromString
|
from twisted.internet.endpoints import serverFromString
|
||||||
|
@ -44,7 +34,7 @@ from .ws_protocol import WebSocketFactory
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Server:
|
class Server(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
application,
|
application,
|
||||||
|
@ -52,7 +42,6 @@ class Server:
|
||||||
signal_handlers=True,
|
signal_handlers=True,
|
||||||
action_logger=None,
|
action_logger=None,
|
||||||
http_timeout=None,
|
http_timeout=None,
|
||||||
request_buffer_size=8192,
|
|
||||||
websocket_timeout=86400,
|
websocket_timeout=86400,
|
||||||
websocket_connect_timeout=20,
|
websocket_connect_timeout=20,
|
||||||
ping_interval=20,
|
ping_interval=20,
|
||||||
|
@ -65,7 +54,9 @@ class Server:
|
||||||
websocket_handshake_timeout=5,
|
websocket_handshake_timeout=5,
|
||||||
application_close_timeout=10,
|
application_close_timeout=10,
|
||||||
ready_callable=None,
|
ready_callable=None,
|
||||||
server_name="daphne",
|
server_name="Daphne",
|
||||||
|
# Deprecated and does not work, remove in version 2.2
|
||||||
|
ws_protocols=None,
|
||||||
):
|
):
|
||||||
self.application = application
|
self.application = application
|
||||||
self.endpoints = endpoints or []
|
self.endpoints = endpoints or []
|
||||||
|
@ -76,7 +67,6 @@ class Server:
|
||||||
self.http_timeout = http_timeout
|
self.http_timeout = http_timeout
|
||||||
self.ping_interval = ping_interval
|
self.ping_interval = ping_interval
|
||||||
self.ping_timeout = ping_timeout
|
self.ping_timeout = ping_timeout
|
||||||
self.request_buffer_size = request_buffer_size
|
|
||||||
self.proxy_forwarded_address_header = proxy_forwarded_address_header
|
self.proxy_forwarded_address_header = proxy_forwarded_address_header
|
||||||
self.proxy_forwarded_port_header = proxy_forwarded_port_header
|
self.proxy_forwarded_port_header = proxy_forwarded_port_header
|
||||||
self.proxy_forwarded_proto_header = proxy_forwarded_proto_header
|
self.proxy_forwarded_proto_header = proxy_forwarded_proto_header
|
||||||
|
@ -207,17 +197,15 @@ class Server:
|
||||||
assert "application_instance" not in self.connections[protocol]
|
assert "application_instance" not in self.connections[protocol]
|
||||||
# Make an instance of the application
|
# Make an instance of the application
|
||||||
input_queue = asyncio.Queue()
|
input_queue = asyncio.Queue()
|
||||||
scope.setdefault("asgi", {"version": "3.0"})
|
application_instance = self.application(scope=scope)
|
||||||
application_instance = self.application(
|
|
||||||
scope=scope,
|
|
||||||
receive=input_queue.get,
|
|
||||||
send=partial(self.handle_reply, protocol),
|
|
||||||
)
|
|
||||||
# Run it, and stash the future for later checking
|
# Run it, and stash the future for later checking
|
||||||
if protocol not in self.connections:
|
if protocol not in self.connections:
|
||||||
return None
|
return None
|
||||||
self.connections[protocol]["application_instance"] = asyncio.ensure_future(
|
self.connections[protocol]["application_instance"] = asyncio.ensure_future(
|
||||||
application_instance,
|
application_instance(
|
||||||
|
receive=input_queue.get,
|
||||||
|
send=lambda message: self.handle_reply(protocol, message),
|
||||||
|
),
|
||||||
loop=asyncio.get_event_loop(),
|
loop=asyncio.get_event_loop(),
|
||||||
)
|
)
|
||||||
return input_queue
|
return input_queue
|
||||||
|
@ -285,7 +273,7 @@ class Server:
|
||||||
if application_instance and application_instance.done():
|
if application_instance and application_instance.done():
|
||||||
try:
|
try:
|
||||||
exception = application_instance.exception()
|
exception = application_instance.exception()
|
||||||
except (CancelledError, asyncio.CancelledError):
|
except CancelledError:
|
||||||
# Future cancellation. We can ignore this.
|
# Future cancellation. We can ignore this.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -294,10 +282,13 @@ class Server:
|
||||||
# Protocol is asking the server to exit (likely during test)
|
# Protocol is asking the server to exit (likely during test)
|
||||||
self.stop()
|
self.stop()
|
||||||
else:
|
else:
|
||||||
logger.error(
|
exception_output = "{}\n{}{}".format(
|
||||||
"Exception inside application: %s",
|
|
||||||
exception,
|
exception,
|
||||||
exc_info=exception,
|
"".join(traceback.format_tb(exception.__traceback__)),
|
||||||
|
" {}".format(exception),
|
||||||
|
)
|
||||||
|
logger.error(
|
||||||
|
"Exception inside application: %s", exception_output
|
||||||
)
|
)
|
||||||
if not disconnected:
|
if not disconnected:
|
||||||
protocol.handle_exception(exception)
|
protocol.handle_exception(exception)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import traceback
|
||||||
from concurrent.futures import CancelledError
|
from concurrent.futures import CancelledError
|
||||||
|
|
||||||
|
|
||||||
class BaseDaphneTestingInstance:
|
class DaphneTestingInstance:
|
||||||
"""
|
"""
|
||||||
Launches an instance of Daphne in a subprocess, with a host and port
|
Launches an instance of Daphne in a subprocess, with a host and port
|
||||||
attribute allowing you to call it.
|
attribute allowing you to call it.
|
||||||
|
@ -17,23 +17,17 @@ class BaseDaphneTestingInstance:
|
||||||
|
|
||||||
startup_timeout = 2
|
startup_timeout = 2
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, xff=False, http_timeout=None):
|
||||||
self, xff=False, http_timeout=None, request_buffer_size=None, *, application
|
|
||||||
):
|
|
||||||
self.xff = xff
|
self.xff = xff
|
||||||
self.http_timeout = http_timeout
|
self.http_timeout = http_timeout
|
||||||
self.host = "127.0.0.1"
|
self.host = "127.0.0.1"
|
||||||
self.request_buffer_size = request_buffer_size
|
|
||||||
self.application = application
|
|
||||||
|
|
||||||
def get_application(self):
|
|
||||||
return self.application
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
# Clear result storage
|
||||||
|
TestApplication.delete_setup()
|
||||||
|
TestApplication.delete_result()
|
||||||
# Option Daphne features
|
# Option Daphne features
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if self.request_buffer_size:
|
|
||||||
kwargs["request_buffer_size"] = self.request_buffer_size
|
|
||||||
# Optionally enable X-Forwarded-For support.
|
# Optionally enable X-Forwarded-For support.
|
||||||
if self.xff:
|
if self.xff:
|
||||||
kwargs["proxy_forwarded_address_header"] = "X-Forwarded-For"
|
kwargs["proxy_forwarded_address_header"] = "X-Forwarded-For"
|
||||||
|
@ -44,7 +38,7 @@ class BaseDaphneTestingInstance:
|
||||||
# Start up process
|
# Start up process
|
||||||
self.process = DaphneProcess(
|
self.process = DaphneProcess(
|
||||||
host=self.host,
|
host=self.host,
|
||||||
get_application=self.get_application,
|
application=TestApplication,
|
||||||
kwargs=kwargs,
|
kwargs=kwargs,
|
||||||
setup=self.process_setup,
|
setup=self.process_setup,
|
||||||
teardown=self.process_teardown,
|
teardown=self.process_teardown,
|
||||||
|
@ -78,21 +72,6 @@ class BaseDaphneTestingInstance:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_received(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DaphneTestingInstance(BaseDaphneTestingInstance):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.lock = multiprocessing.Lock()
|
|
||||||
super().__init__(*args, **kwargs, application=TestApplication(lock=self.lock))
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
# Clear result storage
|
|
||||||
TestApplication.delete_setup()
|
|
||||||
TestApplication.delete_result()
|
|
||||||
return super().__enter__()
|
|
||||||
|
|
||||||
def get_received(self):
|
def get_received(self):
|
||||||
"""
|
"""
|
||||||
Returns the scope and messages the test application has received
|
Returns the scope and messages the test application has received
|
||||||
|
@ -103,8 +82,7 @@ class DaphneTestingInstance(BaseDaphneTestingInstance):
|
||||||
raises them.
|
raises them.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with self.lock:
|
inner_result = TestApplication.load_result()
|
||||||
inner_result = TestApplication.load_result()
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise ValueError("No results available yet.")
|
raise ValueError("No results available yet.")
|
||||||
# Check for exception
|
# Check for exception
|
||||||
|
@ -126,16 +104,14 @@ class DaphneProcess(multiprocessing.Process):
|
||||||
port it ends up listening on back to the parent process.
|
port it ends up listening on back to the parent process.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, host, application, kwargs=None, setup=None, teardown=None):
|
||||||
self, host, get_application, kwargs=None, setup=None, teardown=None, port=None
|
|
||||||
):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.host = host
|
self.host = host
|
||||||
self.get_application = get_application
|
self.application = application
|
||||||
self.kwargs = kwargs or {}
|
self.kwargs = kwargs or {}
|
||||||
self.setup = setup
|
self.setup = setup or (lambda: None)
|
||||||
self.teardown = teardown
|
self.teardown = teardown or (lambda: None)
|
||||||
self.port = multiprocessing.Value("i", port if port is not None else 0)
|
self.port = multiprocessing.Value("i")
|
||||||
self.ready = multiprocessing.Event()
|
self.ready = multiprocessing.Event()
|
||||||
self.errors = multiprocessing.Queue()
|
self.errors = multiprocessing.Queue()
|
||||||
|
|
||||||
|
@ -148,33 +124,27 @@ class DaphneProcess(multiprocessing.Process):
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
from .endpoints import build_endpoint_description_strings
|
|
||||||
from .server import Server
|
from .server import Server
|
||||||
|
from .endpoints import build_endpoint_description_strings
|
||||||
application = self.get_application()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create the server class
|
# Create the server class
|
||||||
endpoints = build_endpoint_description_strings(
|
endpoints = build_endpoint_description_strings(host=self.host, port=0)
|
||||||
host=self.host, port=self.port.value
|
|
||||||
)
|
|
||||||
self.server = Server(
|
self.server = Server(
|
||||||
application=application,
|
application=self.application,
|
||||||
endpoints=endpoints,
|
endpoints=endpoints,
|
||||||
signal_handlers=False,
|
signal_handlers=False,
|
||||||
**self.kwargs,
|
**self.kwargs
|
||||||
)
|
)
|
||||||
# Set up a poller to look for the port
|
# Set up a poller to look for the port
|
||||||
reactor.callLater(0.1, self.resolve_port)
|
reactor.callLater(0.1, self.resolve_port)
|
||||||
# Run with setup/teardown
|
# Run with setup/teardown
|
||||||
if self.setup is not None:
|
self.setup()
|
||||||
self.setup()
|
|
||||||
try:
|
try:
|
||||||
self.server.run()
|
self.server.run()
|
||||||
finally:
|
finally:
|
||||||
if self.teardown is not None:
|
self.teardown()
|
||||||
self.teardown()
|
except Exception as e:
|
||||||
except BaseException as e:
|
|
||||||
# Put the error on our queue so the parent gets it
|
# Put the error on our queue so the parent gets it
|
||||||
self.errors.put((e, traceback.format_exc()))
|
self.errors.put((e, traceback.format_exc()))
|
||||||
|
|
||||||
|
@ -197,22 +167,19 @@ class TestApplication:
|
||||||
setup_storage = os.path.join(tempfile.gettempdir(), "setup.testio")
|
setup_storage = os.path.join(tempfile.gettempdir(), "setup.testio")
|
||||||
result_storage = os.path.join(tempfile.gettempdir(), "result.testio")
|
result_storage = os.path.join(tempfile.gettempdir(), "result.testio")
|
||||||
|
|
||||||
def __init__(self, lock):
|
def __init__(self, scope):
|
||||||
self.lock = lock
|
self.scope = scope
|
||||||
self.messages = []
|
self.messages = []
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
async def __call__(self, send, receive):
|
||||||
self.scope = scope
|
|
||||||
# Receive input and send output
|
# Receive input and send output
|
||||||
logging.debug("test app coroutine alive")
|
logging.debug("test app coroutine alive")
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
# Receive a message and save it into the result store
|
# Receive a message and save it into the result store
|
||||||
self.messages.append(await receive())
|
self.messages.append(await receive())
|
||||||
self.lock.acquire()
|
|
||||||
logging.debug("test app received %r", self.messages[-1])
|
logging.debug("test app received %r", self.messages[-1])
|
||||||
self.save_result(self.scope, self.messages)
|
self.save_result(self.scope, self.messages)
|
||||||
self.lock.release()
|
|
||||||
# See if there are any messages to send back
|
# See if there are any messages to send back
|
||||||
setup = self.load_setup()
|
setup = self.load_setup()
|
||||||
self.delete_setup()
|
self.delete_setup()
|
||||||
|
@ -293,8 +260,8 @@ class TestApplication:
|
||||||
|
|
||||||
|
|
||||||
def _reinstall_reactor():
|
def _reinstall_reactor():
|
||||||
import asyncio
|
|
||||||
import sys
|
import sys
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from twisted.internet import asyncioreactor
|
from twisted.internet import asyncioreactor
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from zope.interface import implementer
|
||||||
|
|
||||||
|
|
||||||
@implementer(IPlugin, IStreamServerEndpointStringParser)
|
@implementer(IPlugin, IStreamServerEndpointStringParser)
|
||||||
class _FDParser:
|
class _FDParser(object):
|
||||||
prefix = "fd"
|
prefix = "fd"
|
||||||
|
|
||||||
def _parseServer(self, reactor, fileno, domain=socket.AF_INET):
|
def _parseServer(self, reactor, fileno, domain=socket.AF_INET):
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import importlib
|
import importlib
|
||||||
import re
|
|
||||||
|
|
||||||
from twisted.web.http_headers import Headers
|
from twisted.web.http_headers import Headers
|
||||||
|
|
||||||
# Header name regex as per h11.
|
|
||||||
# https://github.com/python-hyper/h11/blob/a2c68948accadc3876dffcf979d98002e4a4ed27/h11/_abnf.py#L10-L21
|
|
||||||
HEADER_NAME_RE = re.compile(rb"[-!#$%&'*+.^_`|~0-9a-zA-Z]+")
|
|
||||||
|
|
||||||
|
|
||||||
def import_by_path(path):
|
def import_by_path(path):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -31,21 +31,17 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
||||||
self.server.protocol_connected(self)
|
self.server.protocol_connected(self)
|
||||||
self.request = request
|
self.request = request
|
||||||
self.protocol_to_accept = None
|
self.protocol_to_accept = None
|
||||||
self.root_path = self.server.root_path
|
|
||||||
self.socket_opened = time.time()
|
self.socket_opened = time.time()
|
||||||
self.last_ping = time.time()
|
self.last_ping = time.time()
|
||||||
try:
|
try:
|
||||||
# Sanitize and decode headers, potentially extracting root path
|
# Sanitize and decode headers
|
||||||
self.clean_headers = []
|
self.clean_headers = []
|
||||||
for name, value in request.headers.items():
|
for name, value in request.headers.items():
|
||||||
name = name.encode("ascii")
|
name = name.encode("ascii")
|
||||||
# Prevent CVE-2015-0219
|
# Prevent CVE-2015-0219
|
||||||
if b"_" in name:
|
if b"_" in name:
|
||||||
continue
|
continue
|
||||||
if name.lower() == b"daphne-root-path":
|
self.clean_headers.append((name.lower(), value.encode("latin1")))
|
||||||
self.root_path = unquote(value)
|
|
||||||
else:
|
|
||||||
self.clean_headers.append((name.lower(), value.encode("latin1")))
|
|
||||||
# Get client address if possible
|
# Get client address if possible
|
||||||
peer = self.transport.getPeer()
|
peer = self.transport.getPeer()
|
||||||
host = self.transport.getHost()
|
host = self.transport.getHost()
|
||||||
|
@ -80,7 +76,6 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
||||||
"type": "websocket",
|
"type": "websocket",
|
||||||
"path": unquote(self.path.decode("ascii")),
|
"path": unquote(self.path.decode("ascii")),
|
||||||
"raw_path": self.path,
|
"raw_path": self.path,
|
||||||
"root_path": self.root_path,
|
|
||||||
"headers": self.clean_headers,
|
"headers": self.clean_headers,
|
||||||
"query_string": self._raw_query_string, # Passed by HTTP protocol
|
"query_string": self._raw_query_string, # Passed by HTTP protocol
|
||||||
"client": self.client_addr,
|
"client": self.client_addr,
|
||||||
|
@ -115,9 +110,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
||||||
"connecting",
|
"connecting",
|
||||||
{
|
{
|
||||||
"path": self.request.path,
|
"path": self.request.path,
|
||||||
"client": (
|
"client": "%s:%s" % tuple(self.client_addr)
|
||||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
if self.client_addr
|
||||||
),
|
else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -138,9 +133,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
||||||
"connected",
|
"connected",
|
||||||
{
|
{
|
||||||
"path": self.request.path,
|
"path": self.request.path,
|
||||||
"client": (
|
"client": "%s:%s" % tuple(self.client_addr)
|
||||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
if self.client_addr
|
||||||
),
|
else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -175,9 +170,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
||||||
"disconnected",
|
"disconnected",
|
||||||
{
|
{
|
||||||
"path": self.request.path,
|
"path": self.request.path,
|
||||||
"client": (
|
"client": "%s:%s" % tuple(self.client_addr)
|
||||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
if self.client_addr
|
||||||
),
|
else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -242,9 +237,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
||||||
"rejected",
|
"rejected",
|
||||||
{
|
{
|
||||||
"path": self.request.path,
|
"path": self.request.path,
|
||||||
"client": (
|
"client": "%s:%s" % tuple(self.client_addr)
|
||||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
if self.client_addr
|
||||||
),
|
else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -302,7 +297,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
||||||
return id(self) == id(other)
|
return id(self) == id(other)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<WebSocketProtocol client={self.client_addr!r} path={self.path!r}>"
|
return "<WebSocketProtocol client=%r path=%r>" % (self.client_addr, self.path)
|
||||||
|
|
||||||
|
|
||||||
class WebSocketFactory(WebSocketServerFactory):
|
class WebSocketFactory(WebSocketServerFactory):
|
||||||
|
@ -323,7 +318,7 @@ class WebSocketFactory(WebSocketServerFactory):
|
||||||
Builds protocol instances. We use this to inject the factory object into the protocol.
|
Builds protocol instances. We use this to inject the factory object into the protocol.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
protocol = super().buildProtocol(addr)
|
protocol = super(WebSocketFactory, self).buildProtocol(addr)
|
||||||
protocol.factory = self
|
protocol.factory = self
|
||||||
return protocol
|
return protocol
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "daphne"
|
|
||||||
dynamic = ["version"]
|
|
||||||
description = "Django ASGI (HTTP/WebSocket) server"
|
|
||||||
requires-python = ">=3.9"
|
|
||||||
authors = [
|
|
||||||
{ name = "Django Software Foundation", email = "foundation@djangoproject.com" },
|
|
||||||
]
|
|
||||||
|
|
||||||
license = { text = "BSD" }
|
|
||||||
classifiers = [
|
|
||||||
"Development Status :: 4 - Beta",
|
|
||||||
"Environment :: Web Environment",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"License :: OSI Approved :: BSD License",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
"Programming Language :: Python",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"Programming Language :: Python :: 3.12",
|
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Topic :: Internet :: WWW/HTTP",
|
|
||||||
]
|
|
||||||
|
|
||||||
dependencies = ["asgiref>=3.5.2,<4", "autobahn>=22.4.2", "twisted[tls]>=22.4"]
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
tests = [
|
|
||||||
"django",
|
|
||||||
"hypothesis",
|
|
||||||
"pytest",
|
|
||||||
"pytest-asyncio",
|
|
||||||
"pytest-cov",
|
|
||||||
"black",
|
|
||||||
"tox",
|
|
||||||
"flake8",
|
|
||||||
"flake8-bugbear",
|
|
||||||
"mypy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
homepage = "https://github.com/django/daphne"
|
|
||||||
documentation = "https://channels.readthedocs.io"
|
|
||||||
repository = "https://github.com/django/daphne.git"
|
|
||||||
changelog = "https://github.com/django/daphne/blob/main/CHANGELOG.txt"
|
|
||||||
issues = "https://github.com/django/daphne/issues"
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
daphne = "daphne.cli:CommandLineInterface.entrypoint"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[tool.setuptools]
|
|
||||||
package-dir = { daphne = "daphne", twisted = "daphne/twisted" }
|
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
|
||||||
version = { attr = "daphne.__version__" }
|
|
||||||
readme = { file = "README.rst", content-type = "text/x-rst" }
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
|
|
||||||
[tool.pytest]
|
|
||||||
testpaths = ["tests"]
|
|
||||||
asyncio_mode = "strict"
|
|
||||||
filterwarnings = ["ignore::pytest.PytestDeprecationWarning"]
|
|
||||||
|
|
||||||
[tool.coverage.run]
|
|
||||||
omit = ["tests/*"]
|
|
||||||
concurrency = ["multiprocessing"]
|
|
||||||
|
|
||||||
[tool.coverage.report]
|
|
||||||
show_missing = "true"
|
|
||||||
skip_covered = "true"
|
|
||||||
|
|
||||||
[tool.coverage.html]
|
|
||||||
directory = "reports/coverage_html_report"
|
|
16
setup.cfg
Normal file
16
setup.cfg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[bdist_wheel]
|
||||||
|
universal=1
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
addopts = tests/
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
include_trailing_comma = True
|
||||||
|
multi_line_output = 3
|
||||||
|
known_first_party = channels,daphne,asgiref,channels_redis
|
||||||
|
line_length = 88
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
exclude = venv/*,tox/*,docs/*,testproject/*,js_client/*,.eggs/*
|
||||||
|
ignore = E123,E128,E266,E402,W503,E731,W601
|
||||||
|
max-line-length = 120
|
47
setup.py
Executable file
47
setup.py
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
from daphne import __version__
|
||||||
|
|
||||||
|
# We use the README as the long_description
|
||||||
|
readme_path = os.path.join(os.path.dirname(__file__), "README.rst")
|
||||||
|
with open(readme_path) as fp:
|
||||||
|
long_description = fp.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="daphne",
|
||||||
|
version=__version__,
|
||||||
|
url="https://github.com/django/daphne",
|
||||||
|
author="Django Software Foundation",
|
||||||
|
author_email="foundation@djangoproject.com",
|
||||||
|
description="Django ASGI (HTTP/WebSocket) server",
|
||||||
|
long_description=long_description,
|
||||||
|
license="BSD",
|
||||||
|
zip_safe=False,
|
||||||
|
package_dir={"twisted": "daphne/twisted"},
|
||||||
|
packages=find_packages() + ["twisted.plugins"],
|
||||||
|
include_package_data=True,
|
||||||
|
install_requires=["twisted[tls]>=18.7", "autobahn>=0.18", "asgiref~=3.2"],
|
||||||
|
setup_requires=["pytest-runner"],
|
||||||
|
extras_require={
|
||||||
|
"tests": ["hypothesis==4.23", "pytest~=3.10", "pytest-asyncio~=0.8"]
|
||||||
|
},
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": ["daphne = daphne.cli:CommandLineInterface.entrypoint"]
|
||||||
|
},
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Web Environment",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: BSD License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Topic :: Internet :: WWW/HTTP",
|
||||||
|
],
|
||||||
|
)
|
|
@ -20,24 +20,13 @@ class DaphneTestCase(unittest.TestCase):
|
||||||
### Plain HTTP helpers
|
### Plain HTTP helpers
|
||||||
|
|
||||||
def run_daphne_http(
|
def run_daphne_http(
|
||||||
self,
|
self, method, path, params, body, responses, headers=None, timeout=1, xff=False
|
||||||
method,
|
|
||||||
path,
|
|
||||||
params,
|
|
||||||
body,
|
|
||||||
responses,
|
|
||||||
headers=None,
|
|
||||||
timeout=1,
|
|
||||||
xff=False,
|
|
||||||
request_buffer_size=None,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Runs Daphne with the given request callback (given the base URL)
|
Runs Daphne with the given request callback (given the base URL)
|
||||||
and response messages.
|
and response messages.
|
||||||
"""
|
"""
|
||||||
with DaphneTestingInstance(
|
with DaphneTestingInstance(xff=xff) as test_app:
|
||||||
xff=xff, request_buffer_size=request_buffer_size
|
|
||||||
) as test_app:
|
|
||||||
# Add the response messages
|
# Add the response messages
|
||||||
test_app.add_send_messages(responses)
|
test_app.add_send_messages(responses)
|
||||||
# Send it the request. We have to do this the long way to allow
|
# Send it the request. We have to do this the long way to allow
|
||||||
|
@ -90,14 +79,7 @@ class DaphneTestCase(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_daphne_request(
|
def run_daphne_request(
|
||||||
self,
|
self, method, path, params=None, body=None, headers=None, xff=False
|
||||||
method,
|
|
||||||
path,
|
|
||||||
params=None,
|
|
||||||
body=None,
|
|
||||||
headers=None,
|
|
||||||
xff=False,
|
|
||||||
request_buffer_size=None,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Convenience method for just testing request handling.
|
Convenience method for just testing request handling.
|
||||||
|
@ -110,7 +92,6 @@ class DaphneTestCase(unittest.TestCase):
|
||||||
body=body,
|
body=body,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
xff=xff,
|
xff=xff,
|
||||||
request_buffer_size=request_buffer_size,
|
|
||||||
responses=[
|
responses=[
|
||||||
{"type": "http.response.start", "status": 200},
|
{"type": "http.response.start", "status": 200},
|
||||||
{"type": "http.response.body", "body": b"OK"},
|
{"type": "http.response.body", "body": b"OK"},
|
||||||
|
@ -182,7 +163,7 @@ class DaphneTestCase(unittest.TestCase):
|
||||||
if response.status != 101:
|
if response.status != 101:
|
||||||
raise RuntimeError("WebSocket upgrade did not result in status code 101")
|
raise RuntimeError("WebSocket upgrade did not result in status code 101")
|
||||||
# Prepare headers for subprotocol searching
|
# Prepare headers for subprotocol searching
|
||||||
response_headers = {n.lower(): v for n, v in response.getheaders()}
|
response_headers = dict((n.lower(), v) for n, v in response.getheaders())
|
||||||
response.read()
|
response.read()
|
||||||
assert not response.closed
|
assert not response.closed
|
||||||
# Return the raw socket and any subprotocol
|
# Return the raw socket and any subprotocol
|
||||||
|
@ -252,7 +233,7 @@ class DaphneTestCase(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
socket.inet_aton(address)
|
socket.inet_aton(address)
|
||||||
except OSError:
|
except socket.error:
|
||||||
self.fail("'%s' is not a valid IP address." % address)
|
self.fail("'%s' is not a valid IP address." % address)
|
||||||
|
|
||||||
def assert_key_sets(self, required_keys, optional_keys, actual_keys):
|
def assert_key_sets(self, required_keys, optional_keys, actual_keys):
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import django
|
|
||||||
from django.conf import settings
|
|
||||||
from django.test.utils import override_settings
|
|
||||||
|
|
||||||
from daphne.checks import check_daphne_installed
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_daphne_installed():
|
|
||||||
"""
|
|
||||||
Test check error is raised if daphne is not listed before staticfiles, and vice versa.
|
|
||||||
"""
|
|
||||||
settings.configure(
|
|
||||||
INSTALLED_APPS=["daphne.apps.DaphneConfig", "django.contrib.staticfiles"]
|
|
||||||
)
|
|
||||||
django.setup()
|
|
||||||
errors = check_daphne_installed(None)
|
|
||||||
assert len(errors) == 0
|
|
||||||
with override_settings(INSTALLED_APPS=["django.contrib.staticfiles", "daphne"]):
|
|
||||||
errors = check_daphne_installed(None)
|
|
||||||
assert len(errors) == 1
|
|
||||||
assert errors[0].id == "daphne.E001"
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
# coding: utf8
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from argparse import ArgumentError
|
from argparse import ArgumentError
|
||||||
from unittest import TestCase, skipUnless
|
from unittest import TestCase
|
||||||
|
|
||||||
from daphne.cli import CommandLineInterface
|
from daphne.cli import CommandLineInterface
|
||||||
from daphne.endpoints import build_endpoint_description_strings as build
|
from daphne.endpoints import build_endpoint_description_strings as build
|
||||||
|
@ -81,8 +82,6 @@ class TestCLIInterface(TestCase):
|
||||||
Mock server object for testing.
|
Mock server object for testing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
abort_start = False
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.init_kwargs = kwargs
|
self.init_kwargs = kwargs
|
||||||
|
|
||||||
|
@ -243,27 +242,3 @@ class TestCLIInterface(TestCase):
|
||||||
exc.exception.message,
|
exc.exception.message,
|
||||||
"--proxy-headers has to be passed for this parameter.",
|
"--proxy-headers has to be passed for this parameter.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_custom_servername(self):
|
|
||||||
"""
|
|
||||||
Passing `--server-name` will set the default server header
|
|
||||||
from 'daphne' to the passed one.
|
|
||||||
"""
|
|
||||||
self.assertCLI([], {"server_name": "daphne"})
|
|
||||||
self.assertCLI(["--server-name", ""], {"server_name": ""})
|
|
||||||
self.assertCLI(["--server-name", "python"], {"server_name": "python"})
|
|
||||||
|
|
||||||
def test_no_servername(self):
|
|
||||||
"""
|
|
||||||
Passing `--no-server-name` will set server name to '' (empty string)
|
|
||||||
"""
|
|
||||||
self.assertCLI(["--no-server-name"], {"server_name": ""})
|
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(os.getenv("ASGI_THREADS"), "ASGI_THREADS environment variable not set.")
|
|
||||||
class TestASGIThreads(TestCase):
|
|
||||||
def test_default_executor(self):
|
|
||||||
from daphne.server import twisted_loop
|
|
||||||
|
|
||||||
executor = twisted_loop._default_executor
|
|
||||||
self.assertEqual(executor._max_workers, int(os.getenv("ASGI_THREADS")))
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import unittest
|
|
||||||
|
|
||||||
from daphne.http_protocol import WebRequest
|
|
||||||
|
|
||||||
|
|
||||||
class MockServer:
|
|
||||||
"""
|
|
||||||
Mock server object for testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def protocol_connected(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MockFactory:
|
|
||||||
"""
|
|
||||||
Mock factory object for testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.server = MockServer()
|
|
||||||
|
|
||||||
|
|
||||||
class MockChannel:
|
|
||||||
"""
|
|
||||||
Mock channel object for testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.factory = MockFactory()
|
|
||||||
self.transport = None
|
|
||||||
|
|
||||||
def getPeer(self, *args, **kwargs):
|
|
||||||
return "peer"
|
|
||||||
|
|
||||||
def getHost(self, *args, **kwargs):
|
|
||||||
return "host"
|
|
||||||
|
|
||||||
|
|
||||||
class TestHTTPProtocol(unittest.TestCase):
|
|
||||||
"""
|
|
||||||
Tests the HTTP protocol classes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_web_request_initialisation(self):
|
|
||||||
channel = MockChannel()
|
|
||||||
request = WebRequest(channel)
|
|
||||||
self.assertIsNone(request.client_addr)
|
|
||||||
self.assertIsNone(request.server_addr)
|
|
|
@ -1,10 +1,12 @@
|
||||||
|
# coding: utf8
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
|
from hypothesis import assume, given, settings
|
||||||
|
|
||||||
import http_strategies
|
import http_strategies
|
||||||
from http_base import DaphneTestCase
|
from http_base import DaphneTestCase
|
||||||
from hypothesis import assume, given, settings
|
|
||||||
from hypothesis.strategies import integers
|
|
||||||
|
|
||||||
|
|
||||||
class TestHTTPRequest(DaphneTestCase):
|
class TestHTTPRequest(DaphneTestCase):
|
||||||
|
@ -22,7 +24,6 @@ class TestHTTPRequest(DaphneTestCase):
|
||||||
# Check overall keys
|
# Check overall keys
|
||||||
self.assert_key_sets(
|
self.assert_key_sets(
|
||||||
required_keys={
|
required_keys={
|
||||||
"asgi",
|
|
||||||
"type",
|
"type",
|
||||||
"http_version",
|
"http_version",
|
||||||
"method",
|
"method",
|
||||||
|
@ -34,7 +35,6 @@ class TestHTTPRequest(DaphneTestCase):
|
||||||
optional_keys={"scheme", "root_path", "client", "server"},
|
optional_keys={"scheme", "root_path", "client", "server"},
|
||||||
actual_keys=scope.keys(),
|
actual_keys=scope.keys(),
|
||||||
)
|
)
|
||||||
self.assertEqual(scope["asgi"]["version"], "3.0")
|
|
||||||
# Check that it is the right type
|
# Check that it is the right type
|
||||||
self.assertEqual(scope["type"], "http")
|
self.assertEqual(scope["type"], "http")
|
||||||
# Method (uppercased unicode string)
|
# Method (uppercased unicode string)
|
||||||
|
@ -120,26 +120,6 @@ class TestHTTPRequest(DaphneTestCase):
|
||||||
self.assert_valid_http_scope(scope, "GET", request_path, params=request_params)
|
self.assert_valid_http_scope(scope, "GET", request_path, params=request_params)
|
||||||
self.assert_valid_http_request_message(messages[0], body=b"")
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
||||||
|
|
||||||
@given(request_path=http_strategies.http_path(), chunk_size=integers(min_value=1))
|
|
||||||
@settings(max_examples=5, deadline=5000)
|
|
||||||
def test_request_body_chunking(self, request_path, chunk_size):
|
|
||||||
"""
|
|
||||||
Tests request body chunking logic.
|
|
||||||
"""
|
|
||||||
body = b"The quick brown fox jumps over the lazy dog"
|
|
||||||
_, messages = self.run_daphne_request(
|
|
||||||
"POST",
|
|
||||||
request_path,
|
|
||||||
body=body,
|
|
||||||
request_buffer_size=chunk_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Avoid running those asserts when there's a single "http.disconnect"
|
|
||||||
if len(messages) > 1:
|
|
||||||
assert messages[0]["body"].decode() == body.decode()[:chunk_size]
|
|
||||||
assert not messages[-2]["more_body"]
|
|
||||||
assert messages[-1] == {"type": "http.disconnect"}
|
|
||||||
|
|
||||||
@given(
|
@given(
|
||||||
request_path=http_strategies.http_path(),
|
request_path=http_strategies.http_path(),
|
||||||
request_body=http_strategies.http_body(),
|
request_body=http_strategies.http_body(),
|
||||||
|
@ -304,21 +284,9 @@ class TestHTTPRequest(DaphneTestCase):
|
||||||
response = self.run_daphne_raw(
|
response = self.run_daphne_raw(
|
||||||
b"GET /\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
b"GET /\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
||||||
)
|
)
|
||||||
self.assertTrue(b"400 Bad Request" in response)
|
self.assertTrue(response.startswith(b"HTTP/1.0 400 Bad Request"))
|
||||||
# Bad querystring
|
# Bad querystring
|
||||||
response = self.run_daphne_raw(
|
response = self.run_daphne_raw(
|
||||||
b"GET /?\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
b"GET /?\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
||||||
)
|
)
|
||||||
self.assertTrue(b"400 Bad Request" in response)
|
self.assertTrue(response.startswith(b"HTTP/1.0 400 Bad Request"))
|
||||||
|
|
||||||
def test_invalid_header_name(self):
|
|
||||||
"""
|
|
||||||
Tests that requests with invalid header names fail.
|
|
||||||
"""
|
|
||||||
# Test cases follow those used by h11
|
|
||||||
# https://github.com/python-hyper/h11/blob/a2c68948accadc3876dffcf979d98002e4a4ed27/h11/tests/test_headers.py#L24-L35
|
|
||||||
for header_name in [b"foo bar", b"foo\x00bar", b"foo\xffbar", b"foo\x01bar"]:
|
|
||||||
response = self.run_daphne_raw(
|
|
||||||
f"GET / HTTP/1.0\r\n{header_name}: baz\r\n\r\n".encode("ascii")
|
|
||||||
)
|
|
||||||
self.assertTrue(b"400 Bad Request" in response)
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
# coding: utf8
|
||||||
|
|
||||||
|
from hypothesis import given, settings
|
||||||
|
|
||||||
import http_strategies
|
import http_strategies
|
||||||
from http_base import DaphneTestCase
|
from http_base import DaphneTestCase
|
||||||
from hypothesis import given, settings
|
|
||||||
|
|
||||||
|
|
||||||
class TestHTTPResponse(DaphneTestCase):
|
class TestHTTPResponse(DaphneTestCase):
|
||||||
|
@ -13,11 +16,10 @@ class TestHTTPResponse(DaphneTestCase):
|
||||||
Lowercases and sorts headers, and strips transfer-encoding ones.
|
Lowercases and sorts headers, and strips transfer-encoding ones.
|
||||||
"""
|
"""
|
||||||
return sorted(
|
return sorted(
|
||||||
[(b"server", b"daphne")]
|
[
|
||||||
+ [
|
|
||||||
(name.lower(), value.strip())
|
(name.lower(), value.strip())
|
||||||
for name, value in headers
|
for name, value in headers
|
||||||
if name.lower() not in (b"server", b"transfer-encoding")
|
if name.lower() != b"transfer-encoding"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def test_fd_endpoint_plugin_installed():
|
|
||||||
# Find the site-packages directory
|
|
||||||
for path in sys.path:
|
|
||||||
if "site-packages" in path:
|
|
||||||
site_packages = Path(path)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise AssertionError("Could not find site-packages in sys.path")
|
|
||||||
|
|
||||||
plugin_path = site_packages / "twisted" / "plugins" / "fd_endpoint.py"
|
|
||||||
assert plugin_path.exists(), f"fd_endpoint.py not found at {plugin_path}"
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# coding: utf8
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from twisted.web.http_headers import Headers
|
from twisted.web.http_headers import Headers
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
# coding: utf8
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import time
|
import time
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
import http_strategies
|
|
||||||
from http_base import DaphneTestCase, DaphneTestingInstance
|
|
||||||
from hypothesis import given, settings
|
from hypothesis import given, settings
|
||||||
|
|
||||||
from daphne.testing import BaseDaphneTestingInstance
|
import http_strategies
|
||||||
|
from http_base import DaphneTestCase, DaphneTestingInstance
|
||||||
|
|
||||||
|
|
||||||
class TestWebsocket(DaphneTestCase):
|
class TestWebsocket(DaphneTestCase):
|
||||||
|
@ -23,18 +24,10 @@ class TestWebsocket(DaphneTestCase):
|
||||||
"""
|
"""
|
||||||
# Check overall keys
|
# Check overall keys
|
||||||
self.assert_key_sets(
|
self.assert_key_sets(
|
||||||
required_keys={
|
required_keys={"type", "path", "raw_path", "query_string", "headers"},
|
||||||
"asgi",
|
|
||||||
"type",
|
|
||||||
"path",
|
|
||||||
"raw_path",
|
|
||||||
"query_string",
|
|
||||||
"headers",
|
|
||||||
},
|
|
||||||
optional_keys={"scheme", "root_path", "client", "server", "subprotocols"},
|
optional_keys={"scheme", "root_path", "client", "server", "subprotocols"},
|
||||||
actual_keys=scope.keys(),
|
actual_keys=scope.keys(),
|
||||||
)
|
)
|
||||||
self.assertEqual(scope["asgi"]["version"], "3.0")
|
|
||||||
# Check that it is the right type
|
# Check that it is the right type
|
||||||
self.assertEqual(scope["type"], "websocket")
|
self.assertEqual(scope["type"], "websocket")
|
||||||
# Path
|
# Path
|
||||||
|
@ -192,30 +185,6 @@ class TestWebsocket(DaphneTestCase):
|
||||||
self.assertEqual(scope["path"], "/foo/bar")
|
self.assertEqual(scope["path"], "/foo/bar")
|
||||||
self.assertEqual(scope["raw_path"], b"/foo%2Fbar")
|
self.assertEqual(scope["raw_path"], b"/foo%2Fbar")
|
||||||
|
|
||||||
@given(daphne_path=http_strategies.http_path())
|
|
||||||
@settings(max_examples=5, deadline=2000)
|
|
||||||
def test_root_path(self, *, daphne_path):
|
|
||||||
"""
|
|
||||||
Tests root_path handling.
|
|
||||||
"""
|
|
||||||
headers = [("Daphne-Root-Path", parse.quote(daphne_path))]
|
|
||||||
with DaphneTestingInstance() as test_app:
|
|
||||||
test_app.add_send_messages([{"type": "websocket.accept"}])
|
|
||||||
self.websocket_handshake(
|
|
||||||
test_app,
|
|
||||||
path="/",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
# Validate the scope and messages we got
|
|
||||||
scope, _ = test_app.get_received()
|
|
||||||
|
|
||||||
# Daphne-Root-Path is not included in the returned 'headers' section.
|
|
||||||
self.assertNotIn(
|
|
||||||
"daphne-root-path", (header[0].lower() for header in scope["headers"])
|
|
||||||
)
|
|
||||||
# And what we're looking for, root_path being set.
|
|
||||||
self.assertEqual(scope["root_path"], daphne_path)
|
|
||||||
|
|
||||||
def test_text_frames(self):
|
def test_text_frames(self):
|
||||||
"""
|
"""
|
||||||
Tests we can send and receive text frames.
|
Tests we can send and receive text frames.
|
||||||
|
@ -285,54 +254,3 @@ class TestWebsocket(DaphneTestCase):
|
||||||
self.websocket_send_frame(sock, "still alive?")
|
self.websocket_send_frame(sock, "still alive?")
|
||||||
# Receive a frame and make sure it's correct
|
# Receive a frame and make sure it's correct
|
||||||
assert self.websocket_receive_frame(sock) == "cake"
|
assert self.websocket_receive_frame(sock) == "cake"
|
||||||
|
|
||||||
def test_application_checker_handles_asyncio_cancellederror(self):
|
|
||||||
with CancellingTestingInstance() as app:
|
|
||||||
# Connect to the websocket app, it will immediately raise
|
|
||||||
# asyncio.CancelledError
|
|
||||||
sock, _ = self.websocket_handshake(app)
|
|
||||||
# Disconnect from the socket
|
|
||||||
sock.close()
|
|
||||||
# Wait for application_checker to clean up the applications for
|
|
||||||
# disconnected clients, and for the server to be stopped.
|
|
||||||
time.sleep(3)
|
|
||||||
# Make sure we received either no error, or a ConnectionsNotEmpty
|
|
||||||
while not app.process.errors.empty():
|
|
||||||
err, _tb = app.process.errors.get()
|
|
||||||
if not isinstance(err, ConnectionsNotEmpty):
|
|
||||||
raise err
|
|
||||||
self.fail(
|
|
||||||
"Server connections were not cleaned up after an asyncio.CancelledError was raised"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def cancelling_application(scope, receive, send):
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from twisted.internet import reactor
|
|
||||||
|
|
||||||
# Stop the server after a short delay so that the teardown is run.
|
|
||||||
reactor.callLater(2, reactor.stop)
|
|
||||||
await send({"type": "websocket.accept"})
|
|
||||||
raise asyncio.CancelledError()
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionsNotEmpty(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CancellingTestingInstance(BaseDaphneTestingInstance):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(application=cancelling_application)
|
|
||||||
|
|
||||||
def process_teardown(self):
|
|
||||||
import multiprocessing
|
|
||||||
|
|
||||||
# Get a hold of the enclosing DaphneProcess (we're currently running in
|
|
||||||
# the same process as the application).
|
|
||||||
proc = multiprocessing.current_process()
|
|
||||||
# By now the (only) socket should have disconnected, and the
|
|
||||||
# application_checker should have run. If there are any connections
|
|
||||||
# still, it means that the application_checker did not clean them up.
|
|
||||||
if proc.server.connections:
|
|
||||||
raise ConnectionsNotEmpty()
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user