1import datetime
2import io
3import json
4import mimetypes
5import os
6import re
7import sys
8import time
9import warnings
10from email.header import Header
11from http.client import responses
12from urllib.parse import urlsplit
13
14from asgiref.sync import async_to_sync, sync_to_async
15
16from django.conf import settings
17from django.core import signals, signing
18from django.core.exceptions import DisallowedRedirect
19from django.core.serializers.json import DjangoJSONEncoder
20from django.http.cookie import SimpleCookie
21from django.utils import timezone
22from django.utils.datastructures import CaseInsensitiveMapping
23from django.utils.encoding import iri_to_uri
24from django.utils.functional import cached_property
25from django.utils.http import content_disposition_header, http_date
26from django.utils.regex_helper import _lazy_re_compile
27
28_charset_from_content_type_re = _lazy_re_compile(
29 r";\s*charset=(?P<charset>[^\s;]+)", re.I
30)
31
32
33class ResponseHeaders(CaseInsensitiveMapping):
34 def __init__(self, data):
35 """
36 Populate the initial data using __setitem__ to ensure values are
37 correctly encoded.
38 """
39 self._store = {}
40 if data:
41 for header, value in self._unpack_items(data):
42 self[header] = value
43
44 def _convert_to_charset(self, value, charset, mime_encode=False):
45 """
46 Convert headers key/value to ascii/latin-1 native strings.
47 `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
48 `value` can't be represented in the given charset, apply MIME-encoding.
49 """
50 try:
51 if isinstance(value, str):
52 # Ensure string is valid in given charset
53 value.encode(charset)
54 elif isinstance(value, bytes):
55 # Convert bytestring using given charset
56 value = value.decode(charset)
57 else:
58 value = str(value)
59 # Ensure string is valid in given charset.
60 value.encode(charset)
61 if "\n" in value or "\r" in value:
62 raise BadHeaderError(
63 f"Header values can't contain newlines (got {value!r})"
64 )
65 except UnicodeError as e:
66 # Encoding to a string of the specified charset failed, but we
67 # don't know what type that value was, or if it contains newlines,
68 # which we may need to check for before sending it to be
69 # encoded for multiple character sets.
70 if (isinstance(value, bytes) and (b"\n" in value or b"\r" in value)) or (
71 isinstance(value, str) and ("\n" in value or "\r" in value)
72 ):
73 raise BadHeaderError(
74 f"Header values can't contain newlines (got {value!r})"
75 ) from e
76 if mime_encode:
77 value = Header(value, "utf-8", maxlinelen=sys.maxsize).encode()
78 else:
79 e.reason += ", HTTP response headers must be in %s format" % charset
80 raise
81 return value
82
83 def __delitem__(self, key):
84 self.pop(key)
85
86 def __setitem__(self, key, value):
87 key = self._convert_to_charset(key, "ascii")
88 value = self._convert_to_charset(value, "latin-1", mime_encode=True)
89 self._store[key.lower()] = (key, value)
90
91 def pop(self, key, default=None):
92 return self._store.pop(key.lower(), default)
93
94 def setdefault(self, key, value):
95 if key not in self:
96 self[key] = value
97
98
99class BadHeaderError(ValueError):
100 pass
101
102
103class HttpResponseBase:
104 """
105 An HTTP response base class with dictionary-accessed headers.
106
107 This class doesn't handle content. It should not be used directly.
108 Use the HttpResponse and StreamingHttpResponse subclasses instead.
109 """
110
111 status_code = 200
112
113 def __init__(
114 self, content_type=None, status=None, reason=None, charset=None, headers=None
115 ):
116 self.headers = ResponseHeaders(headers)
117 self._charset = charset
118 if "Content-Type" not in self.headers:
119 if content_type is None:
120 content_type = f"text/html; charset={self.charset}"
121 self.headers["Content-Type"] = content_type
122 elif content_type:
123 raise ValueError(
124 "'headers' must not contain 'Content-Type' when the "
125 "'content_type' parameter is provided."
126 )
127 self._resource_closers = []
128 # This parameter is set by the handler. It's necessary to preserve the
129 # historical behavior of request_finished.
130 self._handler_class = None
131 self.cookies = SimpleCookie()
132 self.closed = False
133 if status is not None:
134 try:
135 self.status_code = int(status)
136 except (ValueError, TypeError):
137 raise TypeError("HTTP status code must be an integer.")
138
139 if not 100 <= self.status_code <= 599:
140 raise ValueError("HTTP status code must be an integer from 100 to 599.")
141 self._reason_phrase = reason
142
143 @property
144 def reason_phrase(self):
145 if self._reason_phrase is not None:
146 return self._reason_phrase
147 # Leave self._reason_phrase unset in order to use the default
148 # reason phrase for status code.
149 return responses.get(self.status_code, "Unknown Status Code")
150
151 @reason_phrase.setter
152 def reason_phrase(self, value):
153 self._reason_phrase = value
154
155 @property
156 def charset(self):
157 if self._charset is not None:
158 return self._charset
159 # The Content-Type header may not yet be set, because the charset is
160 # being inserted *into* it.
161 if content_type := self.headers.get("Content-Type"):
162 if matched := _charset_from_content_type_re.search(content_type):
163 # Extract the charset and strip its double quotes.
164 # Note that having parsed it from the Content-Type, we don't
165 # store it back into the _charset for later intentionally, to
166 # allow for the Content-Type to be switched again later.
167 return matched["charset"].replace('"', "")
168 return settings.DEFAULT_CHARSET
169
170 @charset.setter
171 def charset(self, value):
172 self._charset = value
173
174 def serialize_headers(self):
175 """HTTP headers as a bytestring."""
176 return b"\r\n".join(
177 [
178 key.encode("ascii") + b": " + value.encode("latin-1")
179 for key, value in self.headers.items()
180 ]
181 )
182
183 __bytes__ = serialize_headers
184
185 @property
186 def _content_type_for_repr(self):
187 return (
188 ', "%s"' % self.headers["Content-Type"]
189 if "Content-Type" in self.headers
190 else ""
191 )
192
193 def __setitem__(self, header, value):
194 self.headers[header] = value
195
196 def __delitem__(self, header):
197 del self.headers[header]
198
199 def __getitem__(self, header):
200 return self.headers[header]
201
202 def has_header(self, header):
203 """Case-insensitive check for a header."""
204 return header in self.headers
205
206 __contains__ = has_header
207
208 def items(self):
209 return self.headers.items()
210
211 def get(self, header, alternate=None):
212 return self.headers.get(header, alternate)
213
214 def set_cookie(
215 self,
216 key,
217 value="",
218 max_age=None,
219 expires=None,
220 path="/",
221 domain=None,
222 secure=False,
223 httponly=False,
224 samesite=None,
225 ):
226 """
227 Set a cookie.
228
229 ``expires`` can be:
230 - a string in the correct format,
231 - a naive ``datetime.datetime`` object in UTC,
232 - an aware ``datetime.datetime`` object in any time zone.
233 If it is a ``datetime.datetime`` object then calculate ``max_age``.
234
235 ``max_age`` can be:
236 - int/float specifying seconds,
237 - ``datetime.timedelta`` object.
238 """
239 self.cookies[key] = value
240 if expires is not None:
241 if isinstance(expires, datetime.datetime):
242 if timezone.is_naive(expires):
243 expires = timezone.make_aware(expires, datetime.timezone.utc)
244 delta = expires - datetime.datetime.now(tz=datetime.timezone.utc)
245 # Add one second so the date matches exactly (a fraction of
246 # time gets lost between converting to a timedelta and
247 # then the date string).
248 delta += datetime.timedelta(seconds=1)
249 # Just set max_age - the max_age logic will set expires.
250 expires = None
251 if max_age is not None:
252 raise ValueError("'expires' and 'max_age' can't be used together.")
253 max_age = max(0, delta.days * 86400 + delta.seconds)
254 else:
255 self.cookies[key]["expires"] = expires
256 else:
257 self.cookies[key]["expires"] = ""
258 if max_age is not None:
259 if isinstance(max_age, datetime.timedelta):
260 max_age = max_age.total_seconds()
261 self.cookies[key]["max-age"] = int(max_age)
262 # IE requires expires, so set it if hasn't been already.
263 if not expires:
264 self.cookies[key]["expires"] = http_date(time.time() + max_age)
265 if path is not None:
266 self.cookies[key]["path"] = path
267 if domain is not None:
268 self.cookies[key]["domain"] = domain
269 if secure:
270 self.cookies[key]["secure"] = True
271 if httponly:
272 self.cookies[key]["httponly"] = True
273 if samesite:
274 if samesite.lower() not in ("lax", "none", "strict"):
275 raise ValueError('samesite must be "lax", "none", or "strict".')
276 self.cookies[key]["samesite"] = samesite
277
278 def setdefault(self, key, value):
279 """Set a header unless it has already been set."""
280 self.headers.setdefault(key, value)
281
282 def set_signed_cookie(self, key, value, salt="", **kwargs):
283 value = signing.get_cookie_signer(salt=key + salt).sign(value)
284 return self.set_cookie(key, value, **kwargs)
285
286 def delete_cookie(self, key, path="/", domain=None, samesite=None):
287 # Browsers can ignore the Set-Cookie header if the cookie doesn't use
288 # the secure flag and:
289 # - the cookie name starts with "__Host-" or "__Secure-", or
290 # - the samesite is "none".
291 secure = key.startswith(("__Secure-", "__Host-")) or (
292 samesite and samesite.lower() == "none"
293 )
294 self.set_cookie(
295 key,
296 max_age=0,
297 path=path,
298 domain=domain,
299 secure=secure,
300 expires="Thu, 01 Jan 1970 00:00:00 GMT",
301 samesite=samesite,
302 )
303
304 # Common methods used by subclasses
305
306 def make_bytes(self, value):
307 """Turn a value into a bytestring encoded in the output charset."""
308 # Per PEP 3333, this response body must be bytes. To avoid returning
309 # an instance of a subclass, this function returns `bytes(value)`.
310 # This doesn't make a copy when `value` already contains bytes.
311
312 # Handle string types -- we can't rely on force_bytes here because:
313 # - Python attempts str conversion first
314 # - when self._charset != 'utf-8' it re-encodes the content
315 if isinstance(value, (bytes, memoryview)):
316 return bytes(value)
317 if isinstance(value, str):
318 return bytes(value.encode(self.charset))
319 # Handle non-string types.
320 return str(value).encode(self.charset)
321
322 # These methods partially implement the file-like object interface.
323 # See https://docs.python.org/library/io.html#io.IOBase
324
325 # The WSGI server must call this method upon completion of the request.
326 # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
327 def close(self):
328 for closer in self._resource_closers:
329 try:
330 closer()
331 except Exception:
332 pass
333 # Free resources that were still referenced.
334 self._resource_closers.clear()
335 self.closed = True
336 signals.request_finished.send(sender=self._handler_class)
337
338 def write(self, content):
339 raise OSError("This %s instance is not writable" % self.__class__.__name__)
340
341 def flush(self):
342 pass
343
344 def tell(self):
345 raise OSError(
346 "This %s instance cannot tell its position" % self.__class__.__name__
347 )
348
349 # These methods partially implement a stream-like object interface.
350 # See https://docs.python.org/library/io.html#io.IOBase
351
352 def readable(self):
353 return False
354
355 def seekable(self):
356 return False
357
358 def writable(self):
359 return False
360
361 def writelines(self, lines):
362 raise OSError("This %s instance is not writable" % self.__class__.__name__)
363
364
365class HttpResponse(HttpResponseBase):
366 """
367 An HTTP response class with a string as content.
368
369 This content can be read, appended to, or replaced.
370 """
371
372 streaming = False
373
374 def __init__(self, content=b"", *args, **kwargs):
375 super().__init__(*args, **kwargs)
376 # Content is a bytestring. See the `content` property methods.
377 self.content = content
378
379 def __repr__(self):
380 return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
381 "cls": self.__class__.__name__,
382 "status_code": self.status_code,
383 "content_type": self._content_type_for_repr,
384 }
385
386 def serialize(self):
387 """Full HTTP message, including headers, as a bytestring."""
388 return self.serialize_headers() + b"\r\n\r\n" + self.content
389
390 __bytes__ = serialize
391
392 @property
393 def content(self):
394 return b"".join(self._container)
395
396 @content.setter
397 def content(self, value):
398 # Consume iterators upon assignment to allow repeated iteration.
399 if hasattr(value, "__iter__") and not isinstance(
400 value, (bytes, memoryview, str)
401 ):
402 content = b"".join(self.make_bytes(chunk) for chunk in value)
403 if hasattr(value, "close"):
404 try:
405 value.close()
406 except Exception:
407 pass
408 else:
409 content = self.make_bytes(value)
410 # Create a list of properly encoded bytestrings to support write().
411 self._container = [content]
412 self.__dict__.pop("text", None)
413
414 @cached_property
415 def text(self):
416 return self.content.decode(self.charset or "utf-8")
417
418 def __iter__(self):
419 return iter(self._container)
420
421 def write(self, content):
422 self._container.append(self.make_bytes(content))
423
424 def tell(self):
425 return len(self.content)
426
427 def getvalue(self):
428 return self.content
429
430 def writable(self):
431 return True
432
433 def writelines(self, lines):
434 for line in lines:
435 self.write(line)
436
437
438class StreamingHttpResponse(HttpResponseBase):
439 """
440 A streaming HTTP response class with an iterator as content.
441
442 This should only be iterated once, when the response is streamed to the
443 client. However, it can be appended to or replaced with a new iterator
444 that wraps the original content (or yields entirely new content).
445 """
446
447 streaming = True
448
449 def __init__(self, streaming_content=(), *args, **kwargs):
450 super().__init__(*args, **kwargs)
451 # `streaming_content` should be an iterable of bytestrings.
452 # See the `streaming_content` property methods.
453 self.streaming_content = streaming_content
454
455 def __repr__(self):
456 return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
457 "cls": self.__class__.__qualname__,
458 "status_code": self.status_code,
459 "content_type": self._content_type_for_repr,
460 }
461
462 @property
463 def content(self):
464 raise AttributeError(
465 "This %s instance has no `content` attribute. Use "
466 "`streaming_content` instead." % self.__class__.__name__
467 )
468
469 @property
470 def text(self):
471 raise AttributeError(
472 "This %s instance has no `text` attribute." % self.__class__.__name__
473 )
474
475 @property
476 def streaming_content(self):
477 if self.is_async:
478 # pull to lexical scope to capture fixed reference in case
479 # streaming_content is set again later.
480 _iterator = self._iterator
481
482 async def awrapper():
483 async for part in _iterator:
484 yield self.make_bytes(part)
485
486 return awrapper()
487 else:
488 return map(self.make_bytes, self._iterator)
489
490 @streaming_content.setter
491 def streaming_content(self, value):
492 self._set_streaming_content(value)
493
494 def _set_streaming_content(self, value):
495 # Ensure we can never iterate on "value" more than once.
496 try:
497 self._iterator = iter(value)
498 self.is_async = False
499 except TypeError:
500 self._iterator = aiter(value)
501 self.is_async = True
502 if hasattr(value, "close"):
503 self._resource_closers.append(value.close)
504
505 def __iter__(self):
506 try:
507 return iter(self.streaming_content)
508 except TypeError:
509 warnings.warn(
510 "StreamingHttpResponse must consume asynchronous iterators in order to "
511 "serve them synchronously. Use a synchronous iterator instead.",
512 Warning,
513 stacklevel=2,
514 )
515
516 # async iterator. Consume in async_to_sync and map back.
517 async def to_list(_iterator):
518 as_list = []
519 async for chunk in _iterator:
520 as_list.append(chunk)
521 return as_list
522
523 return map(self.make_bytes, iter(async_to_sync(to_list)(self._iterator)))
524
525 async def __aiter__(self):
526 try:
527 async for part in self.streaming_content:
528 yield part
529 except TypeError:
530 warnings.warn(
531 "StreamingHttpResponse must consume synchronous iterators in order to "
532 "serve them asynchronously. Use an asynchronous iterator instead.",
533 Warning,
534 stacklevel=2,
535 )
536 # sync iterator. Consume via sync_to_async and yield via async
537 # generator.
538 for part in await sync_to_async(list)(self.streaming_content):
539 yield part
540
541 def getvalue(self):
542 return b"".join(self.streaming_content)
543
544
545class FileResponse(StreamingHttpResponse):
546 """
547 A streaming HTTP response class optimized for files.
548 """
549
550 block_size = 4096
551
552 def __init__(self, *args, as_attachment=False, filename="", **kwargs):
553 self.as_attachment = as_attachment
554 self.filename = filename
555 self._no_explicit_content_type = (
556 "content_type" not in kwargs or kwargs["content_type"] is None
557 )
558 super().__init__(*args, **kwargs)
559
560 def _set_streaming_content(self, value):
561 if not hasattr(value, "read"):
562 self.file_to_stream = None
563 return super()._set_streaming_content(value)
564
565 self.file_to_stream = filelike = value
566 if hasattr(filelike, "close"):
567 self._resource_closers.append(filelike.close)
568 value = iter(lambda: filelike.read(self.block_size), b"")
569 self.set_headers(filelike)
570 super()._set_streaming_content(value)
571
572 def set_headers(self, filelike):
573 """
574 Set some common response headers (Content-Length, Content-Type, and
575 Content-Disposition) based on the `filelike` response content.
576 """
577 filename = getattr(filelike, "name", "")
578 filename = filename if isinstance(filename, str) else ""
579 seekable = hasattr(filelike, "seek") and (
580 not hasattr(filelike, "seekable") or filelike.seekable()
581 )
582 if hasattr(filelike, "tell"):
583 if seekable:
584 initial_position = filelike.tell()
585 filelike.seek(0, io.SEEK_END)
586 self.headers["Content-Length"] = filelike.tell() - initial_position
587 filelike.seek(initial_position)
588 elif hasattr(filelike, "getbuffer"):
589 self.headers["Content-Length"] = (
590 filelike.getbuffer().nbytes - filelike.tell()
591 )
592 elif os.path.exists(filename):
593 self.headers["Content-Length"] = (
594 os.path.getsize(filename) - filelike.tell()
595 )
596 elif seekable:
597 self.headers["Content-Length"] = sum(
598 iter(lambda: len(filelike.read(self.block_size)), 0)
599 )
600 filelike.seek(-int(self.headers["Content-Length"]), io.SEEK_END)
601
602 filename = os.path.basename(self.filename or filename)
603 if self._no_explicit_content_type:
604 if filename:
605 content_type, encoding = mimetypes.guess_type(filename)
606 # Encoding isn't set to prevent browsers from automatically
607 # uncompressing files.
608 content_type = {
609 "br": "application/x-brotli",
610 "bzip2": "application/x-bzip",
611 "compress": "application/x-compress",
612 "gzip": "application/gzip",
613 "xz": "application/x-xz",
614 }.get(encoding, content_type)
615 self.headers["Content-Type"] = (
616 content_type or "application/octet-stream"
617 )
618 else:
619 self.headers["Content-Type"] = "application/octet-stream"
620
621 if content_disposition := content_disposition_header(
622 self.as_attachment, filename
623 ):
624 self.headers["Content-Disposition"] = content_disposition
625
626
627class HttpResponseRedirectBase(HttpResponse):
628 allowed_schemes = ["http", "https", "ftp"]
629
630 def __init__(self, redirect_to, preserve_request=False, *args, **kwargs):
631 super().__init__(*args, **kwargs)
632 self["Location"] = iri_to_uri(redirect_to)
633 parsed = urlsplit(str(redirect_to))
634 if preserve_request:
635 self.status_code = self.status_code_preserve_request
636 if parsed.scheme and parsed.scheme not in self.allowed_schemes:
637 raise DisallowedRedirect(
638 "Unsafe redirect to URL with protocol '%s'" % parsed.scheme
639 )
640
641 url = property(lambda self: self["Location"])
642
643 def __repr__(self):
644 return (
645 '<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">'
646 % {
647 "cls": self.__class__.__name__,
648 "status_code": self.status_code,
649 "content_type": self._content_type_for_repr,
650 "url": self.url,
651 }
652 )
653
654
655class HttpResponseRedirect(HttpResponseRedirectBase):
656 status_code = 302
657 status_code_preserve_request = 307
658
659
660class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
661 status_code = 301
662 status_code_preserve_request = 308
663
664
665class HttpResponseNotModified(HttpResponse):
666 status_code = 304
667
668 def __init__(self, *args, **kwargs):
669 super().__init__(*args, **kwargs)
670 del self["content-type"]
671
672 @HttpResponse.content.setter
673 def content(self, value):
674 if value:
675 raise AttributeError(
676 "You cannot set content to a 304 (Not Modified) response"
677 )
678 self._container = []
679
680
681class HttpResponseBadRequest(HttpResponse):
682 status_code = 400
683
684
685class HttpResponseNotFound(HttpResponse):
686 status_code = 404
687
688
689class HttpResponseForbidden(HttpResponse):
690 status_code = 403
691
692
693class HttpResponseNotAllowed(HttpResponse):
694 status_code = 405
695
696 def __init__(self, permitted_methods, *args, **kwargs):
697 super().__init__(*args, **kwargs)
698 self["Allow"] = ", ".join(permitted_methods)
699
700 def __repr__(self):
701 return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % {
702 "cls": self.__class__.__name__,
703 "status_code": self.status_code,
704 "content_type": self._content_type_for_repr,
705 "methods": self["Allow"],
706 }
707
708
709class HttpResponseGone(HttpResponse):
710 status_code = 410
711
712
713class HttpResponseServerError(HttpResponse):
714 status_code = 500
715
716
717class Http404(Exception):
718 pass
719
720
721class JsonResponse(HttpResponse):
722 """
723 An HTTP response class that consumes data to be serialized to JSON.
724
725 :param data: Data to be dumped into json. By default only ``dict`` objects
726 are allowed to be passed due to a security flaw before ECMAScript 5. See
727 the ``safe`` parameter for more information.
728 :param encoder: Should be a json encoder class. Defaults to
729 ``django.core.serializers.json.DjangoJSONEncoder``.
730 :param safe: Controls if only ``dict`` objects may be serialized. Defaults
731 to ``True``.
732 :param json_dumps_params: A dictionary of kwargs passed to json.dumps().
733 """
734
735 def __init__(
736 self,
737 data,
738 encoder=DjangoJSONEncoder,
739 safe=True,
740 json_dumps_params=None,
741 **kwargs,
742 ):
743 if safe and not isinstance(data, dict):
744 raise TypeError(
745 "In order to allow non-dict objects to be serialized set the "
746 "safe parameter to False."
747 )
748 if json_dumps_params is None:
749 json_dumps_params = {}
750 kwargs.setdefault("content_type", "application/json")
751 data = json.dumps(data, cls=encoder, **json_dumps_params)
752 super().__init__(content=data, **kwargs)