[1.7.x] Fixed #24193 -- Prevented unclosed file warnings in static.serve()

This regression was caused by 818e59a3f0. The patch is a partial
backport of the new FileResponse class available in later Django
versions.
Thanks Raphaël Hertzog for the report, and Tim Graham and Collin
Anderson for the reviews.
This commit is contained in:
Claude Paroz 2015-01-21 20:59:40 +01:00
parent 7b677fe063
commit b1bf8d64fb
5 changed files with 30 additions and 11 deletions

View File

@ -1,7 +1,8 @@
from django.http.cookie import SimpleCookie, parse_cookie from django.http.cookie import SimpleCookie, parse_cookie
from django.http.request import (HttpRequest, QueryDict, from django.http.request import (HttpRequest, QueryDict,
RawPostDataException, UnreadablePostError, build_request_repr) RawPostDataException, UnreadablePostError, build_request_repr)
from django.http.response import (HttpResponse, StreamingHttpResponse, from django.http.response import (
HttpResponse, StreamingHttpResponse, FileResponse,
HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseRedirect, HttpResponsePermanentRedirect,
HttpResponseNotModified, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotModified, HttpResponseBadRequest, HttpResponseForbidden,
HttpResponseNotFound, HttpResponseNotAllowed, HttpResponseGone, HttpResponseNotFound, HttpResponseNotAllowed, HttpResponseGone,
@ -16,5 +17,5 @@ __all__ = [
'HttpResponseBadRequest', 'HttpResponseForbidden', 'HttpResponseNotFound', 'HttpResponseBadRequest', 'HttpResponseForbidden', 'HttpResponseNotFound',
'HttpResponseNotAllowed', 'HttpResponseGone', 'HttpResponseServerError', 'HttpResponseNotAllowed', 'HttpResponseGone', 'HttpResponseServerError',
'Http404', 'BadHeaderError', 'fix_location_header', 'JsonResponse', 'Http404', 'BadHeaderError', 'fix_location_header', 'JsonResponse',
'conditional_content_removal', 'FileResponse', 'conditional_content_removal',
] ]

View File

@ -382,6 +382,9 @@ class StreamingHttpResponse(HttpResponseBase):
@streaming_content.setter @streaming_content.setter
def streaming_content(self, value): def streaming_content(self, value):
self._set_streaming_content(value)
def _set_streaming_content(self, value):
# Ensure we can never iterate on "value" more than once. # Ensure we can never iterate on "value" more than once.
self._iterator = iter(value) self._iterator = iter(value)
if hasattr(value, 'close'): if hasattr(value, 'close'):
@ -391,6 +394,21 @@ class StreamingHttpResponse(HttpResponseBase):
return self.streaming_content return self.streaming_content
class FileResponse(StreamingHttpResponse):
"""
A streaming HTTP response class optimized for files.
"""
block_size = 4096
def _set_streaming_content(self, value):
if hasattr(value, 'read'):
self._iterator = iter(lambda: value.read(self.block_size), b'')
if hasattr(value, 'close'):
self._closable_objects.append(value)
else:
super(FileResponse, self)._set_streaming_content(value)
class HttpResponseRedirectBase(HttpResponse): class HttpResponseRedirectBase(HttpResponse):
allowed_schemes = ['http', 'https', 'ftp'] allowed_schemes = ['http', 'https', 'ftp']

View File

@ -11,14 +11,12 @@ import posixpath
import re import re
from django.http import (Http404, HttpResponse, HttpResponseRedirect, from django.http import (Http404, HttpResponse, HttpResponseRedirect,
HttpResponseNotModified, StreamingHttpResponse) HttpResponseNotModified, FileResponse)
from django.template import loader, Template, Context, TemplateDoesNotExist from django.template import loader, Template, Context, TemplateDoesNotExist
from django.utils.http import http_date, parse_http_date from django.utils.http import http_date, parse_http_date
from django.utils.six.moves.urllib.parse import unquote from django.utils.six.moves.urllib.parse import unquote
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
STREAM_CHUNK_SIZE = 4096
def serve(request, path, document_root=None, show_indexes=False): def serve(request, path, document_root=None, show_indexes=False):
""" """
@ -63,9 +61,7 @@ def serve(request, path, document_root=None, show_indexes=False):
return HttpResponseNotModified() return HttpResponseNotModified()
content_type, encoding = mimetypes.guess_type(fullpath) content_type, encoding = mimetypes.guess_type(fullpath)
content_type = content_type or 'application/octet-stream' content_type = content_type or 'application/octet-stream'
f = open(fullpath, 'rb') response = FileResponse(open(fullpath, 'rb'), content_type=content_type)
response = StreamingHttpResponse(iter(lambda: f.read(STREAM_CHUNK_SIZE), b''),
content_type=content_type)
response["Last-Modified"] = http_date(statobj.st_mtime) response["Last-Modified"] = http_date(statobj.st_mtime)
if stat.S_ISREG(statobj.st_mode): if stat.S_ISREG(statobj.st_mode):
response["Content-Length"] = statobj.st_size response["Content-Length"] = statobj.st_size

View File

@ -17,3 +17,6 @@ Bugfixes
* Fixed a migration crash on MySQL when migrating from a ``OneToOneField`` to a * Fixed a migration crash on MySQL when migrating from a ``OneToOneField`` to a
``ForeignKey`` (:ticket:`24163`). ``ForeignKey`` (:ticket:`24163`).
* Prevented the ``static.serve`` view from producing ``ResourceWarning``\s in
certain circumstances (security fix regression, :ticket:`24193`).

View File

@ -5,10 +5,10 @@ from os import path
import unittest import unittest
from django.conf.urls.static import static from django.conf.urls.static import static
from django.http import HttpResponseNotModified from django.http import FileResponse, HttpResponseNotModified
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
from django.utils.http import http_date from django.utils.http import http_date
from django.views.static import was_modified_since, STREAM_CHUNK_SIZE from django.views.static import was_modified_since
from .. import urls from .. import urls
from ..urls import media_dir from ..urls import media_dir
@ -37,10 +37,11 @@ class StaticTests(SimpleTestCase):
"The static view should stream files in chunks to avoid large memory usage" "The static view should stream files in chunks to avoid large memory usage"
response = self.client.get('/%s/%s' % (self.prefix, 'long-line.txt')) response = self.client.get('/%s/%s' % (self.prefix, 'long-line.txt'))
first_chunk = next(response.streaming_content) first_chunk = next(response.streaming_content)
self.assertEqual(len(first_chunk), STREAM_CHUNK_SIZE) self.assertEqual(len(first_chunk), FileResponse.block_size)
second_chunk = next(response.streaming_content) second_chunk = next(response.streaming_content)
# strip() to prevent OS line endings from causing differences # strip() to prevent OS line endings from causing differences
self.assertEqual(len(second_chunk.strip()), 1449) self.assertEqual(len(second_chunk.strip()), 1449)
response.close()
def test_unknown_mime_type(self): def test_unknown_mime_type(self):
response = self.client.get('/%s/file.unknown' % self.prefix) response = self.client.get('/%s/file.unknown' % self.prefix)