123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- import datetime
- import json
- import mimetypes
- import os
- import re
- import sys
- import time
- from email.header import Header
- from http.client import responses
- from urllib.parse import quote, urlparse
-
- from django.conf import settings
- from django.core import signals, signing
- 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.encoding import iri_to_uri
- from django.utils.http import http_date
-
- _charset_from_content_type_re = re.compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I)
-
-
- class BadHeaderError(ValueError):
- pass
-
-
- class HttpResponseBase:
- """
- An HTTP response base class with dictionary-accessed headers.
-
- This class doesn't handle content. It should not be used directly.
- Use the HttpResponse and StreamingHttpResponse subclasses instead.
- """
-
- 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._closable_objects = []
- # This parameter is set by the handler. It's necessary to preserve the
- # historical behavior of request_finished.
- self._handler_class = None
- self.cookies = SimpleCookie()
- self.closed = False
- if status is not None:
- try:
- self.status_code = int(status)
- except (ValueError, TypeError):
- raise TypeError('HTTP status code must be an integer.')
-
- if not 100 <= self.status_code <= 599:
- raise ValueError('HTTP status code must be an integer from 100 to 599.')
- self._reason_phrase = reason
- self._charset = charset
- if content_type is None:
- content_type = '%s; charset=%s' % (settings.DEFAULT_CONTENT_TYPE,
- self.charset)
- self['Content-Type'] = content_type
-
- @property
- def reason_phrase(self):
- if self._reason_phrase is not None:
- return self._reason_phrase
- # Leave self._reason_phrase unset in order to use the default
- # reason phrase for status code.
- return responses.get(self.status_code, 'Unknown Status Code')
-
- @reason_phrase.setter
- def reason_phrase(self, value):
- self._reason_phrase = value
-
- @property
- def charset(self):
- if self._charset is not None:
- return self._charset
- content_type = self.get('Content-Type', '')
- matched = _charset_from_content_type_re.search(content_type)
- if matched:
- # Extract the charset and strip its double quotes
- return matched.group('charset').replace('"', '')
- return settings.DEFAULT_CHARSET
-
- @charset.setter
- def charset(self, value):
- self._charset = value
-
- def serialize_headers(self):
- """HTTP headers as a bytestring."""
- def to_bytes(val, encoding):
- return val if isinstance(val, bytes) else val.encode(encoding)
-
- headers = [
- (to_bytes(key, 'ascii') + b': ' + to_bytes(value, 'latin-1'))
- for key, value in self._headers.values()
- ]
- return b'\r\n'.join(headers)
-
- __bytes__ = serialize_headers
-
- @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
-
- 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)
-
- def __delitem__(self, header):
- self._headers.pop(header.lower(), False)
-
- def __getitem__(self, header):
- return self._headers[header.lower()][1]
-
- def has_header(self, header):
- """Case-insensitive check for a header."""
- return header.lower() in self._headers
-
- __contains__ = has_header
-
- def items(self):
- return self._headers.values()
-
- def get(self, header, alternate=None):
- return self._headers.get(header.lower(), (None, alternate))[1]
-
- def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
- domain=None, secure=False, httponly=False, samesite=None):
- """
- Set a cookie.
-
- ``expires`` can be:
- - a string in the correct format,
- - a naive ``datetime.datetime`` object in UTC,
- - an aware ``datetime.datetime`` object in any time zone.
- If it is a ``datetime.datetime`` object then calculate ``max_age``.
- """
- self.cookies[key] = value
- if expires is not None:
- if isinstance(expires, datetime.datetime):
- if timezone.is_aware(expires):
- expires = timezone.make_naive(expires, timezone.utc)
- delta = expires - expires.utcnow()
- # Add one second so the date matches exactly (a fraction of
- # time gets lost between converting to a timedelta and
- # then the date string).
- delta = delta + datetime.timedelta(seconds=1)
- # Just set max_age - the max_age logic will set expires.
- expires = None
- max_age = max(0, delta.days * 86400 + delta.seconds)
- else:
- self.cookies[key]['expires'] = expires
- else:
- self.cookies[key]['expires'] = ''
- if max_age is not None:
- self.cookies[key]['max-age'] = max_age
- # IE requires expires, so set it if hasn't been already.
- if not expires:
- self.cookies[key]['expires'] = http_date(time.time() + max_age)
- if path is not None:
- self.cookies[key]['path'] = path
- if domain is not None:
- self.cookies[key]['domain'] = domain
- if secure:
- self.cookies[key]['secure'] = True
- if httponly:
- self.cookies[key]['httponly'] = True
- if samesite:
- if samesite.lower() not in ('lax', 'strict'):
- raise ValueError('samesite must be "lax" or "strict".')
- self.cookies[key]['samesite'] = samesite
-
- def setdefault(self, key, value):
- """Set a header unless it has already been set."""
- if key not in self:
- self[key] = value
-
- def set_signed_cookie(self, key, value, salt='', **kwargs):
- value = signing.get_cookie_signer(salt=key + salt).sign(value)
- return self.set_cookie(key, value, **kwargs)
-
- def delete_cookie(self, key, path='/', domain=None):
- # Most browsers ignore the Set-Cookie header if the cookie name starts
- # with __Host- or __Secure- and the cookie doesn't use the secure flag.
- secure = key.startswith(('__Secure-', '__Host-'))
- self.set_cookie(
- key, max_age=0, path=path, domain=domain, secure=secure,
- expires='Thu, 01 Jan 1970 00:00:00 GMT',
- )
-
- # Common methods used by subclasses
-
- def make_bytes(self, value):
- """Turn a value into a bytestring encoded in the output charset."""
- # Per PEP 3333, this response body must be bytes. To avoid returning
- # an instance of a subclass, this function returns `bytes(value)`.
- # This doesn't make a copy when `value` already contains bytes.
-
- # Handle string types -- we can't rely on force_bytes here because:
- # - Python attempts str conversion first
- # - when self._charset != 'utf-8' it re-encodes the content
- if isinstance(value, bytes):
- return bytes(value)
- if isinstance(value, str):
- return bytes(value.encode(self.charset))
- # Handle non-string types.
- return str(value).encode(self.charset)
-
- # These methods partially implement the file-like object interface.
- # See https://docs.python.org/library/io.html#io.IOBase
-
- # The WSGI server must call this method upon completion of the request.
- # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
- def close(self):
- for closable in self._closable_objects:
- try:
- closable.close()
- except Exception:
- pass
- self.closed = True
- signals.request_finished.send(sender=self._handler_class)
-
- def write(self, content):
- raise IOError("This %s instance is not writable" % self.__class__.__name__)
-
- def flush(self):
- pass
-
- def tell(self):
- raise IOError("This %s instance cannot tell its position" % self.__class__.__name__)
-
- # These methods partially implement a stream-like object interface.
- # See https://docs.python.org/library/io.html#io.IOBase
-
- def readable(self):
- return False
-
- def seekable(self):
- return False
-
- def writable(self):
- return False
-
- def writelines(self, lines):
- raise IOError("This %s instance is not writable" % self.__class__.__name__)
-
-
- class HttpResponse(HttpResponseBase):
- """
- An HTTP response class with a string as content.
-
- This content that can be read, appended to, or replaced.
- """
-
- streaming = False
-
- def __init__(self, content=b'', *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Content is a bytestring. See the `content` property methods.
- self.content = content
-
- def __repr__(self):
- return '<%(cls)s status_code=%(status_code)d%(content_type)s>' % {
- 'cls': self.__class__.__name__,
- 'status_code': self.status_code,
- 'content_type': self._content_type_for_repr,
- }
-
- def serialize(self):
- """Full HTTP message, including headers, as a bytestring."""
- return self.serialize_headers() + b'\r\n\r\n' + self.content
-
- __bytes__ = serialize
-
- @property
- def content(self):
- return b''.join(self._container)
-
- @content.setter
- def content(self, value):
- # Consume iterators upon assignment to allow repeated iteration.
- if hasattr(value, '__iter__') and not isinstance(value, (bytes, str)):
- content = b''.join(self.make_bytes(chunk) for chunk in value)
- if hasattr(value, 'close'):
- try:
- value.close()
- except Exception:
- pass
- else:
- content = self.make_bytes(value)
- # Create a list of properly encoded bytestrings to support write().
- self._container = [content]
-
- def __iter__(self):
- return iter(self._container)
-
- def write(self, content):
- self._container.append(self.make_bytes(content))
-
- def tell(self):
- return len(self.content)
-
- def getvalue(self):
- return self.content
-
- def writable(self):
- return True
-
- def writelines(self, lines):
- for line in lines:
- self.write(line)
-
-
- class StreamingHttpResponse(HttpResponseBase):
- """
- A streaming HTTP response class with an iterator as content.
-
- This should only be iterated once, when the response is streamed to the
- client. However, it can be appended to or replaced with a new iterator
- that wraps the original content (or yields entirely new content).
- """
-
- streaming = True
-
- def __init__(self, streaming_content=(), *args, **kwargs):
- super().__init__(*args, **kwargs)
- # `streaming_content` should be an iterable of bytestrings.
- # See the `streaming_content` property methods.
- self.streaming_content = streaming_content
-
- @property
- def content(self):
- raise AttributeError(
- "This %s instance has no `content` attribute. Use "
- "`streaming_content` instead." % self.__class__.__name__
- )
-
- @property
- def streaming_content(self):
- return map(self.make_bytes, self._iterator)
-
- @streaming_content.setter
- 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.
- self._iterator = iter(value)
- if hasattr(value, 'close'):
- self._closable_objects.append(value)
-
- def __iter__(self):
- return self.streaming_content
-
- def getvalue(self):
- return b''.join(self.streaming_content)
-
-
- class FileResponse(StreamingHttpResponse):
- """
- A streaming HTTP response class optimized for files.
- """
- block_size = 4096
-
- def __init__(self, *args, as_attachment=False, filename='', **kwargs):
- self.as_attachment = as_attachment
- self.filename = filename
- super().__init__(*args, **kwargs)
-
- def _set_streaming_content(self, value):
- if not hasattr(value, 'read'):
- self.file_to_stream = None
- return super()._set_streaming_content(value)
-
- self.file_to_stream = filelike = value
- if hasattr(filelike, 'close'):
- self._closable_objects.append(filelike)
- value = iter(lambda: filelike.read(self.block_size), b'')
- self.set_headers(filelike)
- super()._set_streaming_content(value)
-
- def set_headers(self, filelike):
- """
- Set some common response headers (Content-Length, Content-Type, and
- Content-Disposition) based on the `filelike` response content.
- """
- encoding_map = {
- 'bzip2': 'application/x-bzip',
- 'gzip': 'application/gzip',
- 'xz': 'application/x-xz',
- }
- 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)
- elif hasattr(filelike, 'getbuffer'):
- self['Content-Length'] = filelike.getbuffer().nbytes
-
- if self.get('Content-Type', '').startswith(settings.DEFAULT_CONTENT_TYPE):
- 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'
- else:
- self['Content-Type'] = 'application/octet-stream'
-
- if self.as_attachment:
- filename = self.filename or os.path.basename(filename)
- if filename:
- try:
- filename.encode('ascii')
- file_expr = 'filename="{}"'.format(filename)
- except UnicodeEncodeError:
- file_expr = "filename*=utf-8''{}".format(quote(filename))
- self['Content-Disposition'] = 'attachment; {}'.format(file_expr)
-
-
- class HttpResponseRedirectBase(HttpResponse):
- allowed_schemes = ['http', 'https', 'ftp']
-
- def __init__(self, redirect_to, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self['Location'] = iri_to_uri(redirect_to)
- parsed = urlparse(str(redirect_to))
- if parsed.scheme and parsed.scheme not in self.allowed_schemes:
- raise DisallowedRedirect("Unsafe redirect to URL with protocol '%s'" % parsed.scheme)
-
- url = property(lambda self: self['Location'])
-
- def __repr__(self):
- return '<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">' % {
- 'cls': self.__class__.__name__,
- 'status_code': self.status_code,
- 'content_type': self._content_type_for_repr,
- 'url': self.url,
- }
-
-
- class HttpResponseRedirect(HttpResponseRedirectBase):
- status_code = 302
-
-
- class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
- status_code = 301
-
-
- class HttpResponseNotModified(HttpResponse):
- status_code = 304
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- del self['content-type']
-
- @HttpResponse.content.setter
- def content(self, value):
- if value:
- raise AttributeError("You cannot set content to a 304 (Not Modified) response")
- self._container = []
-
-
- class HttpResponseBadRequest(HttpResponse):
- status_code = 400
-
-
- class HttpResponseNotFound(HttpResponse):
- status_code = 404
-
-
- class HttpResponseForbidden(HttpResponse):
- status_code = 403
-
-
- class HttpResponseNotAllowed(HttpResponse):
- status_code = 405
-
- def __init__(self, permitted_methods, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self['Allow'] = ', '.join(permitted_methods)
-
- def __repr__(self):
- return '<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>' % {
- 'cls': self.__class__.__name__,
- 'status_code': self.status_code,
- 'content_type': self._content_type_for_repr,
- 'methods': self['Allow'],
- }
-
-
- class HttpResponseGone(HttpResponse):
- status_code = 410
-
-
- class HttpResponseServerError(HttpResponse):
- status_code = 500
-
-
- class Http404(Exception):
- pass
-
-
- class JsonResponse(HttpResponse):
- """
- An HTTP response class that consumes data to be serialized to JSON.
-
- :param data: Data to be dumped into json. By default only ``dict`` objects
- are allowed to be passed due to a security flaw before EcmaScript 5. See
- the ``safe`` parameter for more information.
- :param encoder: Should be a json encoder class. Defaults to
- ``django.core.serializers.json.DjangoJSONEncoder``.
- :param safe: Controls if only ``dict`` objects may be serialized. Defaults
- to ``True``.
- :param json_dumps_params: A dictionary of kwargs passed to json.dumps().
- """
-
- def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,
- json_dumps_params=None, **kwargs):
- if safe and not isinstance(data, dict):
- raise TypeError(
- 'In order to allow non-dict objects to be serialized set the '
- 'safe parameter to False.'
- )
- if json_dumps_params is None:
- json_dumps_params = {}
- kwargs.setdefault('content_type', 'application/json')
- data = json.dumps(data, cls=encoder, **json_dumps_params)
- super().__init__(content=data, **kwargs)
|