From 65da239aa2702e0fec982e6952af53690644cd2e Mon Sep 17 00:00:00 2001 From: Bas Peschier Date: Sun, 28 Aug 2016 14:39:08 +0200 Subject: [PATCH 01/15] Tell Twisted to keep producing data after connection upgrade --- daphne/http_protocol.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daphne/http_protocol.py b/daphne/http_protocol.py index f3e7d62..867a834 100755 --- a/daphne/http_protocol.py +++ b/daphne/http_protocol.py @@ -110,6 +110,10 @@ class WebRequest(http.Request): logger.debug("Connection %s did not get successful WS handshake.", self.reply_channel) del self.factory.reply_protocols[self.reply_channel] self.reply_channel = None + + # Resume the producer so we keep getting data + self.channel.resumeProducing() + # Boring old HTTP. else: # Sanitize and decode headers, potentially extracting root path From 0b37e80614fabb8ade419972d93c1115d13a365d Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sun, 28 Aug 2016 11:14:13 -0700 Subject: [PATCH 02/15] Add attribute check for #31 and remove version pin --- daphne/http_protocol.py | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/daphne/http_protocol.py b/daphne/http_protocol.py index 867a834..87281fa 100755 --- a/daphne/http_protocol.py +++ b/daphne/http_protocol.py @@ -110,9 +110,9 @@ class WebRequest(http.Request): logger.debug("Connection %s did not get successful WS handshake.", self.reply_channel) del self.factory.reply_protocols[self.reply_channel] self.reply_channel = None - - # Resume the producer so we keep getting data - self.channel.resumeProducing() + # Resume the producer so we keep getting data, if it's available as a method + if hasattr(self.channel, "resumeProducing"): + self.channel.resumeProducing() # Boring old HTTP. else: diff --git a/setup.py b/setup.py index 5576dc6..39ca85a 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( include_package_data=True, install_requires=[ 'asgiref>=0.13', - 'twisted>=15.5,<16.3', + 'twisted>=16.0', 'autobahn>=0.12', ], entry_points={'console_scripts': [ From 1a5cce9c759f5f07b42a4f6e655f27d6beb51c8d Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sun, 28 Aug 2016 11:27:05 -0700 Subject: [PATCH 03/15] Releasing 0.15.0 --- CHANGELOG.txt | 12 ++++++++++++ daphne/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f056660..6c794e7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +0.15.0 (2016-08-28) +------------------- + +* Connections now force-close themselves after pings fail for a certain + timeframe, controllable via the new --ping-timeout option. + +* Badly-formatted websocket response messages now log to console in + all situations + +* Compatability with Twisted 16.3 and up + + 0.14.3 (2016-07-21) ------------------- diff --git a/daphne/__init__.py b/daphne/__init__.py index 23f0070..9da2f8f 100755 --- a/daphne/__init__.py +++ b/daphne/__init__.py @@ -1 +1 @@ -__version__ = "0.14.3" +__version__ = "0.15.0" From edb67cac74044883e5f1e8449af2f71fa810cee9 Mon Sep 17 00:00:00 2001 From: Steven Davidson Date: Mon, 29 Aug 2016 00:03:42 +0100 Subject: [PATCH 04/15] Attach path to http.disconnect https://github.com/andrewgodwin/channels/issues/303 --- daphne/http_protocol.py | 1 + daphne/tests/test_http.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/daphne/http_protocol.py b/daphne/http_protocol.py index 87281fa..cf732f0 100755 --- a/daphne/http_protocol.py +++ b/daphne/http_protocol.py @@ -177,6 +177,7 @@ class WebRequest(http.Request): try: self.factory.channel_layer.send("http.disconnect", { "reply_channel": self.reply_channel, + "path": self.unquote(self.path), }) except self.factory.channel_layer.ChannelFull: pass diff --git a/daphne/tests/test_http.py b/daphne/tests/test_http.py index 49d83ef..ba7225f 100644 --- a/daphne/tests/test_http.py +++ b/daphne/tests/test_http.py @@ -66,3 +66,30 @@ class TestHTTPProtocol(TestCase): # Get the resulting message off of the channel layer, check root_path _, message = self.channel_layer.receive_many(["http.request"]) self.assertEqual(message['root_path'], "/foobar /bar") + + def test_http_disconnect_sets_path_key(self): + """ + Tests http disconnect has the path key set, see http://channels.readthedocs.io/en/latest/asgi.html#disconnect + """ + # Send a simple request to the protocol + self.proto.dataReceived( + b"GET /te%20st-%C3%A0/?foo=bar HTTP/1.1\r\n" + + b"Host: anywhere.com\r\n" + + b"\r\n" + ) + # Get the request message + _, message = self.channel_layer.receive_many(["http.request"]) + + # Send back an example response + self.factory.dispatch_reply( + message['reply_channel'], + { + "status": 200, + "status_text": b"OK", + "content": b"DISCO", + } + ) + + # Get the disconnection notification + _, disconnect_message = self.channel_layer.receive_many(["http.disconnect"]) + self.assertEqual(disconnect_message['path'], "/te st-à/") \ No newline at end of file From 8662b02daf9a0e0e9f93009913d86776d5d3a7f5 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 9 Sep 2016 12:52:06 +0100 Subject: [PATCH 05/15] Add maintenance and security README --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index 8c75c98..18569c0 100644 --- a/README.rst +++ b/README.rst @@ -55,3 +55,16 @@ The header takes precedence if both are set. As with ``SCRIPT_ALIAS``, the value should start with a slash, but not end with one; for example:: daphne --root-path=/forum django_project.asgi:channel_layer + + +Maintenance and Security +------------------------ + +To report security issues, please contact security@djangoproject.com. For GPG +signatures and more security process information, see +https://docs.djangoproject.com/en/dev/internals/security/. + +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 +`main Channels readme `_. From 2176b209f71b4bb996dcbcba2048aeeadbd4168d Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 9 Sep 2016 13:27:25 +0100 Subject: [PATCH 06/15] Django-ification --- README.rst | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 18569c0..959d4e7 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ daphne ====== -.. image:: https://api.travis-ci.org/andrewgodwin/daphne.svg - :target: https://travis-ci.org/andrewgodwin/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 :target: https://pypi.python.org/pypi/daphne @@ -67,4 +67,4 @@ https://docs.djangoproject.com/en/dev/internals/security/. 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 -`main Channels readme `_. +`main Channels readme `_. diff --git a/setup.py b/setup.py index 39ca85a..a2c1fa4 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ readme_path = os.path.join(os.path.dirname(__file__), "README.rst") setup( name='daphne', version=__version__, - url='http://www.djangoproject.com/', + url='http://github.com/django/daphne', author='Django Software Foundation', author_email='foundation@djangoproject.com', description='Django ASGI (HTTP/WebSocket) server', From c6270e7e4e099198137f660942feefba955d978d Mon Sep 17 00:00:00 2001 From: Krukov Dima Date: Fri, 16 Sep 2016 20:34:20 +0000 Subject: [PATCH 07/15] Pep8 --- daphne/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daphne/cli.py b/daphne/cli.py index 57724ef..ac99e24 100755 --- a/daphne/cli.py +++ b/daphne/cli.py @@ -113,12 +113,12 @@ class CommandLineInterface(object): args = self.parser.parse_args(args) # Set up logging logging.basicConfig( - level = { + level={ 0: logging.WARN, 1: logging.INFO, 2: logging.DEBUG, }[args.verbosity], - format = "%(asctime)-15s %(levelname)-8s %(message)s" , + format="%(asctime)-15s %(levelname)-8s %(message)s", ) # If verbosity is 1 or greater, or they told us explicitly, set up access log access_log_stream = None From 08440612966705213f2fdcbf617678a798990854 Mon Sep 17 00:00:00 2001 From: Krukov Dima Date: Fri, 16 Sep 2016 20:36:31 +0000 Subject: [PATCH 08/15] Add verbosity to log twisted log --- daphne/cli.py | 1 + daphne/server.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/daphne/cli.py b/daphne/cli.py index ac99e24..0726aa1 100755 --- a/daphne/cli.py +++ b/daphne/cli.py @@ -153,4 +153,5 @@ class CommandLineInterface(object): action_logger=AccessLogGenerator(access_log_stream) if access_log_stream else None, ws_protocols=args.ws_protocols, root_path=args.root_path, + verbosity=args.verbosity, ).run() diff --git a/daphne/server.py b/daphne/server.py index eaf143d..b42654f 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -26,6 +26,7 @@ class Server(object): ping_timeout=30, ws_protocols=None, root_path="", + verbosity=None ): self.channel_layer = channel_layer self.host = host @@ -42,6 +43,7 @@ class Server(object): self.websocket_timeout = websocket_timeout or getattr(channel_layer, "group_expiry", 86400) self.ws_protocols = ws_protocols self.root_path = root_path + self.verbosity = verbosity def run(self): self.factory = HTTPFactory( @@ -54,8 +56,9 @@ class Server(object): ws_protocols=self.ws_protocols, root_path=self.root_path, ) - # Redirect the Twisted log to nowhere - globalLogBeginner.beginLoggingTo([lambda _: None], redirectStandardIO=False, discardBuffer=True) + if self.verbosity <= 1: + # Redirect the Twisted log to nowhere + globalLogBeginner.beginLoggingTo([lambda _: None], redirectStandardIO=False, discardBuffer=True) # Listen on a socket if self.unix_socket: reactor.listenUNIX(self.unix_socket, self.factory) From bcaf1de155d56c4333e136b57b54a7bb65456a35 Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Wed, 21 Sep 2016 08:43:24 +0100 Subject: [PATCH 09/15] Convert readthedocs links for their .org -> .io migration for hosted projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’: > Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard. Test Plan: Manually visited all the links I’ve modified. --- README.rst | 2 +- daphne/tests/test_http.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 959d4e7..6124b47 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ daphne :target: https://pypi.python.org/pypi/daphne Daphne is a HTTP, HTTP2 and WebSocket protocol server for -`ASGI `_, and developed +`ASGI `_, and developed to power Django Channels. It supports automatic negotiation of protocols; there's no need for URL diff --git a/daphne/tests/test_http.py b/daphne/tests/test_http.py index ba7225f..48e1da2 100644 --- a/daphne/tests/test_http.py +++ b/daphne/tests/test_http.py @@ -69,7 +69,7 @@ class TestHTTPProtocol(TestCase): def test_http_disconnect_sets_path_key(self): """ - Tests http disconnect has the path key set, see http://channels.readthedocs.io/en/latest/asgi.html#disconnect + Tests http disconnect has the path key set, see https://channels.readthedocs.io/en/latest/asgi.html#disconnect """ # Send a simple request to the protocol self.proto.dataReceived( @@ -92,4 +92,4 @@ class TestHTTPProtocol(TestCase): # Get the disconnection notification _, disconnect_message = self.channel_layer.receive_many(["http.disconnect"]) - self.assertEqual(disconnect_message['path'], "/te st-à/") \ No newline at end of file + self.assertEqual(disconnect_message['path'], "/te st-à/") From cf096bab5cb472ca81585cdf2b85ea096fd8c17e Mon Sep 17 00:00:00 2001 From: Krukov Dima Date: Wed, 21 Sep 2016 18:12:35 +0000 Subject: [PATCH 10/15] Logging to the python standard library --- daphne/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daphne/server.py b/daphne/server.py index b42654f..116d238 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -2,7 +2,7 @@ import logging import socket from twisted.internet import reactor, defer -from twisted.logger import globalLogBeginner +from twisted.logger import globalLogBeginner, STDLibLogObserver from .http_protocol import HTTPFactory @@ -59,6 +59,8 @@ class Server(object): if self.verbosity <= 1: # Redirect the Twisted log to nowhere globalLogBeginner.beginLoggingTo([lambda _: None], redirectStandardIO=False, discardBuffer=True) + else: + globalLogBeginner.beginLoggingTo([STDLibLogObserver(__name__)]) # Listen on a socket if self.unix_socket: reactor.listenUNIX(self.unix_socket, self.factory) From 790c482cb6574abf668fea62af755ac4d2db16ea Mon Sep 17 00:00:00 2001 From: Krukov Dima Date: Wed, 21 Sep 2016 18:24:05 +0000 Subject: [PATCH 11/15] Catching error at receive_many form channel layer --- daphne/server.py | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/daphne/server.py b/daphne/server.py index eaf143d..7eb9ac9 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -89,14 +89,19 @@ class Server(object): # Don't do anything if there's no channels to listen on if channels: delay = 0.01 - channel, message = self.channel_layer.receive_many(channels, block=False) - if channel: - delay = 0.00 - # Deal with the message - try: - self.factory.dispatch_reply(channel, message) - except Exception as e: - logger.error("HTTP/WS send decode error: %s" % e) + try: + channel, message = self.channel_layer.receive_many(channels, block=False) + except Exception as e: + logger.error('Error at trying to receive messages: %s' % e) + delay = 5.00 + else: + if channel: + delay = 0.00 + # Deal with the message + try: + self.factory.dispatch_reply(channel, message) + except Exception as e: + logger.error("HTTP/WS send decode error: %s" % e) reactor.callLater(delay, self.backend_reader_sync) @defer.inlineCallbacks @@ -111,15 +116,20 @@ class Server(object): return channels = self.factory.reply_channels() if channels: - channel, message = yield self.channel_layer.receive_many_twisted(channels) - # Deal with the message - if channel: - try: - self.factory.dispatch_reply(channel, message) - except Exception as e: - logger.error("HTTP/WS send decode error: %s" % e) + try: + channel, message = yield self.channel_layer.receive_many_twisted(channels) + except Exception as e: + logger.error('Error at trying to receive messages: %s' % e) + yield self.sleep(5.00) else: - yield self.sleep(0.01) + # Deal with the message + if channel: + try: + self.factory.dispatch_reply(channel, message) + except Exception as e: + logger.error("HTTP/WS send decode error: %s" % e) + else: + yield self.sleep(0.01) else: yield self.sleep(0.05) From 5a5fd08633d343506a69600fbae51ec9ca1b837d Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Thu, 22 Sep 2016 22:25:09 +0100 Subject: [PATCH 12/15] Tidy up setup.py a bit * Remove unused import 'sys' * Github is HTTPS * Add some trove classifiers based upon Django's --- setup.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index a2c1fa4..fe700a1 100755 --- a/setup.py +++ b/setup.py @@ -1,21 +1,23 @@ import os -import sys + 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='http://github.com/django/daphne', + url='https://github.com/django/daphne', author='Django Software Foundation', author_email='foundation@djangoproject.com', description='Django ASGI (HTTP/WebSocket) server', - long_description=open(readme_path).read(), + long_description=long_description, license='BSD', zip_safe=False, packages=find_packages(), @@ -28,4 +30,17 @@ setup( 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 :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Topic :: Internet :: WWW/HTTP', + ], ) From 685f3aed1e5f7d492d16928c58e684f90ba85388 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Wed, 5 Oct 2016 12:08:03 -0700 Subject: [PATCH 13/15] Switch to new explicit WebSocket acceptance --- daphne/access.py | 12 ++++++ daphne/http_protocol.py | 39 +++++++++++++----- daphne/ws_protocol.py | 90 +++++++++++++++++++++++++++++++---------- 3 files changed, 109 insertions(+), 32 deletions(-) diff --git a/daphne/access.py b/daphne/access.py index 9bd1489..c5fa69c 100644 --- a/daphne/access.py +++ b/daphne/access.py @@ -24,6 +24,18 @@ class AccessLogGenerator(object): length=details['size'], ) # Websocket requests + elif protocol == "websocket" and action == "connecting": + self.write_entry( + host=details['client'], + date=datetime.datetime.now(), + request="WSCONNECTING %(path)s" % details, + ) + elif protocol == "websocket" and action == "rejected": + self.write_entry( + host=details['client'], + date=datetime.datetime.now(), + request="WSREJECT %(path)s" % details, + ) elif protocol == "websocket" and action == "connected": self.write_entry( host=details['client'], diff --git a/daphne/http_protocol.py b/daphne/http_protocol.py index cf732f0..5e2681e 100755 --- a/daphne/http_protocol.py +++ b/daphne/http_protocol.py @@ -281,12 +281,13 @@ class HTTPFactory(http.HTTPFactory): protocol = HTTPProtocol - def __init__(self, channel_layer, action_logger=None, timeout=120, websocket_timeout=86400, ping_interval=20, ping_timeout=30, ws_protocols=None, root_path=""): + def __init__(self, channel_layer, action_logger=None, timeout=120, websocket_timeout=86400, ping_interval=20, ping_timeout=30, ws_protocols=None, root_path="", websocket_connect_timeout=30): http.HTTPFactory.__init__(self) self.channel_layer = channel_layer self.action_logger = action_logger self.timeout = timeout self.websocket_timeout = websocket_timeout + self.websocket_connect_timeout = websocket_connect_timeout self.ping_interval = ping_interval # We track all sub-protocols for response channel mapping self.reply_protocols = {} @@ -304,21 +305,37 @@ class HTTPFactory(http.HTTPFactory): if channel.startswith("http") and isinstance(self.reply_protocols[channel], WebRequest): self.reply_protocols[channel].serverResponse(message) elif channel.startswith("websocket") and isinstance(self.reply_protocols[channel], WebSocketProtocol): - # Ensure the message is a valid WebSocket one - unknown_message_keys = set(message.keys()) - {"bytes", "text", "close"} - if unknown_message_keys: + # Switch depending on current socket state + protocol = self.reply_protocols[channel] + # See if the message is valid + non_accept_keys = set(message.keys()) - {"accept"} + non_send_keys = set(message.keys()) - {"bytes", "text", "close"} + if non_accept_keys and non_send_keys: raise ValueError( - "Got invalid WebSocket reply message on %s - contains unknown keys %s" % ( + "Got invalid WebSocket reply message on %s - " + "contains unknown keys %s (looking for either {'accept'} or {'text', 'bytes', 'close'})" % ( channel, unknown_message_keys, ) ) - if message.get("bytes", None): - self.reply_protocols[channel].serverSend(message["bytes"], True) - if message.get("text", None): - self.reply_protocols[channel].serverSend(message["text"], False) - if message.get("close", False): - self.reply_protocols[channel].serverClose() + if "accept" in message: + if protocol.state != protocol.STATE_CONNECTING: + raise ValueError( + "Got invalid WebSocket connection reply message on %s - websocket is not in handshake phase" % ( + channel, + ) + ) + if message['accept']: + protocol.serverAccept() + else: + protocol.serverReject() + else: + if message.get("bytes", None): + protocol.serverSend(message["bytes"], True) + if message.get("text", None): + protocol.serverSend(message["text"], False) + if message.get("close", False): + protocol.serverClose() else: raise ValueError("Cannot dispatch message on channel %r" % channel) diff --git a/daphne/ws_protocol.py b/daphne/ws_protocol.py index dfa8adc..db3e675 100755 --- a/daphne/ws_protocol.py +++ b/daphne/ws_protocol.py @@ -5,6 +5,7 @@ import six import time import traceback from six.moves.urllib_parse import unquote, urlencode +from twisted.internet import defer from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory @@ -27,6 +28,7 @@ class WebSocketProtocol(WebSocketServerProtocol): def onConnect(self, request): self.request = request self.packets_received = 0 + self.protocol_to_accept = None self.socket_opened = time.time() self.last_data = time.time() try: @@ -78,8 +80,31 @@ class WebSocketProtocol(WebSocketServerProtocol): ws_protocol = protocol break + # Work out what subprotocol we will accept, if any if ws_protocol and ws_protocol in self.factory.protocols: - return ws_protocol + self.protocol_to_accept = ws_protocol + else: + self.protocol_to_accept = None + + # Send over the connect message + try: + self.channel_layer.send("websocket.connect", self.request_info) + except self.channel_layer.ChannelFull: + # You have to consume websocket.connect according to the spec, + # so drop the connection. + self.muted = True + logger.warn("WebSocket force closed for %s due to connect backpressure", self.reply_channel) + # Send code 1013 "try again later" with close. + raise ConnectionDeny(code=503, reason="Connection queue at capacity") + else: + self.factory.log_action("websocket", "connecting", { + "path": self.request.path, + "client": "%s:%s" % tuple(self.client_addr) if self.client_addr else None, + }) + + # Make a deferred and return it - we'll either call it or err it later on + self.handshake_deferred = defer.Deferred() + return self.handshake_deferred @classmethod def unquote(cls, value): @@ -93,21 +118,11 @@ class WebSocketProtocol(WebSocketServerProtocol): def onOpen(self): # Send news that this channel is open - logger.debug("WebSocket open for %s", self.reply_channel) - try: - self.channel_layer.send("websocket.connect", self.request_info) - except self.channel_layer.ChannelFull: - # You have to consume websocket.connect according to the spec, - # so drop the connection. - self.muted = True - logger.warn("WebSocket force closed for %s due to connect backpressure", self.reply_channel) - # Send code 1013 "try again later" with close. - self.sendCloseFrame(code=1013, isReply=False) - else: - self.factory.log_action("websocket", "connected", { - "path": self.request.path, - "client": "%s:%s" % tuple(self.client_addr) if self.client_addr else None, - }) + logger.debug("WebSocket %s open and established", self.reply_channel) + self.factory.log_action("websocket", "connected", { + "path": self.request.path, + "client": "%s:%s" % tuple(self.client_addr) if self.client_addr else None, + }) def onMessage(self, payload, isBinary): # If we're muted, do nothing. @@ -140,10 +155,31 @@ class WebSocketProtocol(WebSocketServerProtocol): # Send code 1013 "try again later" with close. self.sendCloseFrame(code=1013, isReply=False) + def serverAccept(self): + """ + Called when we get a message saying to accept the connection. + """ + self.handshake_deferred.callback(self.protocol_to_accept) + logger.debug("WebSocket %s accepted by application", self.reply_channel) + + def serverReject(self): + """ + Called when we get a message saying to accept the connection. + """ + self.handshake_deferred.errback(ConnectionDeny(code=403, reason="Access denied")) + self.cleanup() + logger.debug("WebSocket %s rejected by application", self.reply_channel) + self.factory.log_action("websocket", "rejected", { + "path": self.request.path, + "client": "%s:%s" % tuple(self.client_addr) if self.client_addr else None, + }) + def serverSend(self, content, binary=False): """ Server-side channel message to send a message. """ + if self.state == self.STATE_CONNECTING: + self.serverAccept() self.last_data = time.time() logger.debug("Sent WebSocket packet to client for %s", self.reply_channel) if binary: @@ -158,9 +194,9 @@ class WebSocketProtocol(WebSocketServerProtocol): self.sendClose() def onClose(self, wasClean, code, reason): + self.cleanup() if hasattr(self, "reply_channel"): logger.debug("WebSocket closed for %s", self.reply_channel) - del self.factory.reply_protocols[self.reply_channel] try: if not self.muted: self.channel_layer.send("websocket.disconnect", { @@ -178,6 +214,13 @@ class WebSocketProtocol(WebSocketServerProtocol): else: logger.debug("WebSocket closed before handshake established") + def cleanup(self): + """ + Call to clean up this socket after it's closed. + """ + if hasattr(self, "reply_channel"): + del self.factory.reply_protocols[self.reply_channel] + def duration(self): """ Returns the time since the socket was opened @@ -186,11 +229,16 @@ class WebSocketProtocol(WebSocketServerProtocol): def check_ping(self): """ - Checks to see if we should send a keepalive ping. + Checks to see if we should send a keepalive ping/deny socket connection """ - if (time.time() - self.last_data) > self.main_factory.ping_interval: - self._sendAutoPing() - self.last_data = time.time() + # If we're still connecting, deny the connection + if self.state == self.STATE_CONNECTING: + if self.duration() > self.main_factory.websocket_connect_timeout: + self.serverReject() + elif self.state == self.STATE_OPEN: + if (time.time() - self.last_data) > self.main_factory.ping_interval: + self._sendAutoPing() + self.last_data = time.time() class WebSocketFactory(WebSocketServerFactory): From 8c637ff7280679e17f96c8b0da2a5d1bfa0315fb Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Wed, 5 Oct 2016 14:46:54 -0700 Subject: [PATCH 14/15] Fix default verbosity --- daphne/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daphne/server.py b/daphne/server.py index 99e6e67..dcb98d9 100755 --- a/daphne/server.py +++ b/daphne/server.py @@ -26,7 +26,7 @@ class Server(object): ping_timeout=30, ws_protocols=None, root_path="", - verbosity=None + verbosity=1 ): self.channel_layer = channel_layer self.host = host From b537bed18039bdd935b501fe02d1aac02b06a384 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Wed, 5 Oct 2016 16:00:11 -0700 Subject: [PATCH 15/15] Make accept silently pass if already accepted --- daphne/http_protocol.py | 32 ++++++++++++-------------------- daphne/ws_protocol.py | 2 +- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/daphne/http_protocol.py b/daphne/http_protocol.py index 5e2681e..d44e9f2 100755 --- a/daphne/http_protocol.py +++ b/daphne/http_protocol.py @@ -308,33 +308,25 @@ class HTTPFactory(http.HTTPFactory): # Switch depending on current socket state protocol = self.reply_protocols[channel] # See if the message is valid - non_accept_keys = set(message.keys()) - {"accept"} - non_send_keys = set(message.keys()) - {"bytes", "text", "close"} - if non_accept_keys and non_send_keys: + unknown_keys = set(message.keys()) - {"bytes", "text", "close", "accept"} + if unknown_keys: raise ValueError( "Got invalid WebSocket reply message on %s - " - "contains unknown keys %s (looking for either {'accept'} or {'text', 'bytes', 'close'})" % ( + "contains unknown keys %s (looking for either {'accept', 'text', 'bytes', 'close'})" % ( channel, unknown_message_keys, ) ) - if "accept" in message: - if protocol.state != protocol.STATE_CONNECTING: - raise ValueError( - "Got invalid WebSocket connection reply message on %s - websocket is not in handshake phase" % ( - channel, - ) - ) - if message['accept']: - protocol.serverAccept() - else: + if message.get("accept", None) and protocol.state == protocol.STATE_CONNECTING: + protocol.serverAccept() + if message.get("bytes", None): + protocol.serverSend(message["bytes"], True) + if message.get("text", None): + protocol.serverSend(message["text"], False) + if message.get("close", False): + if protocol.state == protocol.STATE_CONNECTING: protocol.serverReject() - else: - if message.get("bytes", None): - protocol.serverSend(message["bytes"], True) - if message.get("text", None): - protocol.serverSend(message["text"], False) - if message.get("close", False): + else: protocol.serverClose() else: raise ValueError("Cannot dispatch message on channel %r" % channel) diff --git a/daphne/ws_protocol.py b/daphne/ws_protocol.py index db3e675..60f7ace 100755 --- a/daphne/ws_protocol.py +++ b/daphne/ws_protocol.py @@ -7,7 +7,7 @@ import traceback from six.moves.urllib_parse import unquote, urlencode from twisted.internet import defer -from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory +from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory, ConnectionDeny logger = logging.getLogger(__name__)