diff --git a/channels/exceptions.py b/channels/exceptions.py index 3e52699..53cdd37 100644 --- a/channels/exceptions.py +++ b/channels/exceptions.py @@ -14,3 +14,10 @@ class ResponseLater(Exception): returning a response. """ pass + + +class RequestTimeout(Exception): + """ + Raised when it takes too long to read a request body. + """ + pass diff --git a/channels/handler.py b/channels/handler.py index 6e2bedb..26ab586 100644 --- a/channels/handler.py +++ b/channels/handler.py @@ -4,6 +4,7 @@ import cgi import codecs import logging import sys +import time import traceback from io import BytesIO from threading import Lock @@ -13,11 +14,11 @@ from django.conf import settings from django.core import signals from django.core.handlers import base from django.core.urlresolvers import set_script_prefix -from django.http import FileResponse, HttpResponseServerError +from django.http import FileResponse, HttpResponse, HttpResponseServerError from django.utils import six from django.utils.functional import cached_property -from .exceptions import ResponseLater as ResponseLaterOuter +from .exceptions import ResponseLater as ResponseLaterOuter, RequestTimeout logger = logging.getLogger('django.request') @@ -30,6 +31,10 @@ class AsgiRequest(http.HttpRequest): ResponseLater = ResponseLaterOuter + # Number of seconds until a Request gives up on trying to read a request + # body and aborts. + body_receive_timeout = 60 + def __init__(self, message): self.message = message self.reply_channel = self.message.reply_channel @@ -100,10 +105,15 @@ class AsgiRequest(http.HttpRequest): # Body handling self._body = message.get("body", b"") if message.get("body_channel", None): + body_handle_start = time.time() while True: # Get the next chunk from the request body channel chunk = None while chunk is None: + # If they take too long, raise request timeout and the handler + # will turn it into a response + if time.time() - body_handle_start > self.body_receive_timeout: + raise RequestTimeout() _, chunk = message.channel_layer.receive_many( [message['body_channel']], block=True, @@ -184,6 +194,9 @@ class AsgiHandler(base.BaseHandler): } ) response = http.HttpResponseBadRequest() + except RequestTimeout: + # Parsing the rquest failed, so the response is a Request Timeout error + response = HttpResponse("408 Request Timeout (upload too slow)", status_code=408) else: try: response = self.get_response(request) diff --git a/channels/tests/test_request.py b/channels/tests/test_request.py index 7d1c4ec..028d3da 100644 --- a/channels/tests/test_request.py +++ b/channels/tests/test_request.py @@ -4,6 +4,7 @@ from django.utils import six from channels import Channel from channels.tests import ChannelTestCase from channels.handler import AsgiRequest +from channels.exceptions import RequestTimeout class RequestTests(ChannelTestCase): @@ -188,3 +189,30 @@ class RequestTests(ChannelTestCase): self.assertEqual(request.method, "PUT") self.assertEqual(request.read(3), b"one") self.assertEqual(request.read(), b"twothree") + + def test_request_timeout(self): + """ + Tests that the code correctly gives up after the request body read timeout. + """ + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "POST", + "path": b"/test/", + "body": b"there_a", + "body_channel": "test-input", + "headers": { + "host": b"example.com", + "content-type": b"application/x-www-form-urlencoded", + "content-length": b"21", + }, + }) + # Say there's more content, but never provide it! Muahahaha! + Channel("test-input").send({ + "content": b"re=fou", + "more_content": True, + }) + class VeryImpatientRequest(AsgiRequest): + body_receive_timeout = 0 + with self.assertRaises(RequestTimeout): + VeryImpatientRequest(self.get_next_message("test"))