Fixed #24935 -- Refactored common conditional GET handling.

This commit is contained in:
Denis Cornehl 2015-06-05 14:26:48 +01:00 committed by Tim Graham
parent 1f7b25c1a7
commit 7a40fef17a
4 changed files with 130 additions and 109 deletions

View File

@ -1,4 +1,3 @@
import hashlib
import logging import logging
import re import re
@ -7,6 +6,7 @@ from django.conf import settings
from django.core import urlresolvers from django.core import urlresolvers
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.mail import mail_managers from django.core.mail import mail_managers
from django.utils.cache import get_conditional_response, set_response_etag
from django.utils.encoding import force_text from django.utils.encoding import force_text
logger = logging.getLogger('django.request') logger = logging.getLogger('django.request')
@ -113,20 +113,15 @@ class CommonMiddleware(object):
return self.response_redirect_class(self.get_full_path_with_slash(request)) return self.response_redirect_class(self.get_full_path_with_slash(request))
if settings.USE_ETAGS: if settings.USE_ETAGS:
if not response.has_header('ETag'):
set_response_etag(response)
if response.has_header('ETag'): if response.has_header('ETag'):
etag = response['ETag'] return get_conditional_response(
elif response.streaming: request,
etag = None etag=response['ETag'],
else: response=response,
etag = '"%s"' % hashlib.md5(response.content).hexdigest() )
if etag is not None:
if (200 <= response.status_code < 300
and request.META.get('HTTP_IF_NONE_MATCH') == etag):
cookies = response.cookies
response = http.HttpResponseNotModified()
response.cookies = cookies
else:
response['ETag'] = etag
return response return response

View File

@ -1,3 +1,4 @@
from django.utils.cache import get_conditional_response
from django.utils.http import http_date, parse_http_date_safe from django.utils.http import http_date, parse_http_date_safe
@ -14,28 +15,17 @@ class ConditionalGetMiddleware(object):
if not response.streaming and not response.has_header('Content-Length'): if not response.streaming and not response.has_header('Content-Length'):
response['Content-Length'] = str(len(response.content)) response['Content-Length'] = str(len(response.content))
# If-None-Match must be ignored if original result would be anything etag = response.get('ETag')
# other than a 2XX or 304 status. 304 status would result in no change. last_modified = response.get('Last-Modified')
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 if last_modified:
if 200 <= response.status_code < 300 and response.has_header('ETag'): last_modified = parse_http_date_safe(last_modified)
if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
if if_none_match == response['ETag']:
# Setting the status is enough here. The response handling path
# automatically removes content for this status code (in
# http.conditional_content_removal()).
response.status_code = 304
# If-Modified-Since must be ignored if the original result was not a 200. if etag or last_modified:
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25 return get_conditional_response(
if response.status_code == 200 and response.has_header('Last-Modified'): request,
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE') etag=etag,
if if_modified_since is not None: last_modified=last_modified,
if_modified_since = parse_http_date_safe(if_modified_since) response=response,
if if_modified_since is not None: )
last_modified = parse_http_date_safe(response['Last-Modified'])
if last_modified is not None and last_modified <= if_modified_since:
# Setting the status code is enough here (same reasons as
# above).
response.status_code = 304
return response return response

View File

@ -19,18 +19,24 @@ An example: i18n middleware would need to distinguish caches by the
from __future__ import unicode_literals from __future__ import unicode_literals
import hashlib import hashlib
import logging
import re import re
import time import time
from django.conf import settings from django.conf import settings
from django.core.cache import caches from django.core.cache import caches
from django.http import HttpResponse, HttpResponseNotModified
from django.utils.encoding import force_bytes, force_text, iri_to_uri from django.utils.encoding import force_bytes, force_text, iri_to_uri
from django.utils.http import http_date from django.utils.http import (
http_date, parse_etags, parse_http_date_safe, quote_etag,
)
from django.utils.timezone import get_current_timezone_name from django.utils.timezone import get_current_timezone_name
from django.utils.translation import get_language from django.utils.translation import get_language
cc_delim_re = re.compile(r'\s*,\s*') cc_delim_re = re.compile(r'\s*,\s*')
logger = logging.getLogger('django.request')
def patch_cache_control(response, **kwargs): def patch_cache_control(response, **kwargs):
""" """
@ -97,9 +103,99 @@ def get_max_age(response):
pass pass
def _set_response_etag(response): def set_response_etag(response):
if not response.streaming: if not response.streaming:
response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest() response['ETag'] = quote_etag(hashlib.md5(response.content).hexdigest())
return response
def _precondition_failed(request):
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': 412,
'request': request,
},
)
return HttpResponse(status=412)
def _not_modified(request, response=None):
if response:
# We need to keep the cookies, see ticket #4994.
cookies = response.cookies
response = HttpResponseNotModified()
response.cookies = cookies
return response
else:
return HttpResponseNotModified()
def get_conditional_response(request, etag=None, last_modified=None, response=None):
# Get HTTP request headers
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
if_match = request.META.get('HTTP_IF_MATCH')
etags = []
if if_none_match or if_match:
# There can be more than one ETag in the request, so we
# consider the list of values.
try:
etags = parse_etags(if_none_match or if_match)
except ValueError:
# In case of an invalid ETag, ignore all ETag headers.
# Apparently Opera sends invalidly quoted headers at times
# (we should be returning a 400 response, but that's a
# little extreme) -- this is bug #10681.
if_none_match = None
if_match = None
# If-None-Match must be ignored if original result would be anything
# other than a 2XX or 304 status. 304 status would result in no change.
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
if response and not (200 <= response.status_code < 300):
if_none_match = None
if_match = None
# If-Modified-Since must be ignored if the original result was not a 200.
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
if response and response.status_code != 200:
if_modified_since = None
if_unmodified_since = None
if not ((if_match and if_modified_since) or
(if_none_match and if_unmodified_since) or
(if_modified_since and if_unmodified_since) or
(if_match and if_none_match)):
# We only get here if no undefined combinations of headers are
# specified.
if ((if_none_match and (etag in etags or
'*' in etags and etag)) and
(not if_modified_since or
(last_modified and if_modified_since and
last_modified <= if_modified_since))):
if request.method in ('GET', 'HEAD'):
return _not_modified(request, response)
else:
return _precondition_failed(request)
elif (if_match and ((not etag and '*' in etags) or
(etag and etag not in etags) or
(last_modified and if_unmodified_since and
last_modified > if_unmodified_since))):
return _precondition_failed(request)
elif (not if_none_match and request.method in ('GET', 'HEAD') and
last_modified and if_modified_since and
last_modified <= if_modified_since):
return _not_modified(request, response)
elif (not if_match and
last_modified and if_unmodified_since and
last_modified > if_unmodified_since):
return _precondition_failed(request)
return response return response
@ -119,9 +215,9 @@ def patch_response_headers(response, cache_timeout=None):
cache_timeout = 0 # Can't have max-age negative cache_timeout = 0 # Can't have max-age negative
if settings.USE_ETAGS and not response.has_header('ETag'): if settings.USE_ETAGS and not response.has_header('ETag'):
if hasattr(response, 'render') and callable(response.render): if hasattr(response, 'render') and callable(response.render):
response.add_post_render_callback(_set_response_etag) response.add_post_render_callback(set_response_etag)
else: else:
response = _set_response_etag(response) response = set_response_etag(response)
if not response.has_header('Last-Modified'): if not response.has_header('Last-Modified'):
response['Last-Modified'] = http_date() response['Last-Modified'] = http_date()
if not response.has_header('Expires'): if not response.has_header('Expires'):

View File

@ -6,14 +6,11 @@ import logging
from calendar import timegm from calendar import timegm
from functools import wraps from functools import wraps
from django.http import ( from django.http import HttpResponseNotAllowed
HttpResponse, HttpResponseNotAllowed, HttpResponseNotModified,
)
from django.middleware.http import ConditionalGetMiddleware from django.middleware.http import ConditionalGetMiddleware
from django.utils.cache import get_conditional_response
from django.utils.decorators import available_attrs, decorator_from_middleware from django.utils.decorators import available_attrs, decorator_from_middleware
from django.utils.http import ( from django.utils.http import http_date, quote_etag
http_date, parse_etags, parse_http_date_safe, quote_etag,
)
conditional_page = decorator_from_middleware(ConditionalGetMiddleware) conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
@ -56,16 +53,6 @@ require_safe = require_http_methods(["GET", "HEAD"])
require_safe.__doc__ = "Decorator to require that a view only accepts safe methods: GET and HEAD." require_safe.__doc__ = "Decorator to require that a view only accepts safe methods: GET and HEAD."
def _precondition_failed(request):
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': 412,
'request': request
},
)
return HttpResponse(status=412)
def condition(etag_func=None, last_modified_func=None): def condition(etag_func=None, last_modified_func=None):
""" """
Decorator to support conditional retrieval (or change) for a view Decorator to support conditional retrieval (or change) for a view
@ -91,29 +78,6 @@ def condition(etag_func=None, last_modified_func=None):
def decorator(func): def decorator(func):
@wraps(func, assigned=available_attrs(func)) @wraps(func, assigned=available_attrs(func))
def inner(request, *args, **kwargs): def inner(request, *args, **kwargs):
# Get HTTP request headers
if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if_none_match = request.META.get("HTTP_IF_NONE_MATCH")
if_match = request.META.get("HTTP_IF_MATCH")
etags = []
if if_none_match or if_match:
# There can be more than one ETag in the request, so we
# consider the list of values.
try:
etags = parse_etags(if_none_match or if_match)
except ValueError:
# In case of invalid etag ignore all ETag headers.
# Apparently Opera sends invalidly quoted headers at times
# (we should be returning a 400 response, but that's a
# little extreme) -- this is Django bug #10681.
if_none_match = None
if_match = None
# Compute values (if any) for the requested resource. # Compute values (if any) for the requested resource.
def get_last_modified(): def get_last_modified():
if last_modified_func: if last_modified_func:
@ -124,35 +88,11 @@ def condition(etag_func=None, last_modified_func=None):
res_etag = etag_func(request, *args, **kwargs) if etag_func else None res_etag = etag_func(request, *args, **kwargs) if etag_func else None
res_last_modified = get_last_modified() res_last_modified = get_last_modified()
response = None response = get_conditional_response(
if not ((if_match and if_modified_since) or request,
(if_none_match and if_unmodified_since) or etag=res_etag,
(if_modified_since and if_unmodified_since) or last_modified=res_last_modified,
(if_match and if_none_match)): )
# We only get here if no undefined combinations of headers are
# specified.
if ((if_none_match and (res_etag in etags or
"*" in etags and res_etag)) and
(not if_modified_since or
(res_last_modified and if_modified_since and
res_last_modified <= if_modified_since))):
if request.method in ("GET", "HEAD"):
response = HttpResponseNotModified()
else:
response = _precondition_failed(request)
elif (if_match and ((not res_etag and "*" in etags) or
(res_etag and res_etag not in etags) or
(res_last_modified and if_unmodified_since and
res_last_modified > if_unmodified_since))):
response = _precondition_failed(request)
elif (not if_none_match and request.method in ("GET", "HEAD") and
res_last_modified and if_modified_since and
res_last_modified <= if_modified_since):
response = HttpResponseNotModified()
elif (not if_match and
res_last_modified and if_unmodified_since and
res_last_modified > if_unmodified_since):
response = _precondition_failed(request)
if response is None: if response is None:
response = func(request, *args, **kwargs) response = func(request, *args, **kwargs)