diff --git a/swift/common/swob.py b/swift/common/swob.py index 638086ea0e..68b19cf875 100644 --- a/swift/common/swob.py +++ b/swift/common/swob.py @@ -1117,6 +1117,18 @@ class Response(object): self.content_length = 0 return [''] + if self.last_modified and self.request.if_modified_since \ + and self.last_modified <= self.request.if_modified_since: + self.status = 304 + self.content_length = 0 + return [''] + + if self.last_modified and self.request.if_unmodified_since \ + and self.last_modified > self.request.if_unmodified_since: + self.status = 412 + self.content_length = 0 + return [''] + if self.request and self.request.method == 'HEAD': # We explicitly do NOT want to set self.content_length to 0 here return [''] diff --git a/swift/obj/server.py b/swift/obj/server.py index 2c8214e07f..49d33caeb5 100644 --- a/swift/obj/server.py +++ b/swift/obj/server.py @@ -22,7 +22,6 @@ import time import traceback import socket import math -from datetime import datetime from swift import gettext_ as _ from hashlib import md5 @@ -41,9 +40,9 @@ from swift.obj import ssync_receiver from swift.common.http import is_success from swift.common.request_helpers import split_and_validate_path, is_user_meta from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPCreated, \ - HTTPInternalServerError, HTTPNoContent, HTTPNotFound, HTTPNotModified, \ + HTTPInternalServerError, HTTPNoContent, HTTPNotFound, \ HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \ - HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, UTC, \ + HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, \ HTTPInsufficientStorage, HTTPForbidden, HTTPException, HeaderKeyDict, \ HTTPConflict from swift.obj.diskfile import DATAFILE_SYSTEM_META, DiskFileManager @@ -492,16 +491,6 @@ class ObjectController(object): obj_size = int(metadata['Content-Length']) file_x_ts = metadata['X-Timestamp'] file_x_ts_flt = float(file_x_ts) - file_x_ts_utc = datetime.fromtimestamp(file_x_ts_flt, UTC) - - if_unmodified_since = request.if_unmodified_since - if if_unmodified_since and file_x_ts_utc > if_unmodified_since: - return HTTPPreconditionFailed(request=request) - - if_modified_since = request.if_modified_since - if if_modified_since and file_x_ts_utc <= if_modified_since: - return HTTPNotModified(request=request) - keep_cache = (self.keep_cache_private or ('X-Auth-Token' not in request.headers and 'X-Storage-Token' not in request.headers)) diff --git a/test/functional/tests.py b/test/functional/tests.py index 485439a029..665b67aa76 100644 --- a/test/functional/tests.py +++ b/test/functional/tests.py @@ -1718,19 +1718,25 @@ class TestFileComparison(Base): for file_item in self.env.files: hdrs = {'If-Modified-Since': self.env.time_old_f1} self.assert_(file_item.read(hdrs=hdrs)) + self.assert_(file_item.info(hdrs=hdrs)) hdrs = {'If-Modified-Since': self.env.time_new} self.assertRaises(ResponseError, file_item.read, hdrs=hdrs) self.assert_status(304) + self.assertRaises(ResponseError, file_item.info, hdrs=hdrs) + self.assert_status(304) def testIfUnmodifiedSince(self): for file_item in self.env.files: hdrs = {'If-Unmodified-Since': self.env.time_new} self.assert_(file_item.read(hdrs=hdrs)) + self.assert_(file_item.info(hdrs=hdrs)) hdrs = {'If-Unmodified-Since': self.env.time_old_f2} self.assertRaises(ResponseError, file_item.read, hdrs=hdrs) self.assert_status(412) + self.assertRaises(ResponseError, file_item.info, hdrs=hdrs) + self.assert_status(412) def testIfMatchAndUnmodified(self): for file_item in self.env.files: diff --git a/test/unit/common/test_swob.py b/test/unit/common/test_swob.py index 7cc5439e91..b7438dfead 100644 --- a/test/unit/common/test_swob.py +++ b/test/unit/common/test_swob.py @@ -15,8 +15,8 @@ "Tests for swift.common.swob" -import unittest import datetime +import unittest import re import time from StringIO import StringIO @@ -1451,5 +1451,141 @@ class TestConditionalIfMatch(unittest.TestCase): self.assertEquals(body, '') +class TestConditionalIfModifiedSince(unittest.TestCase): + def fake_app(self, environ, start_response): + start_response( + '200 OK', [('Last-Modified', 'Thu, 27 Feb 2014 03:29:37 GMT')]) + return ['hi'] + + def fake_start_response(*a, **kw): + pass + + def test_absent(self): + req = swift.common.swob.Request.blank('/') + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 200) + self.assertEquals(body, 'hi') + + def test_before(self): + req = swift.common.swob.Request.blank( + '/', + headers={'If-Modified-Since': 'Thu, 27 Feb 2014 03:29:36 GMT'}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 200) + self.assertEquals(body, 'hi') + + def test_same(self): + req = swift.common.swob.Request.blank( + '/', + headers={'If-Modified-Since': 'Thu, 27 Feb 2014 03:29:37 GMT'}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 304) + self.assertEquals(body, '') + + def test_greater(self): + req = swift.common.swob.Request.blank( + '/', + headers={'If-Modified-Since': 'Thu, 27 Feb 2014 03:29:38 GMT'}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 304) + self.assertEquals(body, '') + + def test_out_of_range_is_ignored(self): + # All that datetime gives us is a ValueError or OverflowError when + # something is out of range (i.e. less than datetime.datetime.min or + # greater than datetime.datetime.max). Unfortunately, we can't + # distinguish between a date being too old and a date being too new, + # so the best we can do is ignore such headers. + max_date_list = list(datetime.datetime.max.timetuple()) + max_date_list[0] += 1 # bump up the year + too_big_date_header = time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", time.struct_time(max_date_list)) + + req = swift.common.swob.Request.blank( + '/', + headers={'If-Modified-Since': too_big_date_header}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 200) + self.assertEquals(body, 'hi') + + +class TestConditionalIfUnmodifiedSince(unittest.TestCase): + def fake_app(self, environ, start_response): + start_response( + '200 OK', [('Last-Modified', 'Thu, 20 Feb 2014 03:29:37 GMT')]) + return ['hi'] + + def fake_start_response(*a, **kw): + pass + + def test_absent(self): + req = swift.common.swob.Request.blank('/') + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 200) + self.assertEquals(body, 'hi') + + def test_before(self): + req = swift.common.swob.Request.blank( + '/', + headers={'If-Unmodified-Since': 'Thu, 20 Feb 2014 03:29:36 GMT'}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 412) + self.assertEquals(body, '') + + def test_same(self): + req = swift.common.swob.Request.blank( + '/', + headers={'If-Unmodified-Since': 'Thu, 20 Feb 2014 03:29:37 GMT'}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 200) + self.assertEquals(body, 'hi') + + def test_greater(self): + req = swift.common.swob.Request.blank( + '/', + headers={'If-Unmodified-Since': 'Thu, 20 Feb 2014 03:29:38 GMT'}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 200) + self.assertEquals(body, 'hi') + + def test_out_of_range_is_ignored(self): + # All that datetime gives us is a ValueError or OverflowError when + # something is out of range (i.e. less than datetime.datetime.min or + # greater than datetime.datetime.max). Unfortunately, we can't + # distinguish between a date being too old and a date being too new, + # so the best we can do is ignore such headers. + max_date_list = list(datetime.datetime.max.timetuple()) + max_date_list[0] += 1 # bump up the year + too_big_date_header = time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", time.struct_time(max_date_list)) + + req = swift.common.swob.Request.blank( + '/', + headers={'If-Unmodified-Since': too_big_date_header}) + resp = req.get_response(self.fake_app) + resp.conditional_response = True + body = ''.join(resp(req.environ, self.fake_start_response)) + self.assertEquals(resp.status_int, 200) + self.assertEquals(body, 'hi') + + if __name__ == '__main__': unittest.main() diff --git a/test/unit/obj/test_server.py b/test/unit/obj/test_server.py index 1614783d4c..de28f72af2 100755 --- a/test/unit/obj/test_server.py +++ b/test/unit/obj/test_server.py @@ -1274,6 +1274,78 @@ class TestObjectController(unittest.TestCase): resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 304) + def test_HEAD_if_modified_since(self): + timestamp = normalize_timestamp(time()) + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={ + 'X-Timestamp': timestamp, + 'Content-Type': 'application/octet-stream', + 'Content-Length': '4'}) + req.body = 'test' + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 201) + + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 200) + + since = strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(float(timestamp) + 1)) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Modified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 304) + + since = \ + strftime('%a, %d %b %Y %H:%M:%S GMT', gmtime(float(timestamp) - 1)) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Modified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 200) + + since = \ + strftime('%a, %d %b %Y %H:%M:%S GMT', gmtime(float(timestamp) + 1)) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Modified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 304) + + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}) + resp = req.get_response(self.object_controller) + since = resp.headers['Last-Modified'] + self.assertEquals(since, strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp))))) + + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Modified-Since': since}) + resp = self.object_controller.GET(req) + self.assertEquals(resp.status_int, 304) + + timestamp = normalize_timestamp(int(time())) + req = Request.blank('/sda1/p/a/c/o2', + environ={'REQUEST_METHOD': 'PUT'}, + headers={ + 'X-Timestamp': timestamp, + 'Content-Type': 'application/octet-stream', + 'Content-Length': '4'}) + req.body = 'test' + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 201) + + since = strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(float(timestamp))) + req = Request.blank('/sda1/p/a/c/o2', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Modified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 304) + def test_GET_if_unmodified_since(self): timestamp = normalize_timestamp(time()) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -1322,6 +1394,42 @@ class TestObjectController(unittest.TestCase): resp = self.object_controller.GET(req) self.assertEquals(resp.status_int, 200) + def test_HEAD_if_unmodified_since(self): + timestamp = normalize_timestamp(time()) + req = Request.blank( + '/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': timestamp, + 'Content-Type': 'application/octet-stream', + 'Content-Length': '4'}) + req.body = 'test' + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 201) + + since = strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp)) + 1)) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Unmodified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 200) + + since = strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp)))) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Unmodified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 200) + + since = strftime('%a, %d %b %Y %H:%M:%S GMT', + gmtime(math.ceil(float(timestamp)) - 1)) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'HEAD'}, + headers={'If-Unmodified-Since': since}) + resp = req.get_response(self.object_controller) + self.assertEquals(resp.status_int, 412) + def test_GET_quarantine(self): # Test swift.obj.server.ObjectController.GET timestamp = normalize_timestamp(time())