summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorKevin Christopher Henry <k@severian.com>2016-09-01 09:32:20 -0400
committerTim Graham <timograham@gmail.com>2016-09-10 08:14:52 -0400
commit4ef0e019b7dd3d2bf93b5c705b3b7df9cdb77561 (patch)
tree3b87b0f6df6ca459833f0569df3c2f6320ff4d67 /django
parente7abb5ba8608f90ce97c6edb031ae877195616f5 (diff)
Fixed #27083 -- Added support for weak ETags.
Diffstat (limited to 'django')
-rw-r--r--django/middleware/common.py3
-rw-r--r--django/middleware/http.py4
-rw-r--r--django/utils/http.py45
-rw-r--r--django/views/decorators/http.py20
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