From 231bfb7c4ebf4e68f3ea08f9fba27e9d47dc1ea6 Mon Sep 17 00:00:00 2001 From: niekas Date: Mon, 14 Sep 2020 13:33:13 +0300 Subject: [PATCH 1/5] Allow to accept websocket extensions Also accept `permessage-deflate`, `permessage-bzip2` and `permessage-snappy` compression extensions by default if client requests for them. Compression/decompression of the messages is taken care of by `autobahn` package. --- daphne/server.py | 25 +++++++++++++++++++ daphne/ws_protocol.py | 13 +++++++--- tests/test_websocket.py | 54 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/daphne/server.py b/daphne/server.py index 5ede8082..7b650799 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -23,6 +23,7 @@ import time from concurrent.futures import CancelledError +from autobahn.websocket.compress import PERMESSAGE_COMPRESSION_EXTENSION as EXTENSIONS from twisted.internet import defer, reactor from twisted.internet.endpoints import serverFromString from twisted.logger import STDLibLogObserver, globalLogBeginner @@ -44,6 +45,11 @@ def __init__( http_timeout=None, websocket_timeout=86400, websocket_connect_timeout=20, + websocket_permessage_compression_extensions=[ + "permessage-deflate", + "permessage-bzip2", + "permessage-snappy", + ], ping_interval=20, ping_timeout=30, root_path="", @@ -73,6 +79,9 @@ def __init__( self.websocket_timeout = websocket_timeout self.websocket_connect_timeout = websocket_connect_timeout self.websocket_handshake_timeout = websocket_handshake_timeout + self.websocket_permessage_compression_extensions = ( + websocket_permessage_compression_extensions + ) self.application_close_timeout = application_close_timeout self.root_path = root_path self.verbosity = verbosity @@ -94,6 +103,7 @@ def run(self): autoPingTimeout=self.ping_timeout, allowNullOrigin=True, openHandshakeTimeout=self.websocket_handshake_timeout, + perMessageCompressionAccept=self.accept_permessage_compression_extension, ) if self.verbosity <= 1: # Redirect the Twisted log to nowhere @@ -246,6 +256,21 @@ def check_headers_type(message): ) ) + def accept_permessage_compression_extension(self, offers): + """ + Accepts websocket compression extension as required by `autobahn` package. + """ + for offer in offers: + for ext in self.websocket_permessage_compression_extensions: + if ext in EXTENSIONS and isinstance(offer, EXTENSIONS[ext]["Offer"]): + return EXTENSIONS[ext]["OfferAccept"](offer) + elif ext not in EXTENSIONS: + logger.warning( + "Compression extension %s could not be accepted. " + "It is not supported or a dependency is missing.", + ext, + ) + ### Utility def application_checker(self): diff --git a/daphne/ws_protocol.py b/daphne/ws_protocol.py index 1962450d..47c19e64 100755 --- a/daphne/ws_protocol.py +++ b/daphne/ws_protocol.py @@ -182,7 +182,10 @@ def handle_reply(self, message): if "type" not in message: raise ValueError("Message has no type defined") if message["type"] == "websocket.accept": - self.serverAccept(message.get("subprotocol", None)) + self.serverAccept( + message.get("subprotocol", None), message.get("headers", None) + ) + elif message["type"] == "websocket.close": if self.state == self.STATE_CONNECTING: self.serverReject() @@ -214,11 +217,15 @@ def handle_exception(self, exception): else: self.sendCloseFrame(code=1011) - def serverAccept(self, subprotocol=None): + def serverAccept(self, subprotocol=None, headers=None): """ Called when we get a message saying to accept the connection. """ - self.handshake_deferred.callback(subprotocol) + if headers is None: + self.handshake_deferred.callback(subprotocol) + else: + headers_dict = {key.decode(): value.decode() for key, value in headers} + self.handshake_deferred.callback((subprotocol, headers_dict)) del self.handshake_deferred logger.debug("WebSocket %s accepted by application", self.client_addr) diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 9ec2c0dd..637296c8 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -132,6 +132,60 @@ def test_subprotocols(self): self.assert_valid_websocket_scope(scope, subprotocols=subprotocols) self.assert_valid_websocket_connect_message(messages[0]) + def test_accept_permessage_deflate_extension(self): + """ + Tests that permessage-deflate extension is successfuly accepted + by underlying `autobahn` package. + """ + + headers = [ + ( + b"Sec-WebSocket-Extensions", + b"permessage-deflate; client_max_window_bits", + ), + ] + + with DaphneTestingInstance() as test_app: + test_app.add_send_messages( + [ + { + "type": "websocket.accept", + } + ] + ) + + sock, subprotocol = self.websocket_handshake( + test_app, + headers=headers, + ) + # Validate the scope and messages we got + scope, messages = test_app.get_received() + self.assert_valid_websocket_connect_message(messages[0]) + + def test_accept_custom_extension(self): + """ + Tests that custom headers can be accpeted during handshake. + """ + with DaphneTestingInstance() as test_app: + test_app.add_send_messages( + [ + { + "type": "websocket.accept", + "headers": [(b"Sec-WebSocket-Extensions", b"custom-extension")], + } + ] + ) + + sock, subprotocol = self.websocket_handshake( + test_app, + headers=[ + (b"Sec-WebSocket-Extensions", b"custom-extension"), + ], + ) + # Validate the scope and messages we got + scope, messages = test_app.get_received() + self.assert_valid_websocket_connect_message(messages[0]) + def test_xff(self): """ Tests that X-Forwarded-For headers get parsed right From 2c7c5c65485ee8b8147b729bae85db33ef850a93 Mon Sep 17 00:00:00 2001 From: niekas Date: Mon, 14 Sep 2020 14:04:40 +0300 Subject: [PATCH 2/5] Fix imports in tests as suggested by isort --- tests/test_websocket.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 637296c8..607e653d 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -4,10 +4,9 @@ import time from urllib import parse -from hypothesis import given, settings - import http_strategies from http_base import DaphneTestCase, DaphneTestingInstance +from hypothesis import given, settings class TestWebsocket(DaphneTestCase): From d467616aaf9e0d73470e7ac081f2f8717d405d99 Mon Sep 17 00:00:00 2001 From: niekas Date: Mon, 14 Sep 2020 14:13:07 +0300 Subject: [PATCH 3/5] Fix imports as suggested by isort even for unchanged files, because TravisCI build fails. --- daphne/testing.py | 4 ++-- tests/test_http_request.py | 3 +-- tests/test_http_response.py | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/daphne/testing.py b/daphne/testing.py index 7cc1182a..c3020454 100644 --- a/daphne/testing.py +++ b/daphne/testing.py @@ -127,8 +127,8 @@ def run(self): from twisted.internet import reactor - from .server import Server from .endpoints import build_endpoint_description_strings + from .server import Server try: # Create the server class @@ -266,8 +266,8 @@ def delete_result(cls): def _reinstall_reactor(): - import sys import asyncio + import sys from twisted.internet import asyncioreactor diff --git a/tests/test_http_request.py b/tests/test_http_request.py index 0bd2d825..aae7f99c 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -3,10 +3,9 @@ import collections from urllib import parse -from hypothesis import assume, given, settings - import http_strategies from http_base import DaphneTestCase +from hypothesis import assume, given, settings class TestHTTPRequest(DaphneTestCase): diff --git a/tests/test_http_response.py b/tests/test_http_response.py index 08c34bb4..9dd728d5 100644 --- a/tests/test_http_response.py +++ b/tests/test_http_response.py @@ -1,9 +1,8 @@ # coding: utf8 -from hypothesis import given, settings - import http_strategies from http_base import DaphneTestCase +from hypothesis import given, settings class TestHTTPResponse(DaphneTestCase): From d120da8d29d55d0413ed8f9c1bd4ea096aa5dfce Mon Sep 17 00:00:00 2001 From: Albertas Gimbutas Date: Wed, 4 Aug 2021 02:01:35 +0300 Subject: [PATCH 4/5] Use tuple instead of list for default compressions --- daphne/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daphne/server.py b/daphne/server.py index 0cd7787f..0ea11641 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -47,11 +47,11 @@ def __init__( request_buffer_size=8192, websocket_timeout=86400, websocket_connect_timeout=20, - websocket_permessage_compression_extensions=[ + websocket_permessage_compression_extensions=( "permessage-deflate", "permessage-bzip2", "permessage-snappy", - ], + ), ping_interval=20, ping_timeout=30, root_path="", From 78542781c67447f69af7a53df5b4ece731b17303 Mon Sep 17 00:00:00 2001 From: Albertas Gimbutas Date: Sat, 28 Aug 2021 02:47:53 +0300 Subject: [PATCH 5/5] Add section about permessage compression to README --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index 7525b27c..5b08d59b 100644 --- a/README.rst +++ b/README.rst @@ -105,6 +105,20 @@ should start with a slash, but not end with one; for example:: daphne --root-path=/forum django_project.asgi:application +Permessage compression +---------------------- + +Daphne supports and by default accepts ``permessage-deflate`` compression +(`permessage-deflate specification `_). +Additional ``permessage-bzip2``, ``permessage-snappy`` compressions will be also enabled by default if +``bz2`` and `snappy `_ python packages are available in daphne environment. +The compression implementation is provided by +`Autobahn|Python `_ package, see: +`permessage-deflate `_, +`permessage-bzip2 `_, +`permessage-snappy `_. + + Python Support --------------