diff --git a/README.rst b/README.rst index 72fb3337..9bf6d31e 100644 --- a/README.rst +++ b/README.rst @@ -112,6 +112,21 @@ 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 -------------- diff --git a/daphne/server.py b/daphne/server.py index 5ede8082..402142b7 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..10d62bfd 100755 --- a/daphne/ws_protocol.py +++ b/daphne/ws_protocol.py @@ -182,7 +182,7 @@ 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 +214,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)