python (3.11.7)
       1  # SPDX-FileCopyrightText: 2015 Eric Larson
       2  #
       3  # SPDX-License-Identifier: Apache-2.0
       4  
       5  import calendar
       6  import time
       7  
       8  from email.utils import formatdate, parsedate, parsedate_tz
       9  
      10  from datetime import datetime, timedelta
      11  
      12  TIME_FMT = "%a, %d %b %Y %H:%M:%S GMT"
      13  
      14  
      15  def expire_after(delta, date=None):
      16      date = date or datetime.utcnow()
      17      return date + delta
      18  
      19  
      20  def datetime_to_header(dt):
      21      return formatdate(calendar.timegm(dt.timetuple()))
      22  
      23  
      24  class ESC[4;38;5;81mBaseHeuristic(ESC[4;38;5;149mobject):
      25  
      26      def warning(self, response):
      27          """
      28          Return a valid 1xx warning header value describing the cache
      29          adjustments.
      30  
      31          The response is provided too allow warnings like 113
      32          http://tools.ietf.org/html/rfc7234#section-5.5.4 where we need
      33          to explicitly say response is over 24 hours old.
      34          """
      35          return '110 - "Response is Stale"'
      36  
      37      def update_headers(self, response):
      38          """Update the response headers with any new headers.
      39  
      40          NOTE: This SHOULD always include some Warning header to
      41                signify that the response was cached by the client, not
      42                by way of the provided headers.
      43          """
      44          return {}
      45  
      46      def apply(self, response):
      47          updated_headers = self.update_headers(response)
      48  
      49          if updated_headers:
      50              response.headers.update(updated_headers)
      51              warning_header_value = self.warning(response)
      52              if warning_header_value is not None:
      53                  response.headers.update({"Warning": warning_header_value})
      54  
      55          return response
      56  
      57  
      58  class ESC[4;38;5;81mOneDayCache(ESC[4;38;5;149mBaseHeuristic):
      59      """
      60      Cache the response by providing an expires 1 day in the
      61      future.
      62      """
      63  
      64      def update_headers(self, response):
      65          headers = {}
      66  
      67          if "expires" not in response.headers:
      68              date = parsedate(response.headers["date"])
      69              expires = expire_after(timedelta(days=1), date=datetime(*date[:6]))
      70              headers["expires"] = datetime_to_header(expires)
      71              headers["cache-control"] = "public"
      72          return headers
      73  
      74  
      75  class ESC[4;38;5;81mExpiresAfter(ESC[4;38;5;149mBaseHeuristic):
      76      """
      77      Cache **all** requests for a defined time period.
      78      """
      79  
      80      def __init__(self, **kw):
      81          self.delta = timedelta(**kw)
      82  
      83      def update_headers(self, response):
      84          expires = expire_after(self.delta)
      85          return {"expires": datetime_to_header(expires), "cache-control": "public"}
      86  
      87      def warning(self, response):
      88          tmpl = "110 - Automatically cached for %s. Response might be stale"
      89          return tmpl % self.delta
      90  
      91  
      92  class ESC[4;38;5;81mLastModified(ESC[4;38;5;149mBaseHeuristic):
      93      """
      94      If there is no Expires header already, fall back on Last-Modified
      95      using the heuristic from
      96      http://tools.ietf.org/html/rfc7234#section-4.2.2
      97      to calculate a reasonable value.
      98  
      99      Firefox also does something like this per
     100      https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching_FAQ
     101      http://lxr.mozilla.org/mozilla-release/source/netwerk/protocol/http/nsHttpResponseHead.cpp#397
     102      Unlike mozilla we limit this to 24-hr.
     103      """
     104      cacheable_by_default_statuses = {
     105          200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501
     106      }
     107  
     108      def update_headers(self, resp):
     109          headers = resp.headers
     110  
     111          if "expires" in headers:
     112              return {}
     113  
     114          if "cache-control" in headers and headers["cache-control"] != "public":
     115              return {}
     116  
     117          if resp.status not in self.cacheable_by_default_statuses:
     118              return {}
     119  
     120          if "date" not in headers or "last-modified" not in headers:
     121              return {}
     122  
     123          date = calendar.timegm(parsedate_tz(headers["date"]))
     124          last_modified = parsedate(headers["last-modified"])
     125          if date is None or last_modified is None:
     126              return {}
     127  
     128          now = time.time()
     129          current_age = max(0, now - date)
     130          delta = date - calendar.timegm(last_modified)
     131          freshness_lifetime = max(0, min(delta / 10, 24 * 3600))
     132          if freshness_lifetime <= current_age:
     133              return {}
     134  
     135          expires = date + freshness_lifetime
     136          return {"expires": time.strftime(TIME_FMT, time.gmtime(expires))}
     137  
     138      def warning(self, resp):
     139          return None