diff options
| author | Kevin Christopher Henry <k@severian.com> | 2016-09-01 09:32:20 -0400 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2016-09-10 08:14:52 -0400 |
| commit | 4ef0e019b7dd3d2bf93b5c705b3b7df9cdb77561 (patch) | |
| tree | 3b87b0f6df6ca459833f0569df3c2f6320ff4d67 /django | |
| parent | e7abb5ba8608f90ce97c6edb031ae877195616f5 (diff) | |
Fixed #27083 -- Added support for weak ETags.
Diffstat (limited to 'django')
| -rw-r--r-- | django/middleware/common.py | 3 | ||||
| -rw-r--r-- | django/middleware/http.py | 4 | ||||
| -rw-r--r-- | django/utils/http.py | 45 | ||||
| -rw-r--r-- | django/views/decorators/http.py | 20 |
4 files changed, 39 insertions, 33 deletions
diff --git a/django/middleware/common.py b/django/middleware/common.py index 3d55baebde..dc89bcb9fa 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -10,7 +10,6 @@ from django.utils.cache import ( ) from django.utils.deprecation import MiddlewareMixin from django.utils.encoding import force_text -from django.utils.http import unquote_etag from django.utils.six.moves.urllib.parse import urlparse @@ -122,7 +121,7 @@ class CommonMiddleware(MiddlewareMixin): if response.has_header('ETag'): return get_conditional_response( request, - etag=unquote_etag(response['ETag']), + etag=response['ETag'], response=response, ) # Add the Content-Length header to non-streaming responses if not diff --git a/django/middleware/http.py b/django/middleware/http.py index a3030463c4..83c0f866e4 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -1,6 +1,6 @@ from django.utils.cache import get_conditional_response from django.utils.deprecation import MiddlewareMixin -from django.utils.http import http_date, parse_http_date_safe, unquote_etag +from django.utils.http import http_date, parse_http_date_safe class ConditionalGetMiddleware(MiddlewareMixin): @@ -24,7 +24,7 @@ class ConditionalGetMiddleware(MiddlewareMixin): if etag or last_modified: return get_conditional_response( request, - etag=unquote_etag(etag), + etag=etag, last_modified=last_modified, response=response, ) diff --git a/django/utils/http.py b/django/utils/http.py index c6285e6f40..cba2a08095 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -21,7 +21,15 @@ from django.utils.six.moves.urllib.parse import ( urlparse, ) -ETAG_MATCH = re.compile(r'(?:W/)?"((?:\\.|[^"])*)"') +# based on RFC 7232, Appendix C +ETAG_MATCH = re.compile(r''' + \A( # start of string and capture group + (?:W/)? # optional weak indicator + " # opening quote + [^"]* # any sequence of non-quote characters + " # end quote + )\Z # end of string and capture group +''', re.X) MONTHS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split() __D = r'(?P<day>\d{2})' @@ -234,30 +242,27 @@ def urlsafe_base64_decode(s): def parse_etags(etag_str): """ - Parses a string with one or several etags passed in If-None-Match and - If-Match headers by the rules in RFC 2616. Returns a list of etags - without surrounding double quotes (") and unescaped from \<CHAR>. + Parse a string of ETags given in an If-None-Match or If-Match header as + defined by RFC 7232. Return a list of quoted ETags, or ['*'] if all ETags + should be matched. """ - etags = ETAG_MATCH.findall(etag_str) - if not etags: - # etag_str has wrong format, treat it as an opaque string then - return [etag_str] - etags = [e.encode('ascii').decode('unicode_escape') for e in etags] - return etags - - -def quote_etag(etag): - """ - Wraps a string in double quotes escaping contents as necessary. - """ - return '"%s"' % etag.replace('\\', '\\\\').replace('"', '\\"') + if etag_str.strip() == '*': + return ['*'] + else: + # Parse each ETag individually, and return any that are valid. + etag_matches = (ETAG_MATCH.match(etag.strip()) for etag in etag_str.split(',')) + return [match.group(1) for match in etag_matches if match] -def unquote_etag(etag): +def quote_etag(etag_str): """ - Unquote an ETag string; i.e. revert quote_etag(). + If the provided string is already a quoted ETag, return it. Otherwise, wrap + the string in quotes, making it a strong ETag. """ - return etag.strip('"').replace('\\"', '"').replace('\\\\', '\\') if etag else etag + if ETAG_MATCH.match(etag_str): + return etag_str + else: + return '"%s"' % etag_str def is_same_domain(host, pattern): diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index a5144421d5..846d3921cf 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -62,16 +62,16 @@ def condition(etag_func=None, last_modified_func=None): None if the resource doesn't exist), while the last_modified function should return a datetime object (or None if the resource doesn't exist). - If both parameters are provided, all the preconditions must be met before - the view is processed. + The ETag function should return a complete ETag, including quotes (e.g. + '"etag"'), since that's the only way to distinguish between weak and strong + ETags. If an unquoted ETag is returned (e.g. 'etag'), it will be converted + to a strong ETag by adding quotes. This decorator will either pass control to the wrapped view function or - return an HTTP 304 response (unmodified) or 412 response (preconditions - failed), depending upon the request method. - - Any behavior marked as "undefined" in the HTTP spec (e.g. If-none-match - plus If-modified-since headers) will result in the view function being - called. + return an HTTP 304 response (unmodified) or 412 response (precondition + failed), depending upon the request method. In either case, it will add the + generated ETag and Last-Modified headers to the response if it doesn't + already have them. """ def decorator(func): @wraps(func, assigned=available_attrs(func)) @@ -83,7 +83,9 @@ def condition(etag_func=None, last_modified_func=None): if dt: return timegm(dt.utctimetuple()) + # The value from etag_func() could be quoted or unquoted. res_etag = etag_func(request, *args, **kwargs) if etag_func else None + res_etag = quote_etag(res_etag) if res_etag is not None else None res_last_modified = get_last_modified() response = get_conditional_response( @@ -99,7 +101,7 @@ def condition(etag_func=None, last_modified_func=None): if res_last_modified and not response.has_header('Last-Modified'): response['Last-Modified'] = http_date(res_last_modified) if res_etag and not response.has_header('ETag'): - response['ETag'] = quote_etag(res_etag) + response['ETag'] = res_etag return response |
