http.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. from __future__ import annotations
  2. import re
  3. import typing as t
  4. from datetime import datetime
  5. from .._internal import _dt_as_utc
  6. from ..http import generate_etag
  7. from ..http import parse_date
  8. from ..http import parse_etags
  9. from ..http import parse_if_range_header
  10. from ..http import unquote_etag
  11. _etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
  12. def is_resource_modified(
  13. http_range: str | None = None,
  14. http_if_range: str | None = None,
  15. http_if_modified_since: str | None = None,
  16. http_if_none_match: str | None = None,
  17. http_if_match: str | None = None,
  18. etag: str | None = None,
  19. data: bytes | None = None,
  20. last_modified: datetime | str | None = None,
  21. ignore_if_range: bool = True,
  22. ) -> bool:
  23. """Convenience method for conditional requests.
  24. :param http_range: Range HTTP header
  25. :param http_if_range: If-Range HTTP header
  26. :param http_if_modified_since: If-Modified-Since HTTP header
  27. :param http_if_none_match: If-None-Match HTTP header
  28. :param http_if_match: If-Match HTTP header
  29. :param etag: the etag for the response for comparison.
  30. :param data: or alternatively the data of the response to automatically
  31. generate an etag using :func:`generate_etag`.
  32. :param last_modified: an optional date of the last modification.
  33. :param ignore_if_range: If `False`, `If-Range` header will be taken into
  34. account.
  35. :return: `True` if the resource was modified, otherwise `False`.
  36. .. versionadded:: 2.2
  37. """
  38. if etag is None and data is not None:
  39. etag = generate_etag(data)
  40. elif data is not None:
  41. raise TypeError("both data and etag given")
  42. unmodified = False
  43. if isinstance(last_modified, str):
  44. last_modified = parse_date(last_modified)
  45. # HTTP doesn't use microsecond, remove it to avoid false positive
  46. # comparisons. Mark naive datetimes as UTC.
  47. if last_modified is not None:
  48. last_modified = _dt_as_utc(last_modified.replace(microsecond=0))
  49. if_range = None
  50. if not ignore_if_range and http_range is not None:
  51. # https://tools.ietf.org/html/rfc7233#section-3.2
  52. # A server MUST ignore an If-Range header field received in a request
  53. # that does not contain a Range header field.
  54. if_range = parse_if_range_header(http_if_range)
  55. if if_range is not None and if_range.date is not None:
  56. modified_since: datetime | None = if_range.date
  57. else:
  58. modified_since = parse_date(http_if_modified_since)
  59. if modified_since and last_modified and last_modified <= modified_since:
  60. unmodified = True
  61. if etag:
  62. etag, _ = unquote_etag(etag)
  63. etag = t.cast(str, etag)
  64. if if_range is not None and if_range.etag is not None:
  65. unmodified = parse_etags(if_range.etag).contains(etag)
  66. else:
  67. if_none_match = parse_etags(http_if_none_match)
  68. if if_none_match:
  69. # https://tools.ietf.org/html/rfc7232#section-3.2
  70. # "A recipient MUST use the weak comparison function when comparing
  71. # entity-tags for If-None-Match"
  72. unmodified = if_none_match.contains_weak(etag)
  73. # https://tools.ietf.org/html/rfc7232#section-3.1
  74. # "Origin server MUST use the strong comparison function when
  75. # comparing entity-tags for If-Match"
  76. if_match = parse_etags(http_if_match)
  77. if if_match:
  78. unmodified = not if_match.is_strong(etag)
  79. return not unmodified
  80. _cookie_re = re.compile(
  81. r"""
  82. ([^=;]*)
  83. (?:\s*=\s*
  84. (
  85. "(?:[^\\"]|\\.)*"
  86. |
  87. .*?
  88. )
  89. )?
  90. \s*;\s*
  91. """,
  92. flags=re.ASCII | re.VERBOSE,
  93. )
  94. _cookie_unslash_re = re.compile(rb"\\([0-3][0-7]{2}|.)")
  95. def _cookie_unslash_replace(m: t.Match[bytes]) -> bytes:
  96. v = m.group(1)
  97. if len(v) == 1:
  98. return v
  99. return int(v, 8).to_bytes(1, "big")
  100. def parse_cookie(
  101. cookie: str | None = None,
  102. cls: type[ds.MultiDict] | None = None,
  103. ) -> ds.MultiDict[str, str]:
  104. """Parse a cookie from a string.
  105. The same key can be provided multiple times, the values are stored
  106. in-order. The default :class:`MultiDict` will have the first value
  107. first, and all values can be retrieved with
  108. :meth:`MultiDict.getlist`.
  109. :param cookie: The cookie header as a string.
  110. :param cls: A dict-like class to store the parsed cookies in.
  111. Defaults to :class:`MultiDict`.
  112. .. versionchanged:: 3.0
  113. Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
  114. .. versionadded:: 2.2
  115. """
  116. if cls is None:
  117. cls = ds.MultiDict
  118. if not cookie:
  119. return cls()
  120. cookie = f"{cookie};"
  121. out = []
  122. for ck, cv in _cookie_re.findall(cookie):
  123. ck = ck.strip()
  124. cv = cv.strip()
  125. if not ck:
  126. continue
  127. if len(cv) >= 2 and cv[0] == cv[-1] == '"':
  128. # Work with bytes here, since a UTF-8 character could be multiple bytes.
  129. cv = _cookie_unslash_re.sub(
  130. _cookie_unslash_replace, cv[1:-1].encode()
  131. ).decode(errors="replace")
  132. out.append((ck, cv))
  133. return cls(out)
  134. # circular dependencies
  135. from .. import datastructures as ds