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