Implement timeout on request body reading

This commit is contained in:
Andrew Godwin 2016-04-04 00:55:10 +02:00
parent e18bfed8f3
commit 920882f1da
3 changed files with 50 additions and 2 deletions

View File

@ -14,3 +14,10 @@ class ResponseLater(Exception):
returning a response. returning a response.
""" """
pass pass
class RequestTimeout(Exception):
"""
Raised when it takes too long to read a request body.
"""
pass

View File

@ -4,6 +4,7 @@ import cgi
import codecs import codecs
import logging import logging
import sys import sys
import time
import traceback import traceback
from io import BytesIO from io import BytesIO
from threading import Lock from threading import Lock
@ -13,11 +14,11 @@ from django.conf import settings
from django.core import signals from django.core import signals
from django.core.handlers import base from django.core.handlers import base
from django.core.urlresolvers import set_script_prefix 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 import six
from django.utils.functional import cached_property 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') logger = logging.getLogger('django.request')
@ -30,6 +31,10 @@ class AsgiRequest(http.HttpRequest):
ResponseLater = ResponseLaterOuter 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): def __init__(self, message):
self.message = message self.message = message
self.reply_channel = self.message.reply_channel self.reply_channel = self.message.reply_channel
@ -100,10 +105,15 @@ class AsgiRequest(http.HttpRequest):
# Body handling # Body handling
self._body = message.get("body", b"") self._body = message.get("body", b"")
if message.get("body_channel", None): if message.get("body_channel", None):
body_handle_start = time.time()
while True: while True:
# Get the next chunk from the request body channel # Get the next chunk from the request body channel
chunk = None chunk = None
while chunk is 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( _, chunk = message.channel_layer.receive_many(
[message['body_channel']], [message['body_channel']],
block=True, block=True,
@ -184,6 +194,9 @@ class AsgiHandler(base.BaseHandler):
} }
) )
response = http.HttpResponseBadRequest() 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: else:
try: try:
response = self.get_response(request) response = self.get_response(request)

View File

@ -4,6 +4,7 @@ from django.utils import six
from channels import Channel from channels import Channel
from channels.tests import ChannelTestCase from channels.tests import ChannelTestCase
from channels.handler import AsgiRequest from channels.handler import AsgiRequest
from channels.exceptions import RequestTimeout
class RequestTests(ChannelTestCase): class RequestTests(ChannelTestCase):
@ -188,3 +189,30 @@ class RequestTests(ChannelTestCase):
self.assertEqual(request.method, "PUT") self.assertEqual(request.method, "PUT")
self.assertEqual(request.read(3), b"one") self.assertEqual(request.read(3), b"one")
self.assertEqual(request.read(), b"twothree") 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"))