### Eclipse Workspace Patch 1.0 #P httpcomponents-client Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (revision 959977) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (working copy) @@ -4267,7 +4267,216 @@ Assert.assertTrue(HttpStatus.SC_PARTIAL_CONTENT == status); } } + + /* "[The cache control directive] "private" Indicates that all or part of + * the response message is intended for a single user and MUST NOT be + * cached by a shared cache." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1 + */ + @Test + public void testCacheControlPrivateIsNotCacheableBySharedCache() + throws Exception { + if (impl.isSharedCache()) { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("Cache-Control","private,max-age=3600"); + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp2 = make200Response(); + // this backend request MUST happen + backendExpectsAnyRequest().andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + impl.execute(host,req2); + verifyMocks(); + } + } + + @Test + public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache() + throws Exception { + if (impl.isSharedCache()) { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("X-Personal","stuff"); + resp1.setHeader("Cache-Control","private=\"X-Personal\",s-maxage=3600"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp2 = make200Response(); + + // this backend request MAY happen + backendExpectsAnyRequest().andReturn(resp2).times(0,1); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + Assert.assertNull(result.getFirstHeader("X-Personal")); + } + } + + /* "If the no-cache directive does not specify a field-name, then a + * cache MUST NOT use the response to satisfy a subsequent request + * without successful revalidation with the origin server. This allows + * an origin server to prevent caching even by caches that have been + * configured to return stale responses to client requests." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1 + */ + @Test + public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation() + throws Exception { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Cache-Control","no-cache"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp2 = make200Response(); + + // this MUST happen + backendExpectsAnyRequest().andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + impl.execute(host,req2); + verifyMocks(); + } + + @Test + public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications() + throws Exception { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Cache-Control","no-cache,s-maxage=3600"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + req2.setHeader("Cache-Control","max-stale=7200"); + HttpResponse resp2 = make200Response(); + + // this MUST happen + backendExpectsAnyRequest().andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + impl.execute(host,req2); + verifyMocks(); + } + + /* "If the no-cache directive does specify one or more field-names, then + * a cache MAY use the response to satisfy a subsequent request, subject + * to any other restrictions on caching. However, the specified + * field-name(s) MUST NOT be sent in the response to a subsequent request + * without successful revalidation with the origin server." + */ + @Test + public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() + throws Exception { + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("X-Stuff","things"); + resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp2 = make200Response(); + resp2.setHeader("ETag","\"etag\""); + resp2.setHeader("X-Stuff","things"); + resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600"); + + Capture cap = new Capture(); + EasyMock.expect(mockBackend.execute(EasyMock.eq(host), + EasyMock.capture(cap), + (HttpContext)EasyMock.isNull())) + .andReturn(resp2).times(0,1); + + replayMocks(); + impl.execute(host,req1); + HttpResponse result = impl.execute(host,req2); + verifyMocks(); + + if (!cap.hasCaptured()) { + Assert.assertNull(result.getFirstHeader("X-Stuff")); + } + } + + /* "The purpose of the no-store directive is to prevent the inadvertent + * release or retention of sensitive information (for example, on backup + * tapes). The no-store directive applies to the entire message, and MAY + * be sent either in a response or in a request. If sent in a request, a + * cache MUST NOT store any part of either this request or any response + * to it. If sent in a response, a cache MUST NOT store any part of + * either this response or the request that elicited it. This directive + * applies to both non- shared and shared caches. "MUST NOT store" in + * this context means that the cache MUST NOT intentionally store the + * information in non-volatile storage, and MUST make a best-effort + * attempt to remove the information from volatile storage as promptly + * as possible after forwarding it." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2 + */ + @Test + public void testNoStoreOnRequestIsNotStoredInCache() + throws Exception { + emptyMockCacheExpectsNoPuts(); + request.setHeader("Cache-Control","no-store"); + backendExpectsAnyRequest().andReturn(originResponse); + + replayMocks(); + impl.execute(host,request); + verifyMocks(); + } + + @Test + public void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable() + throws Exception { + emptyMockCacheExpectsNoPuts(); + request.setHeader("Cache-Control","no-store"); + originResponse.setHeader("Cache-Control","max-age=3600"); + backendExpectsAnyRequest().andReturn(originResponse); + + replayMocks(); + impl.execute(host,request); + verifyMocks(); + } + + @Test + public void testNoStoreOnResponseIsNotStoredInCache() + throws Exception { + emptyMockCacheExpectsNoPuts(); + originResponse.setHeader("Cache-Control","no-store"); + backendExpectsAnyRequest().andReturn(originResponse); + + replayMocks(); + impl.execute(host,request); + verifyMocks(); + } + + @Test + public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators() + throws Exception { + emptyMockCacheExpectsNoPuts(); + originResponse.setHeader("Cache-Control","no-store,max-age=3600"); + backendExpectsAnyRequest().andReturn(originResponse); + + replayMocks(); + impl.execute(host,request); + verifyMocks(); + } + private class FakeHeaderGroup extends HeaderGroup{ public void addHeader(String name, String value){ Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (revision 959977) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (working copy) @@ -612,4 +612,8 @@ return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS; } + public boolean isSharedCache() { + return true; + } + } Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java (revision 959977) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java (working copy) @@ -1053,6 +1053,10 @@ } } + @Test + public void testIsSharedCacheByDefault() throws Exception { + Assert.assertTrue(impl.isSharedCache()); + } private byte[] readResponse(HttpResponse response) { try { Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java (revision 959977) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java (working copy) @@ -30,6 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.http.Header; import org.apache.http.HeaderElement; +import org.apache.http.HttpMessage; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; @@ -140,36 +141,41 @@ } protected boolean isExplicitlyNonCacheable(HttpResponse response) { - Header[] cacheControlHeaders = response.getHeaders(HeaderConstants.CACHE_CONTROL); + String[] nonCacheableDirectives = { + HeaderConstants.CACHE_CONTROL_NO_STORE, + HeaderConstants.CACHE_CONTROL_NO_CACHE, + HeaderConstants.CACHE_CONTROL_PRIVATE + }; + return hasCacheControlDirectiveFrom(response, nonCacheableDirectives); + } + + protected boolean hasCacheControlDirectiveFrom(HttpMessage msg, + String[] directives) { + Header[] cacheControlHeaders = msg.getHeaders(HeaderConstants.CACHE_CONTROL); for (Header header : cacheControlHeaders) { for (HeaderElement elem : header.getElements()) { - if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elem.getName()) - || HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elem.getName()) - || "private".equals(elem.getName())) { - return true; - } + for(String directive : directives) { + if (directive.equalsIgnoreCase(elem.getName())) { + return true; + } + } } } return false; - } - + } + protected boolean isExplicitlyCacheable(HttpResponse response) { if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) return true; - Header[] cacheControlHeaders = response.getHeaders(HeaderConstants.CACHE_CONTROL); - for (Header header : cacheControlHeaders) { - for (HeaderElement elem : header.getElements()) { - if ("max-age".equals(elem.getName()) || "s-maxage".equals(elem.getName()) - || "must-revalidate".equals(elem.getName()) - || "proxy-revalidate".equals(elem.getName()) - || "public".equals(elem.getName())) { - return true; - } - } - } - return false; + String explicitlyCacheableDirectives[] = { + "max-age", "s-maxage", "must-revalidate", + "proxy-revalidate", "public" + }; + return hasCacheControlDirectiveFrom(response, + explicitlyCacheableDirectives); } + /** * Determine if the {@link HttpResponse} gotten from the origin is a * cacheable response. @@ -183,7 +189,11 @@ log.debug("Response was not cacheable."); return false; } - + String[] uncacheableRequestDirectives = { "no-store" }; + if (hasCacheControlDirectiveFrom(request,uncacheableRequestDirectives)) { + return false; + } + if (request.getRequestLine().getUri().contains("?") && !isExplicitlyCacheable(response)) { log.debug("Response was not cacheable."); return false; Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestResponseCachingPolicy.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestResponseCachingPolicy.java (revision 959977) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestResponseCachingPolicy.java (working copy) @@ -43,7 +43,7 @@ public class TestResponseCachingPolicy { - private static final ProtocolVersion PROTOCOL_VERSION = new ProtocolVersion("HTTP", 1, 1); + private static final ProtocolVersion HTTP_1_1 = new ProtocolVersion("HTTP", 1, 1); private ResponseCachingPolicy policy; private HttpResponse response; private HttpRequest request; @@ -55,9 +55,10 @@ public void setUp() throws Exception { policy = new ResponseCachingPolicy(0); response = new BasicHttpResponse( - new BasicStatusLine(PROTOCOL_VERSION, HttpStatus.SC_OK, "")); + new BasicStatusLine(HTTP_1_1, HttpStatus.SC_OK, "")); response.setHeader("Date", DateUtils.formatDate(new Date())); response.setHeader("Content-Length", "0"); + request = new BasicHttpRequest("GET","/",HTTP_1_1); } @Test @@ -219,7 +220,7 @@ Assert.assertTrue(policy.isResponseCacheable("GET", response)); response = new BasicHttpResponse( - new BasicStatusLine(PROTOCOL_VERSION, HttpStatus.SC_OK, "")); + new BasicStatusLine(HTTP_1_1, HttpStatus.SC_OK, "")); response.setHeader("Date", DateUtils.formatDate(new Date())); response.addHeader("Cache-Control", "no-transform"); response.setHeader("Content-Length", "0"); @@ -229,12 +230,12 @@ @Test public void testIsGetWithout200Cacheable() { - HttpResponse response = new BasicHttpResponse(new BasicStatusLine(PROTOCOL_VERSION, + HttpResponse response = new BasicHttpResponse(new BasicStatusLine(HTTP_1_1, HttpStatus.SC_NOT_FOUND, "")); Assert.assertFalse(policy.isResponseCacheable("GET", response)); - response = new BasicHttpResponse(new BasicStatusLine(PROTOCOL_VERSION, + response = new BasicHttpResponse(new BasicStatusLine(HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "")); Assert.assertFalse(policy.isResponseCacheable("GET", response)); @@ -261,6 +262,13 @@ } @Test + public void testResponsesToRequestsWithNoStoreAreNotCacheable() { + request.setHeader("Cache-Control","no-store"); + response.setHeader("Cache-Control","public"); + Assert.assertFalse(policy.isResponseCacheable(request,response)); + } + + @Test public void testResponsesWithMultipleAgeHeadersAreNotCacheable() { response.addHeader("Age", "3"); response.addHeader("Age", "5"); @@ -308,7 +316,7 @@ response.setHeader("Content-Length", "0"); Assert.assertTrue(policy.isResponseCacheable("GET", response)); } - + @Test public void testResponsesToGETWithQueryParamsButNoExplicitCachingAreNotCacheable() { request = new BasicHttpRequest("GET", "/foo?s=bar"); Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/HeaderConstants.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/HeaderConstants.java (revision 959977) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/HeaderConstants.java (working copy) @@ -62,6 +62,7 @@ public static final String CACHE_CONTROL_MAX_AGE = "max-age"; public static final String CACHE_CONTROL_MAX_STALE = "max-stale"; public static final String CACHE_CONTROL_MIN_FRESH = "min-fresh"; + public static final String CACHE_CONTROL_PRIVATE = "private"; public static final String WARNING = "Warning"; public static final String RANGE = "Range";