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.
"""
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 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)

View File

@ -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"))