summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorLuke Plant <L.Plant.98@cantab.net>2011-03-01 14:34:03 +0000
committerLuke Plant <L.Plant.98@cantab.net>2011-03-01 14:34:03 +0000
commit58bd2492839f2b1c7463aee34edbd2b009910e7d (patch)
tree1182b1c1fab2ce70d5f1075a74c5aae966cda3db /django
parent8d3ee2984796336285961a8f9a00e1ceb77f2dd4 (diff)
[1.2.X] Fixed #717 - If-Modified-Since handling should compare dates according to RFC 2616
Thanks to Maniac for the report, julienb for the initial patch, and especially to aaugustin for the final patch and tests. Backport of [15696] from trunk. git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.2.X@15697 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
-rw-r--r--django/middleware/http.py18
-rw-r--r--django/utils/http.py55
-rw-r--r--django/views/decorators/http.py18
-rw-r--r--django/views/static.py8
4 files changed, 78 insertions, 21 deletions
diff --git a/django/middleware/http.py b/django/middleware/http.py
index 75af664447..9269b65c94 100644
--- a/django/middleware/http.py
+++ b/django/middleware/http.py
@@ -1,5 +1,5 @@
from django.core.exceptions import MiddlewareNotUsed
-from django.utils.http import http_date
+from django.utils.http import http_date, parse_http_date_safe
class ConditionalGetMiddleware(object):
"""
@@ -15,7 +15,7 @@ class ConditionalGetMiddleware(object):
response['Content-Length'] = str(len(response.content))
if response.has_header('ETag'):
- if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None)
+ 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
@@ -23,11 +23,15 @@ class ConditionalGetMiddleware(object):
response.status_code = 304
if response.has_header('Last-Modified'):
- if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None)
- if if_modified_since == response['Last-Modified']:
- # Setting the status code is enough here (same reasons as
- # above).
- response.status_code = 304
+ if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
+ if if_modified_since is not None:
+ if_modified_since = parse_http_date_safe(if_modified_since)
+ 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
diff --git a/django/utils/http.py b/django/utils/http.py
index 49d067e57c..61623fc696 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -1,3 +1,5 @@
+import calendar
+import datetime
import re
import sys
import urllib
@@ -8,6 +10,17 @@ from django.utils.functional import allow_lazy
ETAG_MATCH = re.compile(r'(?:W/)?"((?:\\.|[^"])*)"')
+MONTHS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split()
+__D = r'(?P<day>\d{2})'
+__D2 = r'(?P<day>[ \d]\d)'
+__M = r'(?P<mon>\w{3})'
+__Y = r'(?P<year>\d{4})'
+__Y2 = r'(?P<year>\d{2})'
+__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
+RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))
+RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))
+ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
+
def urlquote(url, safe='/'):
"""
A version of Python's urllib.quote() function that can operate on unicode
@@ -70,6 +83,48 @@ def http_date(epoch_seconds=None):
rfcdate = formatdate(epoch_seconds)
return '%s GMT' % rfcdate[:25]
+def parse_http_date(date):
+ """
+ Parses a date format as specified by HTTP RFC2616 section 3.3.1.
+
+ The three formats allowed by the RFC are accepted, even if only the first
+ one is still in widespread use.
+
+ Returns an floating point number expressed in seconds since the epoch, in
+ UTC.
+ """
+ # emails.Util.parsedate does the job for RFC1123 dates; unfortunately
+ # RFC2616 makes it mandatory to support RFC850 dates too. So we roll
+ # our own RFC-compliant parsing.
+ for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
+ m = regex.match(date)
+ if m is not None:
+ break
+ else:
+ raise ValueError("%r is not in a valid HTTP date format" % date)
+ try:
+ year = int(m.group('year'))
+ if year < 100:
+ year += 2000 if year < 70 else 1900
+ month = MONTHS.index(m.group('mon').lower()) + 1
+ day = int(m.group('day'))
+ hour = int(m.group('hour'))
+ min = int(m.group('min'))
+ sec = int(m.group('sec'))
+ result = datetime.datetime(year, month, day, hour, min, sec)
+ return calendar.timegm(result.utctimetuple())
+ except Exception:
+ raise ValueError("%r is not a valid date" % date)
+
+def parse_http_date_safe(date):
+ """
+ Same as parse_http_date, but returns None if the input is invalid.
+ """
+ try:
+ return parse_http_date(date)
+ except Exception:
+ pass
+
# Base 36 functions: useful for generating compact URLs
def base36_to_int(s):
diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py
index 220a60c4be..0032bf0600 100644
--- a/django/views/decorators/http.py
+++ b/django/views/decorators/http.py
@@ -9,10 +9,9 @@ except ImportError:
from calendar import timegm
from datetime import timedelta
-from email.Utils import formatdate
from django.utils.decorators import decorator_from_middleware, available_attrs
-from django.utils.http import parse_etags, quote_etag
+from django.utils.http import http_date, parse_http_date_safe, parse_etags, quote_etag
from django.middleware.http import ConditionalGetMiddleware
from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse
@@ -70,6 +69,8 @@ def condition(etag_func=None, last_modified_func=None):
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_none_match = request.META.get("HTTP_IF_NONE_MATCH")
if_match = request.META.get("HTTP_IF_MATCH")
if if_none_match or if_match:
@@ -93,7 +94,7 @@ def condition(etag_func=None, last_modified_func=None):
if last_modified_func:
dt = last_modified_func(request, *args, **kwargs)
if dt:
- res_last_modified = formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT'
+ res_last_modified = timegm(dt.utctimetuple())
else:
res_last_modified = None
else:
@@ -107,7 +108,8 @@ def condition(etag_func=None, last_modified_func=None):
if ((if_none_match and (res_etag in etags or
"*" in etags and res_etag)) and
(not if_modified_since or
- res_last_modified == if_modified_since)):
+ (res_last_modified and if_modified_since and
+ res_last_modified <= if_modified_since))):
if request.method in ("GET", "HEAD"):
response = HttpResponseNotModified()
else:
@@ -115,9 +117,9 @@ def condition(etag_func=None, last_modified_func=None):
elif if_match and ((not res_etag and "*" in etags) or
(res_etag and res_etag not in etags)):
response = HttpResponse(status=412)
- elif (not if_none_match and if_modified_since and
- request.method == "GET" and
- res_last_modified == if_modified_since):
+ elif (not if_none_match and request.method == "GET" and
+ res_last_modified and if_modified_since and
+ res_last_modified <= if_modified_since):
response = HttpResponseNotModified()
if response is None:
@@ -125,7 +127,7 @@ def condition(etag_func=None, last_modified_func=None):
# Set relevant headers on the response if they don't already exist.
if res_last_modified and not response.has_header('Last-Modified'):
- response['Last-Modified'] = res_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)
diff --git a/django/views/static.py b/django/views/static.py
index fda3764b0c..926bb473ac 100644
--- a/django/views/static.py
+++ b/django/views/static.py
@@ -9,12 +9,11 @@ import posixpath
import re
import stat
import urllib
-from email.Utils import parsedate_tz, mktime_tz
from django.template import loader
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified
from django.template import Template, Context, TemplateDoesNotExist
-from django.utils.http import http_date
+from django.utils.http import http_date, parse_http_date
def serve(request, path, document_root=None, show_indexes=False):
"""
@@ -129,10 +128,7 @@ def was_modified_since(header=None, mtime=0, size=0):
raise ValueError
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
re.IGNORECASE)
- header_date = parsedate_tz(matches.group(1))
- if header_date is None:
- raise ValueError
- header_mtime = mktime_tz(header_date)
+ header_mtime = parse_http_date(matches.group(1))
header_len = matches.group(3)
if header_len and int(header_len) != size:
raise ValueError