summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorTom Carrick <tom@carrick.eu>2020-07-14 13:32:24 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-09-14 08:41:59 +0200
commitbcc2befd0e9c1885e45b46d0b0bcdc11def8b249 (patch)
tree59fab69a3182286da87fcd6fe05a8ce0f4277a5a /django
parent71ae1ab0123582cc5bfe0f7d5f4cc19a9412f396 (diff)
Fixed #31789 -- Added a new headers interface to HttpResponse.
Diffstat (limited to 'django')
-rw-r--r--django/contrib/admin/tests.py2
-rw-r--r--django/contrib/admindocs/middleware.py2
-rw-r--r--django/contrib/sitemaps/views.py4
-rw-r--r--django/contrib/syndication/views.py2
-rw-r--r--django/http/response.py128
-rw-r--r--django/middleware/clickjacking.py6
-rw-r--r--django/middleware/common.py2
-rw-r--r--django/middleware/gzip.py8
-rw-r--r--django/middleware/locale.py2
-rw-r--r--django/middleware/security.py8
-rw-r--r--django/utils/cache.py22
-rw-r--r--django/views/decorators/http.py4
-rw-r--r--django/views/generic/base.py4
-rw-r--r--django/views/static.py4
14 files changed, 114 insertions, 84 deletions
diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py
index 941d030d30..482027b1ae 100644
--- a/django/contrib/admin/tests.py
+++ b/django/contrib/admin/tests.py
@@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
class CSPMiddleware(MiddlewareMixin):
"""The admin's JavaScript should be compatible with CSP."""
def process_response(self, request, response):
- response['Content-Security-Policy'] = "default-src 'self'"
+ response.headers['Content-Security-Policy'] = "default-src 'self'"
return response
diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py
index 77d77af715..4779db8366 100644
--- a/django/contrib/admindocs/middleware.py
+++ b/django/contrib/admindocs/middleware.py
@@ -24,5 +24,5 @@ class XViewMiddleware(MiddlewareMixin):
if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or
(request.user.is_active and request.user.is_staff)):
response = HttpResponse()
- response['X-View'] = get_view_name(view_func)
+ response.headers['X-View'] = get_view_name(view_func)
return response
diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py
index ab6b9a6d24..bffdebb082 100644
--- a/django/contrib/sitemaps/views.py
+++ b/django/contrib/sitemaps/views.py
@@ -14,7 +14,7 @@ def x_robots_tag(func):
@wraps(func)
def inner(request, *args, **kwargs):
response = func(request, *args, **kwargs)
- response['X-Robots-Tag'] = 'noindex, noodp, noarchive'
+ response.headers['X-Robots-Tag'] = 'noindex, noodp, noarchive'
return response
return inner
@@ -88,5 +88,5 @@ def sitemap(request, sitemaps, section=None,
if all_sites_lastmod and lastmod is not None:
# if lastmod is defined for all sites, set header so as
# ConditionalGetMiddleware is able to send 304 NOT MODIFIED
- response['Last-Modified'] = http_date(timegm(lastmod))
+ response.headers['Last-Modified'] = http_date(timegm(lastmod))
return response
diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
index df97103318..6d567dd7db 100644
--- a/django/contrib/syndication/views.py
+++ b/django/contrib/syndication/views.py
@@ -42,7 +42,7 @@ class Feed:
if hasattr(self, 'item_pubdate') or hasattr(self, 'item_updateddate'):
# if item_pubdate or item_updateddate is defined for the feed, set
# header so as ConditionalGetMiddleware is able to send 304 NOT MODIFIED
- response['Last-Modified'] = http_date(
+ response.headers['Last-Modified'] = http_date(
timegm(feedgen.latest_post_date().utctimetuple()))
feedgen.write(response, 'utf-8')
return response
diff --git a/django/http/response.py b/django/http/response.py
index 64ac205087..e679c856c0 100644
--- a/django/http/response.py
+++ b/django/http/response.py
@@ -5,6 +5,7 @@ import os
import re
import sys
import time
+from collections.abc import Mapping
from email.header import Header
from http.client import responses
from urllib.parse import quote, urlparse
@@ -15,6 +16,7 @@ from django.core.exceptions import DisallowedRedirect
from django.core.serializers.json import DjangoJSONEncoder
from django.http.cookie import SimpleCookie
from django.utils import timezone
+from django.utils.datastructures import CaseInsensitiveMapping
from django.utils.encoding import iri_to_uri
from django.utils.http import http_date
from django.utils.regex_helper import _lazy_re_compile
@@ -22,6 +24,65 @@ from django.utils.regex_helper import _lazy_re_compile
_charset_from_content_type_re = _lazy_re_compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I)
+class ResponseHeaders(CaseInsensitiveMapping):
+ def __init__(self, data):
+ """
+ Populate the initial data using __setitem__ to ensure values are
+ correctly encoded.
+ """
+ if not isinstance(data, Mapping):
+ data = {
+ k: v
+ for k, v in CaseInsensitiveMapping._destruct_iterable_mapping_values(data)
+ }
+ self._store = {}
+ for header, value in data.items():
+ self[header] = value
+
+ def _convert_to_charset(self, value, charset, mime_encode=False):
+ """
+ Convert headers key/value to ascii/latin-1 native strings.
+ `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
+ `value` can't be represented in the given charset, apply MIME-encoding.
+ """
+ if not isinstance(value, (bytes, str)):
+ value = str(value)
+ if (
+ (isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or
+ (isinstance(value, str) and ('\n' in value or '\r' in value))
+ ):
+ raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
+ try:
+ if isinstance(value, str):
+ # Ensure string is valid in given charset
+ value.encode(charset)
+ else:
+ # Convert bytestring using given charset
+ value = value.decode(charset)
+ except UnicodeError as e:
+ if mime_encode:
+ value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode()
+ else:
+ e.reason += ', HTTP response headers must be in %s format' % charset
+ raise
+ return value
+
+ def __delitem__(self, key):
+ self.pop(key)
+
+ def __setitem__(self, key, value):
+ key = self._convert_to_charset(key, 'ascii')
+ value = self._convert_to_charset(value, 'latin-1', mime_encode=True)
+ self._store[key.lower()] = (key, value)
+
+ def pop(self, key, default=None):
+ return self._store.pop(key.lower(), default)
+
+ def setdefault(self, key, value):
+ if key not in self:
+ self[key] = value
+
+
class BadHeaderError(ValueError):
pass
@@ -37,10 +98,7 @@ class HttpResponseBase:
status_code = 200
def __init__(self, content_type=None, status=None, reason=None, charset=None):
- # _headers is a mapping of the lowercase name to the original case of
- # the header (required for working with legacy systems) and the header
- # value. Both the name of the header and its value are ASCII strings.
- self._headers = {}
+ self.headers = ResponseHeaders({})
self._resource_closers = []
# This parameter is set by the handler. It's necessary to preserve the
# historical behavior of request_finished.
@@ -95,7 +153,7 @@ class HttpResponseBase:
headers = [
(to_bytes(key, 'ascii') + b': ' + to_bytes(value, 'latin-1'))
- for key, value in self._headers.values()
+ for key, value in self.headers.items()
]
return b'\r\n'.join(headers)
@@ -103,57 +161,28 @@ class HttpResponseBase:
@property
def _content_type_for_repr(self):
- return ', "%s"' % self['Content-Type'] if 'Content-Type' in self else ''
-
- def _convert_to_charset(self, value, charset, mime_encode=False):
- """
- Convert headers key/value to ascii/latin-1 native strings.
-
- `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
- `value` can't be represented in the given charset, apply MIME-encoding.
- """
- if not isinstance(value, (bytes, str)):
- value = str(value)
- if ((isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or
- isinstance(value, str) and ('\n' in value or '\r' in value)):
- raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
- try:
- if isinstance(value, str):
- # Ensure string is valid in given charset
- value.encode(charset)
- else:
- # Convert bytestring using given charset
- value = value.decode(charset)
- except UnicodeError as e:
- if mime_encode:
- value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode()
- else:
- e.reason += ', HTTP response headers must be in %s format' % charset
- raise
- return value
+ return ', "%s"' % self.headers['Content-Type'] if 'Content-Type' in self.headers else ''
def __setitem__(self, header, value):
- header = self._convert_to_charset(header, 'ascii')
- value = self._convert_to_charset(value, 'latin-1', mime_encode=True)
- self._headers[header.lower()] = (header, value)
+ self.headers[header] = value
def __delitem__(self, header):
- self._headers.pop(header.lower(), False)
+ del self.headers[header]
def __getitem__(self, header):
- return self._headers[header.lower()][1]
+ return self.headers[header]
def has_header(self, header):
"""Case-insensitive check for a header."""
- return header.lower() in self._headers
+ return header in self.headers
__contains__ = has_header
def items(self):
- return self._headers.values()
+ return self.headers.items()
def get(self, header, alternate=None):
- return self._headers.get(header.lower(), (None, alternate))[1]
+ return self.headers.get(header, alternate)
def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
domain=None, secure=False, httponly=False, samesite=None):
@@ -203,8 +232,7 @@ class HttpResponseBase:
def setdefault(self, key, value):
"""Set a header unless it has already been set."""
- if key not in self:
- self[key] = value
+ self.headers.setdefault(key, value)
def set_signed_cookie(self, key, value, salt='', **kwargs):
value = signing.get_cookie_signer(salt=key + salt).sign(value)
@@ -430,19 +458,19 @@ class FileResponse(StreamingHttpResponse):
filename = getattr(filelike, 'name', None)
filename = filename if (isinstance(filename, str) and filename) else self.filename
if os.path.isabs(filename):
- self['Content-Length'] = os.path.getsize(filelike.name)
+ self.headers['Content-Length'] = os.path.getsize(filelike.name)
elif hasattr(filelike, 'getbuffer'):
- self['Content-Length'] = filelike.getbuffer().nbytes
+ self.headers['Content-Length'] = filelike.getbuffer().nbytes
- if self.get('Content-Type', '').startswith('text/html'):
+ if self.headers.get('Content-Type', '').startswith('text/html'):
if filename:
content_type, encoding = mimetypes.guess_type(filename)
# Encoding isn't set to prevent browsers from automatically
# uncompressing files.
content_type = encoding_map.get(encoding, content_type)
- self['Content-Type'] = content_type or 'application/octet-stream'
+ self.headers['Content-Type'] = content_type or 'application/octet-stream'
else:
- self['Content-Type'] = 'application/octet-stream'
+ self.headers['Content-Type'] = 'application/octet-stream'
filename = self.filename or os.path.basename(filename)
if filename:
@@ -452,9 +480,9 @@ class FileResponse(StreamingHttpResponse):
file_expr = 'filename="{}"'.format(filename)
except UnicodeEncodeError:
file_expr = "filename*=utf-8''{}".format(quote(filename))
- self['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
+ self.headers['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
elif self.as_attachment:
- self['Content-Disposition'] = 'attachment'
+ self.headers['Content-Disposition'] = 'attachment'
class HttpResponseRedirectBase(HttpResponse):
diff --git a/django/middleware/clickjacking.py b/django/middleware/clickjacking.py
index 478ed3cd7e..0161f8eb8f 100644
--- a/django/middleware/clickjacking.py
+++ b/django/middleware/clickjacking.py
@@ -30,8 +30,10 @@ class XFrameOptionsMiddleware(MiddlewareMixin):
if getattr(response, 'xframe_options_exempt', False):
return response
- response['X-Frame-Options'] = self.get_xframe_options_value(request,
- response)
+ response.headers['X-Frame-Options'] = self.get_xframe_options_value(
+ request,
+ response,
+ )
return response
def get_xframe_options_value(self, request, response):
diff --git a/django/middleware/common.py b/django/middleware/common.py
index 7e75e81303..e6f30f44ad 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -110,7 +110,7 @@ class CommonMiddleware(MiddlewareMixin):
# Add the Content-Length header to non-streaming responses if not
# already set.
if not response.streaming and not response.has_header('Content-Length'):
- response['Content-Length'] = str(len(response.content))
+ response.headers['Content-Length'] = str(len(response.content))
return response
diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py
index 14346c5b12..350466151d 100644
--- a/django/middleware/gzip.py
+++ b/django/middleware/gzip.py
@@ -31,21 +31,21 @@ class GZipMiddleware(MiddlewareMixin):
# Delete the `Content-Length` header for streaming content, because
# we won't know the compressed size until we stream it.
response.streaming_content = compress_sequence(response.streaming_content)
- del response['Content-Length']
+ del response.headers['Content-Length']
else:
# Return the compressed content only if it's actually shorter.
compressed_content = compress_string(response.content)
if len(compressed_content) >= len(response.content):
return response
response.content = compressed_content
- response['Content-Length'] = str(len(response.content))
+ response.headers['Content-Length'] = str(len(response.content))
# If there is a strong ETag, make it weak to fulfill the requirements
# of RFC 7232 section-2.1 while also allowing conditional request
# matches on ETags.
etag = response.get('ETag')
if etag and etag.startswith('"'):
- response['ETag'] = 'W/' + etag
- response['Content-Encoding'] = 'gzip'
+ response.headers['ETag'] = 'W/' + etag
+ response.headers['Content-Encoding'] = 'gzip'
return response
diff --git a/django/middleware/locale.py b/django/middleware/locale.py
index e4f3537320..0bbdda3309 100644
--- a/django/middleware/locale.py
+++ b/django/middleware/locale.py
@@ -57,5 +57,5 @@ class LocaleMiddleware(MiddlewareMixin):
if not (i18n_patterns_used and language_from_path):
patch_vary_headers(response, ('Accept-Language',))
- response.setdefault('Content-Language', language)
+ response.headers.setdefault('Content-Language', language)
return response
diff --git a/django/middleware/security.py b/django/middleware/security.py
index 44921cd22b..d923893dc5 100644
--- a/django/middleware/security.py
+++ b/django/middleware/security.py
@@ -38,18 +38,18 @@ class SecurityMiddleware(MiddlewareMixin):
sts_header = sts_header + "; includeSubDomains"
if self.sts_preload:
sts_header = sts_header + "; preload"
- response['Strict-Transport-Security'] = sts_header
+ response.headers['Strict-Transport-Security'] = sts_header
if self.content_type_nosniff:
- response.setdefault('X-Content-Type-Options', 'nosniff')
+ response.headers.setdefault('X-Content-Type-Options', 'nosniff')
if self.xss_filter:
- response.setdefault('X-XSS-Protection', '1; mode=block')
+ response.headers.setdefault('X-XSS-Protection', '1; mode=block')
if self.referrer_policy:
# Support a comma-separated string or iterable of values to allow
# fallback.
- response.setdefault('Referrer-Policy', ','.join(
+ response.headers.setdefault('Referrer-Policy', ','.join(
[v.strip() for v in self.referrer_policy.split(',')]
if isinstance(self.referrer_policy, str) else self.referrer_policy
))
diff --git a/django/utils/cache.py b/django/utils/cache.py
index 72f017f38a..0541d373ee 100644
--- a/django/utils/cache.py
+++ b/django/utils/cache.py
@@ -62,7 +62,7 @@ def patch_cache_control(response, **kwargs):
cc = defaultdict(set)
if response.get('Cache-Control'):
- for field in cc_delim_re.split(response['Cache-Control']):
+ for field in cc_delim_re.split(response.headers['Cache-Control']):
directive, value = dictitem(field)
if directive == 'no-cache':
# no-cache supports multiple field names.
@@ -100,7 +100,7 @@ def patch_cache_control(response, **kwargs):
else:
directives.append(dictvalue(directive, values))
cc = ', '.join(directives)
- response['Cache-Control'] = cc
+ response.headers['Cache-Control'] = cc
def get_max_age(response):
@@ -110,7 +110,7 @@ def get_max_age(response):
"""
if not response.has_header('Cache-Control'):
return
- cc = dict(_to_tuple(el) for el in cc_delim_re.split(response['Cache-Control']))
+ cc = dict(_to_tuple(el) for el in cc_delim_re.split(response.headers['Cache-Control']))
try:
return int(cc['max-age'])
except (ValueError, TypeError, KeyError):
@@ -119,7 +119,7 @@ def get_max_age(response):
def set_response_etag(response):
if not response.streaming and response.content:
- response['ETag'] = quote_etag(hashlib.md5(response.content).hexdigest())
+ response.headers['ETag'] = quote_etag(hashlib.md5(response.content).hexdigest())
return response
@@ -140,7 +140,7 @@ def _not_modified(request, response=None):
# Last-Modified.
for header in ('Cache-Control', 'Content-Location', 'Date', 'ETag', 'Expires', 'Last-Modified', 'Vary'):
if header in response:
- new_response[header] = response[header]
+ new_response.headers[header] = response.headers[header]
# Preserve cookies as per the cookie specification: "If a proxy server
# receives a response which contains a Set-cookie header, it should
@@ -261,7 +261,7 @@ def patch_response_headers(response, cache_timeout=None):
if cache_timeout < 0:
cache_timeout = 0 # Can't have max-age negative
if not response.has_header('Expires'):
- response['Expires'] = http_date(time.time() + cache_timeout)
+ response.headers['Expires'] = http_date(time.time() + cache_timeout)
patch_cache_control(response, max_age=cache_timeout)
@@ -284,7 +284,7 @@ def patch_vary_headers(response, newheaders):
# implementations may rely on the order of the Vary contents in, say,
# computing an MD5 hash.
if response.has_header('Vary'):
- vary_headers = cc_delim_re.split(response['Vary'])
+ vary_headers = cc_delim_re.split(response.headers['Vary'])
else:
vary_headers = []
# Use .lower() here so we treat headers as case-insensitive.
@@ -293,9 +293,9 @@ def patch_vary_headers(response, newheaders):
if newheader.lower() not in existing_headers]
vary_headers += additional_headers
if '*' in vary_headers:
- response['Vary'] = '*'
+ response.headers['Vary'] = '*'
else:
- response['Vary'] = ', '.join(vary_headers)
+ response.headers['Vary'] = ', '.join(vary_headers)
def has_vary_header(response, header_query):
@@ -304,7 +304,7 @@ def has_vary_header(response, header_query):
"""
if not response.has_header('Vary'):
return False
- vary_headers = cc_delim_re.split(response['Vary'])
+ vary_headers = cc_delim_re.split(response.headers['Vary'])
existing_headers = {header.lower() for header in vary_headers}
return header_query.lower() in existing_headers
@@ -391,7 +391,7 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cach
# in that case and would result in storing the same content under
# multiple keys in the cache. See #18191 for details.
headerlist = []
- for header in cc_delim_re.split(response['Vary']):
+ for header in cc_delim_re.split(response.headers['Vary']):
header = header.upper().replace('-', '_')
if header != 'ACCEPT_LANGUAGE' or not is_accept_language_redundant:
headerlist.append('HTTP_' + header)
diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py
index 673302be83..5caf13e341 100644
--- a/django/views/decorators/http.py
+++ b/django/views/decorators/http.py
@@ -102,9 +102,9 @@ def condition(etag_func=None, last_modified_func=None):
# and if the request method is safe.
if request.method in ('GET', 'HEAD'):
if res_last_modified and not response.has_header('Last-Modified'):
- response['Last-Modified'] = http_date(res_last_modified)
+ response.headers['Last-Modified'] = http_date(res_last_modified)
if res_etag:
- response.setdefault('ETag', res_etag)
+ response.headers.setdefault('ETag', res_etag)
return response
diff --git a/django/views/generic/base.py b/django/views/generic/base.py
index 3dd957d8f8..ab800ebce8 100644
--- a/django/views/generic/base.py
+++ b/django/views/generic/base.py
@@ -107,8 +107,8 @@ class View:
def options(self, request, *args, **kwargs):
"""Handle responding to requests for the OPTIONS HTTP verb."""
response = HttpResponse()
- response['Allow'] = ', '.join(self._allowed_methods())
- response['Content-Length'] = '0'
+ response.headers['Allow'] = ', '.join(self._allowed_methods())
+ response.headers['Content-Length'] = '0'
return response
def _allowed_methods(self):
diff --git a/django/views/static.py b/django/views/static.py
index 18e137ce7b..1d4900b1da 100644
--- a/django/views/static.py
+++ b/django/views/static.py
@@ -48,9 +48,9 @@ def serve(request, path, document_root=None, show_indexes=False):
content_type, encoding = mimetypes.guess_type(str(fullpath))
content_type = content_type or 'application/octet-stream'
response = FileResponse(fullpath.open('rb'), content_type=content_type)
- response["Last-Modified"] = http_date(statobj.st_mtime)
+ response.headers["Last-Modified"] = http_date(statobj.st_mtime)
if encoding:
- response["Content-Encoding"] = encoding
+ response.headers["Content-Encoding"] = encoding
return response