Fixed #15785 -- Stopped HttpRequest.read() from reading beyond the end of a wsgi.input stream and removed some redundant code in the multipartparser. Thanks, tomchristie, grahamd and isagalaev.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@16479 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
0278947128
commit
a6cd78662e
|
@ -135,26 +135,11 @@ class WSGIRequest(http.HttpRequest):
|
||||||
self.META['SCRIPT_NAME'] = script_name
|
self.META['SCRIPT_NAME'] = script_name
|
||||||
self.method = environ['REQUEST_METHOD'].upper()
|
self.method = environ['REQUEST_METHOD'].upper()
|
||||||
self._post_parse_error = False
|
self._post_parse_error = False
|
||||||
if type(socket._fileobject) is type and isinstance(self.environ['wsgi.input'], socket._fileobject):
|
|
||||||
# Under development server 'wsgi.input' is an instance of
|
|
||||||
# socket._fileobject which hangs indefinitely on reading bytes past
|
|
||||||
# available count. To prevent this it's wrapped in LimitedStream
|
|
||||||
# that doesn't read past Content-Length bytes.
|
|
||||||
#
|
|
||||||
# This is not done for other kinds of inputs (like flup's FastCGI
|
|
||||||
# streams) beacuse they don't suffer from this problem and we can
|
|
||||||
# avoid using another wrapper with its own .read and .readline
|
|
||||||
# implementation.
|
|
||||||
#
|
|
||||||
# The type check is done because for some reason, AppEngine
|
|
||||||
# implements _fileobject as a function, not a class.
|
|
||||||
try:
|
try:
|
||||||
content_length = int(self.environ.get('CONTENT_LENGTH', 0))
|
content_length = int(self.environ.get('CONTENT_LENGTH'))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
content_length = 0
|
content_length = 0
|
||||||
self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
|
self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
|
||||||
else:
|
|
||||||
self._stream = self.environ['wsgi.input']
|
|
||||||
self._read_started = False
|
self._read_started = False
|
||||||
|
|
||||||
def get_full_path(self):
|
def get_full_path(self):
|
||||||
|
|
|
@ -308,16 +308,6 @@ class HttpRequest(object):
|
||||||
if not hasattr(self, '_raw_post_data'):
|
if not hasattr(self, '_raw_post_data'):
|
||||||
if self._read_started:
|
if self._read_started:
|
||||||
raise Exception("You cannot access raw_post_data after reading from request's data stream")
|
raise Exception("You cannot access raw_post_data after reading from request's data stream")
|
||||||
try:
|
|
||||||
content_length = int(self.META.get('CONTENT_LENGTH', 0))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
# If CONTENT_LENGTH was empty string or not an integer, don't
|
|
||||||
# error out. We've also seen None passed in here (against all
|
|
||||||
# specs, but see ticket #8259), so we handle TypeError as well.
|
|
||||||
content_length = 0
|
|
||||||
if content_length:
|
|
||||||
self._raw_post_data = self.read(content_length)
|
|
||||||
else:
|
|
||||||
self._raw_post_data = self.read()
|
self._raw_post_data = self.read()
|
||||||
self._stream = StringIO(self._raw_post_data)
|
self._stream = StringIO(self._raw_post_data)
|
||||||
return self._raw_post_data
|
return self._raw_post_data
|
||||||
|
|
|
@ -33,7 +33,7 @@ class MultiPartParser(object):
|
||||||
A rfc2388 multipart/form-data parser.
|
A rfc2388 multipart/form-data parser.
|
||||||
|
|
||||||
``MultiValueDict.parse()`` reads the input stream in ``chunk_size`` chunks
|
``MultiValueDict.parse()`` reads the input stream in ``chunk_size`` chunks
|
||||||
and returns a tuple of ``(MultiValueDict(POST), MultiValueDict(FILES))``. If
|
and returns a tuple of ``(MultiValueDict(POST), MultiValueDict(FILES))``.
|
||||||
"""
|
"""
|
||||||
def __init__(self, META, input_data, upload_handlers, encoding=None):
|
def __init__(self, META, input_data, upload_handlers, encoding=None):
|
||||||
"""
|
"""
|
||||||
|
@ -65,14 +65,11 @@ class MultiPartParser(object):
|
||||||
raise MultiPartParserError('Invalid boundary in multipart: %s' % boundary)
|
raise MultiPartParserError('Invalid boundary in multipart: %s' % boundary)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Content-Length should contain the length of the body we are about
|
# Content-Length should contain the length of the body we are about
|
||||||
# to receive.
|
# to receive.
|
||||||
#
|
|
||||||
try:
|
try:
|
||||||
content_length = int(META.get('HTTP_CONTENT_LENGTH', META.get('CONTENT_LENGTH',0)))
|
content_length = int(META.get('HTTP_CONTENT_LENGTH', META.get('CONTENT_LENGTH',0)))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
# For now set it to 0; we'll try again later on down.
|
|
||||||
content_length = 0
|
content_length = 0
|
||||||
|
|
||||||
if content_length < 0:
|
if content_length < 0:
|
||||||
|
@ -110,12 +107,10 @@ class MultiPartParser(object):
|
||||||
if self._content_length == 0:
|
if self._content_length == 0:
|
||||||
return QueryDict(MultiValueDict(), encoding=self._encoding), MultiValueDict()
|
return QueryDict(MultiValueDict(), encoding=self._encoding), MultiValueDict()
|
||||||
|
|
||||||
limited_input_data = LimitBytes(self._input_data, self._content_length)
|
|
||||||
|
|
||||||
# See if the handler will want to take care of the parsing.
|
# See if the handler will want to take care of the parsing.
|
||||||
# This allows overriding everything if somebody wants it.
|
# This allows overriding everything if somebody wants it.
|
||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
result = handler.handle_raw_input(limited_input_data,
|
result = handler.handle_raw_input(self._input_data,
|
||||||
self._meta,
|
self._meta,
|
||||||
self._content_length,
|
self._content_length,
|
||||||
self._boundary,
|
self._boundary,
|
||||||
|
@ -128,7 +123,7 @@ class MultiPartParser(object):
|
||||||
self._files = MultiValueDict()
|
self._files = MultiValueDict()
|
||||||
|
|
||||||
# Instantiate the parser and stream:
|
# Instantiate the parser and stream:
|
||||||
stream = LazyStream(ChunkIter(limited_input_data, self._chunk_size))
|
stream = LazyStream(ChunkIter(self._input_data, self._chunk_size))
|
||||||
|
|
||||||
# Whether or not to signal a file-completion at the beginning of the loop.
|
# Whether or not to signal a file-completion at the beginning of the loop.
|
||||||
old_field_name = None
|
old_field_name = None
|
||||||
|
@ -225,10 +220,10 @@ class MultiPartParser(object):
|
||||||
exhaust(stream)
|
exhaust(stream)
|
||||||
except StopUpload, e:
|
except StopUpload, e:
|
||||||
if not e.connection_reset:
|
if not e.connection_reset:
|
||||||
exhaust(limited_input_data)
|
exhaust(self._input_data)
|
||||||
else:
|
else:
|
||||||
# Make sure that the request data is all fed
|
# Make sure that the request data is all fed
|
||||||
exhaust(limited_input_data)
|
exhaust(self._input_data)
|
||||||
|
|
||||||
# Signal that the upload has completed.
|
# Signal that the upload has completed.
|
||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
|
@ -390,27 +385,6 @@ class ChunkIter(object):
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
class LimitBytes(object):
|
|
||||||
""" Limit bytes for a file object. """
|
|
||||||
def __init__(self, fileobject, length):
|
|
||||||
self._file = fileobject
|
|
||||||
self.remaining = length
|
|
||||||
|
|
||||||
def read(self, num_bytes=None):
|
|
||||||
"""
|
|
||||||
Read data from the underlying file.
|
|
||||||
If you ask for too much or there isn't anything left,
|
|
||||||
this will raise an InputStreamExhausted error.
|
|
||||||
"""
|
|
||||||
if self.remaining <= 0:
|
|
||||||
raise InputStreamExhausted()
|
|
||||||
if num_bytes is None:
|
|
||||||
num_bytes = self.remaining
|
|
||||||
else:
|
|
||||||
num_bytes = min(num_bytes, self.remaining)
|
|
||||||
self.remaining -= num_bytes
|
|
||||||
return self._file.read(num_bytes)
|
|
||||||
|
|
||||||
class InterBoundaryIter(object):
|
class InterBoundaryIter(object):
|
||||||
"""
|
"""
|
||||||
A Producer that will iterate over boundaries.
|
A Producer that will iterate over boundaries.
|
||||||
|
|
|
@ -9,7 +9,7 @@ from StringIO import StringIO
|
||||||
|
|
||||||
from django.core.files import temp as tempfile
|
from django.core.files import temp as tempfile
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.http.multipartparser import MultiPartParser
|
from django.http.multipartparser import MultiPartParser, MultiPartParserError
|
||||||
from django.test import TestCase, client
|
from django.test import TestCase, client
|
||||||
from django.utils import simplejson
|
from django.utils import simplejson
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
|
@ -176,6 +176,47 @@ class FileUploadTests(TestCase):
|
||||||
got = simplejson.loads(self.client.request(**r).content)
|
got = simplejson.loads(self.client.request(**r).content)
|
||||||
self.assertTrue(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file']))
|
self.assertTrue(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file']))
|
||||||
|
|
||||||
|
def test_truncated_multipart_handled_gracefully(self):
|
||||||
|
"""
|
||||||
|
If passed an incomplete multipart message, MultiPartParser does not
|
||||||
|
attempt to read beyond the end of the stream, and simply will handle
|
||||||
|
the part that can be parsed gracefully.
|
||||||
|
"""
|
||||||
|
payload = "\r\n".join([
|
||||||
|
'--' + client.BOUNDARY,
|
||||||
|
'Content-Disposition: form-data; name="file"; filename="foo.txt"',
|
||||||
|
'Content-Type: application/octet-stream',
|
||||||
|
'',
|
||||||
|
'file contents'
|
||||||
|
'--' + client.BOUNDARY + '--',
|
||||||
|
'',
|
||||||
|
])
|
||||||
|
payload = payload[:-10]
|
||||||
|
r = {
|
||||||
|
'CONTENT_LENGTH': len(payload),
|
||||||
|
'CONTENT_TYPE': client.MULTIPART_CONTENT,
|
||||||
|
'PATH_INFO': '/file_uploads/echo/',
|
||||||
|
'REQUEST_METHOD': 'POST',
|
||||||
|
'wsgi.input': client.FakePayload(payload),
|
||||||
|
}
|
||||||
|
got = simplejson.loads(self.client.request(**r).content)
|
||||||
|
self.assertEquals(got, {})
|
||||||
|
|
||||||
|
def test_empty_multipart_handled_gracefully(self):
|
||||||
|
"""
|
||||||
|
If passed an empty multipart message, MultiPartParser will return
|
||||||
|
an empty QueryDict.
|
||||||
|
"""
|
||||||
|
r = {
|
||||||
|
'CONTENT_LENGTH': 0,
|
||||||
|
'CONTENT_TYPE': client.MULTIPART_CONTENT,
|
||||||
|
'PATH_INFO': '/file_uploads/echo/',
|
||||||
|
'REQUEST_METHOD': 'POST',
|
||||||
|
'wsgi.input': client.FakePayload(''),
|
||||||
|
}
|
||||||
|
got = simplejson.loads(self.client.request(**r).content)
|
||||||
|
self.assertEquals(got, {})
|
||||||
|
|
||||||
def test_custom_upload_handler(self):
|
def test_custom_upload_handler(self):
|
||||||
# A small file (under the 5M quota)
|
# A small file (under the 5M quota)
|
||||||
smallfile = tempfile.NamedTemporaryFile()
|
smallfile = tempfile.NamedTemporaryFile()
|
||||||
|
|
|
@ -195,7 +195,10 @@ class RequestsTests(unittest.TestCase):
|
||||||
self.assertEqual(stream.read(), '')
|
self.assertEqual(stream.read(), '')
|
||||||
|
|
||||||
def test_stream(self):
|
def test_stream(self):
|
||||||
request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
|
payload = 'name=value'
|
||||||
|
request = WSGIRequest({'REQUEST_METHOD': 'POST',
|
||||||
|
'CONTENT_LENGTH': len(payload),
|
||||||
|
'wsgi.input': StringIO(payload)})
|
||||||
self.assertEqual(request.read(), 'name=value')
|
self.assertEqual(request.read(), 'name=value')
|
||||||
|
|
||||||
def test_read_after_value(self):
|
def test_read_after_value(self):
|
||||||
|
@ -203,7 +206,10 @@ class RequestsTests(unittest.TestCase):
|
||||||
Reading from request is allowed after accessing request contents as
|
Reading from request is allowed after accessing request contents as
|
||||||
POST or raw_post_data.
|
POST or raw_post_data.
|
||||||
"""
|
"""
|
||||||
request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
|
payload = 'name=value'
|
||||||
|
request = WSGIRequest({'REQUEST_METHOD': 'POST',
|
||||||
|
'CONTENT_LENGTH': len(payload),
|
||||||
|
'wsgi.input': StringIO(payload)})
|
||||||
self.assertEqual(request.POST, {u'name': [u'value']})
|
self.assertEqual(request.POST, {u'name': [u'value']})
|
||||||
self.assertEqual(request.raw_post_data, 'name=value')
|
self.assertEqual(request.raw_post_data, 'name=value')
|
||||||
self.assertEqual(request.read(), 'name=value')
|
self.assertEqual(request.read(), 'name=value')
|
||||||
|
@ -213,7 +219,10 @@ class RequestsTests(unittest.TestCase):
|
||||||
Construction of POST or raw_post_data is not allowed after reading
|
Construction of POST or raw_post_data is not allowed after reading
|
||||||
from request.
|
from request.
|
||||||
"""
|
"""
|
||||||
request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
|
payload = 'name=value'
|
||||||
|
request = WSGIRequest({'REQUEST_METHOD': 'POST',
|
||||||
|
'CONTENT_LENGTH': len(payload),
|
||||||
|
'wsgi.input': StringIO(payload)})
|
||||||
self.assertEqual(request.read(2), 'na')
|
self.assertEqual(request.read(2), 'na')
|
||||||
self.assertRaises(Exception, lambda: request.raw_post_data)
|
self.assertRaises(Exception, lambda: request.raw_post_data)
|
||||||
self.assertEqual(request.POST, {})
|
self.assertEqual(request.POST, {})
|
||||||
|
@ -261,14 +270,20 @@ class RequestsTests(unittest.TestCase):
|
||||||
self.assertEqual(request.POST, {})
|
self.assertEqual(request.POST, {})
|
||||||
|
|
||||||
def test_read_by_lines(self):
|
def test_read_by_lines(self):
|
||||||
request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
|
payload = 'name=value'
|
||||||
|
request = WSGIRequest({'REQUEST_METHOD': 'POST',
|
||||||
|
'CONTENT_LENGTH': len(payload),
|
||||||
|
'wsgi.input': StringIO(payload)})
|
||||||
self.assertEqual(list(request), ['name=value'])
|
self.assertEqual(list(request), ['name=value'])
|
||||||
|
|
||||||
def test_POST_after_raw_post_data_read(self):
|
def test_POST_after_raw_post_data_read(self):
|
||||||
"""
|
"""
|
||||||
POST should be populated even if raw_post_data is read first
|
POST should be populated even if raw_post_data is read first
|
||||||
"""
|
"""
|
||||||
request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
|
payload = 'name=value'
|
||||||
|
request = WSGIRequest({'REQUEST_METHOD': 'POST',
|
||||||
|
'CONTENT_LENGTH': len(payload),
|
||||||
|
'wsgi.input': StringIO(payload)})
|
||||||
raw_data = request.raw_post_data
|
raw_data = request.raw_post_data
|
||||||
self.assertEqual(request.POST, {u'name': [u'value']})
|
self.assertEqual(request.POST, {u'name': [u'value']})
|
||||||
|
|
||||||
|
@ -277,7 +292,10 @@ class RequestsTests(unittest.TestCase):
|
||||||
POST should be populated even if raw_post_data is read first, and then
|
POST should be populated even if raw_post_data is read first, and then
|
||||||
the stream is read second.
|
the stream is read second.
|
||||||
"""
|
"""
|
||||||
request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
|
payload = 'name=value'
|
||||||
|
request = WSGIRequest({'REQUEST_METHOD': 'POST',
|
||||||
|
'CONTENT_LENGTH': len(payload),
|
||||||
|
'wsgi.input': StringIO(payload)})
|
||||||
raw_data = request.raw_post_data
|
raw_data = request.raw_post_data
|
||||||
self.assertEqual(request.read(1), u'n')
|
self.assertEqual(request.read(1), u'n')
|
||||||
self.assertEqual(request.POST, {u'name': [u'value']})
|
self.assertEqual(request.POST, {u'name': [u'value']})
|
||||||
|
|
|
@ -913,14 +913,44 @@ class ResponseTemplateDeprecationTests(TestCase):
|
||||||
response = self.client.get("/test_client_regress/request_methods/")
|
response = self.client.get("/test_client_regress/request_methods/")
|
||||||
self.assertEqual(response.template, None)
|
self.assertEqual(response.template, None)
|
||||||
|
|
||||||
class RawPostDataTest(TestCase):
|
|
||||||
"Access to request.raw_post_data from the test client."
|
class ReadLimitedStreamTest(TestCase):
|
||||||
def test_raw_post_data(self):
|
"""
|
||||||
# Refs #14753
|
Tests that ensure that HttpRequest.raw_post_data, HttpRequest.read() and
|
||||||
try:
|
HttpRequest.read(BUFFER) have proper LimitedStream behaviour.
|
||||||
response = self.client.get("/test_client_regress/raw_post_data/")
|
|
||||||
except AssertionError:
|
Refs #14753, #15785
|
||||||
self.fail("Accessing request.raw_post_data from a view fetched with GET by the test client shouldn't fail.")
|
"""
|
||||||
|
def test_raw_post_data_from_empty_request(self):
|
||||||
|
"""HttpRequest.raw_post_data on a test client GET request should return
|
||||||
|
the empty string."""
|
||||||
|
self.assertEquals(self.client.get("/test_client_regress/raw_post_data/").content, '')
|
||||||
|
|
||||||
|
def test_read_from_empty_request(self):
|
||||||
|
"""HttpRequest.read() on a test client GET request should return the
|
||||||
|
empty string."""
|
||||||
|
self.assertEquals(self.client.get("/test_client_regress/read_all/").content, '')
|
||||||
|
|
||||||
|
def test_read_numbytes_from_empty_request(self):
|
||||||
|
"""HttpRequest.read(LARGE_BUFFER) on a test client GET request should
|
||||||
|
return the empty string."""
|
||||||
|
self.assertEquals(self.client.get("/test_client_regress/read_buffer/").content, '')
|
||||||
|
|
||||||
|
def test_read_from_nonempty_request(self):
|
||||||
|
"""HttpRequest.read() on a test client PUT request with some payload
|
||||||
|
should return that payload."""
|
||||||
|
payload = 'foobar'
|
||||||
|
self.assertEquals(self.client.put("/test_client_regress/read_all/",
|
||||||
|
data=payload,
|
||||||
|
content_type='text/plain').content, payload)
|
||||||
|
|
||||||
|
def test_read_numbytes_from_nonempty_request(self):
|
||||||
|
"""HttpRequest.read(LARGE_BUFFER) on a test client PUT request with
|
||||||
|
some payload should return that payload."""
|
||||||
|
payload = 'foobar'
|
||||||
|
self.assertEquals(self.client.put("/test_client_regress/read_buffer/",
|
||||||
|
data=payload,
|
||||||
|
content_type='text/plain').content, payload)
|
||||||
|
|
||||||
|
|
||||||
class RequestFactoryStateTest(TestCase):
|
class RequestFactoryStateTest(TestCase):
|
||||||
|
|
|
@ -27,5 +27,7 @@ urlpatterns = patterns('',
|
||||||
(r'^check_headers/$', views.check_headers),
|
(r'^check_headers/$', views.check_headers),
|
||||||
(r'^check_headers_redirect/$', RedirectView.as_view(url='/test_client_regress/check_headers/')),
|
(r'^check_headers_redirect/$', RedirectView.as_view(url='/test_client_regress/check_headers/')),
|
||||||
(r'^raw_post_data/$', views.raw_post_data),
|
(r'^raw_post_data/$', views.raw_post_data),
|
||||||
|
(r'^read_all/$', views.read_all),
|
||||||
|
(r'^read_buffer/$', views.read_buffer),
|
||||||
(r'^request_context_view/$', views.request_context_view),
|
(r'^request_context_view/$', views.request_context_view),
|
||||||
)
|
)
|
||||||
|
|
|
@ -96,6 +96,14 @@ def raw_post_data(request):
|
||||||
"A view that is requested with GET and accesses request.raw_post_data. Refs #14753."
|
"A view that is requested with GET and accesses request.raw_post_data. Refs #14753."
|
||||||
return HttpResponse(request.raw_post_data)
|
return HttpResponse(request.raw_post_data)
|
||||||
|
|
||||||
|
def read_all(request):
|
||||||
|
"A view that is requested with accesses request.read()."
|
||||||
|
return HttpResponse(request.read())
|
||||||
|
|
||||||
|
def read_buffer(request):
|
||||||
|
"A view that is requested with accesses request.read(LARGE_BUFFER)."
|
||||||
|
return HttpResponse(request.read(99999))
|
||||||
|
|
||||||
def request_context_view(request):
|
def request_context_view(request):
|
||||||
# Special attribute that won't be present on a plain HttpRequest
|
# Special attribute that won't be present on a plain HttpRequest
|
||||||
request.special_path = request.path
|
request.special_path = request.path
|
||||||
|
|
Loading…
Reference in New Issue