You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

controller.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. """
  2. The httplib2 algorithms ported for use with requests.
  3. """
  4. import logging
  5. import re
  6. import calendar
  7. import time
  8. from email.utils import parsedate_tz
  9. from pip._vendor.requests.structures import CaseInsensitiveDict
  10. from .cache import DictCache
  11. from .serialize import Serializer
  12. logger = logging.getLogger(__name__)
  13. URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
  14. def parse_uri(uri):
  15. """Parses a URI using the regex given in Appendix B of RFC 3986.
  16. (scheme, authority, path, query, fragment) = parse_uri(uri)
  17. """
  18. groups = URI.match(uri).groups()
  19. return (groups[1], groups[3], groups[4], groups[6], groups[8])
  20. class CacheController(object):
  21. """An interface to see if request should cached or not.
  22. """
  23. def __init__(self, cache=None, cache_etags=True, serializer=None,
  24. status_codes=None):
  25. self.cache = cache or DictCache()
  26. self.cache_etags = cache_etags
  27. self.serializer = serializer or Serializer()
  28. self.cacheable_status_codes = status_codes or (200, 203, 300, 301)
  29. @classmethod
  30. def _urlnorm(cls, uri):
  31. """Normalize the URL to create a safe key for the cache"""
  32. (scheme, authority, path, query, fragment) = parse_uri(uri)
  33. if not scheme or not authority:
  34. raise Exception("Only absolute URIs are allowed. uri = %s" % uri)
  35. scheme = scheme.lower()
  36. authority = authority.lower()
  37. if not path:
  38. path = "/"
  39. # Could do syntax based normalization of the URI before
  40. # computing the digest. See Section 6.2.2 of Std 66.
  41. request_uri = query and "?".join([path, query]) or path
  42. defrag_uri = scheme + "://" + authority + request_uri
  43. return defrag_uri
  44. @classmethod
  45. def cache_url(cls, uri):
  46. return cls._urlnorm(uri)
  47. def parse_cache_control(self, headers):
  48. known_directives = {
  49. # https://tools.ietf.org/html/rfc7234#section-5.2
  50. 'max-age': (int, True,),
  51. 'max-stale': (int, False,),
  52. 'min-fresh': (int, True,),
  53. 'no-cache': (None, False,),
  54. 'no-store': (None, False,),
  55. 'no-transform': (None, False,),
  56. 'only-if-cached' : (None, False,),
  57. 'must-revalidate': (None, False,),
  58. 'public': (None, False,),
  59. 'private': (None, False,),
  60. 'proxy-revalidate': (None, False,),
  61. 's-maxage': (int, True,)
  62. }
  63. cc_headers = headers.get('cache-control',
  64. headers.get('Cache-Control', ''))
  65. retval = {}
  66. for cc_directive in cc_headers.split(','):
  67. parts = cc_directive.split('=', 1)
  68. directive = parts[0].strip()
  69. try:
  70. typ, required = known_directives[directive]
  71. except KeyError:
  72. logger.debug('Ignoring unknown cache-control directive: %s',
  73. directive)
  74. continue
  75. if not typ or not required:
  76. retval[directive] = None
  77. if typ:
  78. try:
  79. retval[directive] = typ(parts[1].strip())
  80. except IndexError:
  81. if required:
  82. logger.debug('Missing value for cache-control '
  83. 'directive: %s', directive)
  84. except ValueError:
  85. logger.debug('Invalid value for cache-control directive '
  86. '%s, must be %s', directive, typ.__name__)
  87. return retval
  88. def cached_request(self, request):
  89. """
  90. Return a cached response if it exists in the cache, otherwise
  91. return False.
  92. """
  93. cache_url = self.cache_url(request.url)
  94. logger.debug('Looking up "%s" in the cache', cache_url)
  95. cc = self.parse_cache_control(request.headers)
  96. # Bail out if the request insists on fresh data
  97. if 'no-cache' in cc:
  98. logger.debug('Request header has "no-cache", cache bypassed')
  99. return False
  100. if 'max-age' in cc and cc['max-age'] == 0:
  101. logger.debug('Request header has "max_age" as 0, cache bypassed')
  102. return False
  103. # Request allows serving from the cache, let's see if we find something
  104. cache_data = self.cache.get(cache_url)
  105. if cache_data is None:
  106. logger.debug('No cache entry available')
  107. return False
  108. # Check whether it can be deserialized
  109. resp = self.serializer.loads(request, cache_data)
  110. if not resp:
  111. logger.warning('Cache entry deserialization failed, entry ignored')
  112. return False
  113. # If we have a cached 301, return it immediately. We don't
  114. # need to test our response for other headers b/c it is
  115. # intrinsically "cacheable" as it is Permanent.
  116. # See:
  117. # https://tools.ietf.org/html/rfc7231#section-6.4.2
  118. #
  119. # Client can try to refresh the value by repeating the request
  120. # with cache busting headers as usual (ie no-cache).
  121. if resp.status == 301:
  122. msg = ('Returning cached "301 Moved Permanently" response '
  123. '(ignoring date and etag information)')
  124. logger.debug(msg)
  125. return resp
  126. headers = CaseInsensitiveDict(resp.headers)
  127. if not headers or 'date' not in headers:
  128. if 'etag' not in headers:
  129. # Without date or etag, the cached response can never be used
  130. # and should be deleted.
  131. logger.debug('Purging cached response: no date or etag')
  132. self.cache.delete(cache_url)
  133. logger.debug('Ignoring cached response: no date')
  134. return False
  135. now = time.time()
  136. date = calendar.timegm(
  137. parsedate_tz(headers['date'])
  138. )
  139. current_age = max(0, now - date)
  140. logger.debug('Current age based on date: %i', current_age)
  141. # TODO: There is an assumption that the result will be a
  142. # urllib3 response object. This may not be best since we
  143. # could probably avoid instantiating or constructing the
  144. # response until we know we need it.
  145. resp_cc = self.parse_cache_control(headers)
  146. # determine freshness
  147. freshness_lifetime = 0
  148. # Check the max-age pragma in the cache control header
  149. if 'max-age' in resp_cc:
  150. freshness_lifetime = resp_cc['max-age']
  151. logger.debug('Freshness lifetime from max-age: %i',
  152. freshness_lifetime)
  153. # If there isn't a max-age, check for an expires header
  154. elif 'expires' in headers:
  155. expires = parsedate_tz(headers['expires'])
  156. if expires is not None:
  157. expire_time = calendar.timegm(expires) - date
  158. freshness_lifetime = max(0, expire_time)
  159. logger.debug("Freshness lifetime from expires: %i",
  160. freshness_lifetime)
  161. # Determine if we are setting freshness limit in the
  162. # request. Note, this overrides what was in the response.
  163. if 'max-age' in cc:
  164. freshness_lifetime = cc['max-age']
  165. logger.debug('Freshness lifetime from request max-age: %i',
  166. freshness_lifetime)
  167. if 'min-fresh' in cc:
  168. min_fresh = cc['min-fresh']
  169. # adjust our current age by our min fresh
  170. current_age += min_fresh
  171. logger.debug('Adjusted current age from min-fresh: %i',
  172. current_age)
  173. # Return entry if it is fresh enough
  174. if freshness_lifetime > current_age:
  175. logger.debug('The response is "fresh", returning cached response')
  176. logger.debug('%i > %i', freshness_lifetime, current_age)
  177. return resp
  178. # we're not fresh. If we don't have an Etag, clear it out
  179. if 'etag' not in headers:
  180. logger.debug(
  181. 'The cached response is "stale" with no etag, purging'
  182. )
  183. self.cache.delete(cache_url)
  184. # return the original handler
  185. return False
  186. def conditional_headers(self, request):
  187. cache_url = self.cache_url(request.url)
  188. resp = self.serializer.loads(request, self.cache.get(cache_url))
  189. new_headers = {}
  190. if resp:
  191. headers = CaseInsensitiveDict(resp.headers)
  192. if 'etag' in headers:
  193. new_headers['If-None-Match'] = headers['ETag']
  194. if 'last-modified' in headers:
  195. new_headers['If-Modified-Since'] = headers['Last-Modified']
  196. return new_headers
  197. def cache_response(self, request, response, body=None,
  198. status_codes=None):
  199. """
  200. Algorithm for caching requests.
  201. This assumes a requests Response object.
  202. """
  203. # From httplib2: Don't cache 206's since we aren't going to
  204. # handle byte range requests
  205. cacheable_status_codes = status_codes or self.cacheable_status_codes
  206. if response.status not in cacheable_status_codes:
  207. logger.debug(
  208. 'Status code %s not in %s',
  209. response.status,
  210. cacheable_status_codes
  211. )
  212. return
  213. response_headers = CaseInsensitiveDict(response.headers)
  214. # If we've been given a body, our response has a Content-Length, that
  215. # Content-Length is valid then we can check to see if the body we've
  216. # been given matches the expected size, and if it doesn't we'll just
  217. # skip trying to cache it.
  218. if (body is not None and
  219. "content-length" in response_headers and
  220. response_headers["content-length"].isdigit() and
  221. int(response_headers["content-length"]) != len(body)):
  222. return
  223. cc_req = self.parse_cache_control(request.headers)
  224. cc = self.parse_cache_control(response_headers)
  225. cache_url = self.cache_url(request.url)
  226. logger.debug('Updating cache with response from "%s"', cache_url)
  227. # Delete it from the cache if we happen to have it stored there
  228. no_store = False
  229. if 'no-store' in cc:
  230. no_store = True
  231. logger.debug('Response header has "no-store"')
  232. if 'no-store' in cc_req:
  233. no_store = True
  234. logger.debug('Request header has "no-store"')
  235. if no_store and self.cache.get(cache_url):
  236. logger.debug('Purging existing cache entry to honor "no-store"')
  237. self.cache.delete(cache_url)
  238. # If we've been given an etag, then keep the response
  239. if self.cache_etags and 'etag' in response_headers:
  240. logger.debug('Caching due to etag')
  241. self.cache.set(
  242. cache_url,
  243. self.serializer.dumps(request, response, body=body),
  244. )
  245. # Add to the cache any 301s. We do this before looking that
  246. # the Date headers.
  247. elif response.status == 301:
  248. logger.debug('Caching permanant redirect')
  249. self.cache.set(
  250. cache_url,
  251. self.serializer.dumps(request, response)
  252. )
  253. # Add to the cache if the response headers demand it. If there
  254. # is no date header then we can't do anything about expiring
  255. # the cache.
  256. elif 'date' in response_headers:
  257. # cache when there is a max-age > 0
  258. if 'max-age' in cc and cc['max-age'] > 0:
  259. logger.debug('Caching b/c date exists and max-age > 0')
  260. self.cache.set(
  261. cache_url,
  262. self.serializer.dumps(request, response, body=body),
  263. )
  264. # If the request can expire, it means we should cache it
  265. # in the meantime.
  266. elif 'expires' in response_headers:
  267. if response_headers['expires']:
  268. logger.debug('Caching b/c of expires header')
  269. self.cache.set(
  270. cache_url,
  271. self.serializer.dumps(request, response, body=body),
  272. )
  273. def update_cached_response(self, request, response):
  274. """On a 304 we will get a new set of headers that we want to
  275. update our cached value with, assuming we have one.
  276. This should only ever be called when we've sent an ETag and
  277. gotten a 304 as the response.
  278. """
  279. cache_url = self.cache_url(request.url)
  280. cached_response = self.serializer.loads(
  281. request,
  282. self.cache.get(cache_url)
  283. )
  284. if not cached_response:
  285. # we didn't have a cached response
  286. return response
  287. # Lets update our headers with the headers from the new request:
  288. # http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-26#section-4.1
  289. #
  290. # The server isn't supposed to send headers that would make
  291. # the cached body invalid. But... just in case, we'll be sure
  292. # to strip out ones we know that might be problmatic due to
  293. # typical assumptions.
  294. excluded_headers = [
  295. "content-length",
  296. ]
  297. cached_response.headers.update(
  298. dict((k, v) for k, v in response.headers.items()
  299. if k.lower() not in excluded_headers)
  300. )
  301. # we want a 200 b/c we have content via the cache
  302. cached_response.status = 200
  303. # update our cache
  304. self.cache.set(
  305. cache_url,
  306. self.serializer.dumps(request, cached_response),
  307. )
  308. return cached_response