### 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 958667) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (working copy) @@ -2158,9 +2158,8 @@ originResponse = new BasicHttpResponse(HTTP_1_1, 405, "Method Not Allowed"); originResponse.setHeader("Allow", "GET, HEAD"); - EasyMock.expect( - mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class), - (HttpContext) EasyMock.isNull())).andReturn(originResponse); + backendExpectsAnyRequest().andReturn(originResponse); + replayMocks(); HttpResponse result = impl.execute(host, request); @@ -2591,7 +2590,6 @@ Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L); Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L); - FakeHeaderGroup headerGroup = new FakeHeaderGroup(); headerGroup.setHeader("Date", DateUtils.formatDate(nineSecondsAgo)); @@ -3663,6 +3661,279 @@ HttpTestUtils.getCanonicalHeaderValue(result2, h)); } + /* "All methods that might be expected to cause modifications to the origin + * server's resources MUST be written through to the origin server. This + * currently includes all methods except for GET and HEAD. A cache MUST NOT + * reply to such a request from a client before having transmitted the + * request to the inbound server, and having received a corresponding + * response from the inbound server." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.11 + */ + private void testRequestIsWrittenThroughToOrigin(HttpRequest req) + throws Exception { + HttpResponse resp = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"); + EasyMock.expect(mockBackend.execute(EasyMock.eq(host), + eqRequest(req), + (HttpContext)EasyMock.isNull())) + .andReturn(resp); + + replayMocks(); + impl.execute(host, req); + verifyMocks(); + } + +// @Test +// public void testOPTIONSRequestsAreWrittenThroughToOrigin() +// throws Exception { +// HttpRequest req = new BasicHttpRequest("OPTIONS","*",HTTP_1_1); +// testRequestIsWrittenThroughToOrigin(req); +// } + + @Test + public void testPOSTRequestsAreWrittenThroughToOrigin() + throws Exception { + HttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("POST","/",HTTP_1_1); + req.setEntity(makeBody(128)); + req.setHeader("Content-Length","128"); + testRequestIsWrittenThroughToOrigin(req); + } + + @Test + public void testPUTRequestsAreWrittenThroughToOrigin() + throws Exception { + HttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("PUT","/",HTTP_1_1); + req.setEntity(makeBody(128)); + req.setHeader("Content-Length","128"); + testRequestIsWrittenThroughToOrigin(req); + } + + @Test + public void testDELETERequestsAreWrittenThroughToOrigin() + throws Exception { + HttpRequest req = new BasicHttpRequest("DELETE","/",HTTP_1_1); + testRequestIsWrittenThroughToOrigin(req); + } + + @Test + public void testTRACERequestsAreWrittenThroughToOrigin() + throws Exception { + HttpRequest req = new BasicHttpRequest("TRACE","/",HTTP_1_1); + testRequestIsWrittenThroughToOrigin(req); + } + + @Test + public void testCONNECTRequestsAreWrittenThroughToOrigin() + throws Exception { + HttpRequest req = new BasicHttpRequest("CONNECT","/",HTTP_1_1); + testRequestIsWrittenThroughToOrigin(req); + } + + @Test + public void testUnknownMethodRequestsAreWrittenThroughToOrigin() + throws Exception { + HttpRequest req = new BasicHttpRequest("UNKNOWN","/",HTTP_1_1); + testRequestIsWrittenThroughToOrigin(req); + } + + /* "If a cache receives a value larger than the largest positive + * integer it can represent, or if any of its age calculations + * overflows, it MUST transmit an Age header with a value of + * 2147483648 (2^31)." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.6 + */ + @Test + public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig() + throws Exception { + String reallyOldAge = "1" + Long.MAX_VALUE; + originResponse.setHeader("Age",reallyOldAge); + + backendExpectsAnyRequest().andReturn(originResponse); + + replayMocks(); + HttpResponse result = impl.execute(host,request); + verifyMocks(); + + Assert.assertEquals("2147483648", + result.getFirstHeader("Age").getValue()); + } + + /* "A proxy MUST NOT modify the Allow header field even if it does not + * understand all the methods specified, since the user agent might + * have other means of communicating with the origin server. + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 + */ + @Test + public void testDoesNotModifyAllowHeaderWithUnknownMethods() + throws Exception { + String allowHeaderValue = "GET, HEAD, FOOBAR"; + originResponse.setHeader("Allow",allowHeaderValue); + backendExpectsAnyRequest().andReturn(originResponse); + replayMocks(); + HttpResponse result = impl.execute(host,request); + verifyMocks(); + Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse,"Allow"), + HttpTestUtils.getCanonicalHeaderValue(result, "Allow")); + } + + /* "When a shared cache (see section 13.7) receives a request + * containing an Authorization field, it MUST NOT return the + * corresponding response as a reply to any other request, unless one + * of the following specific exceptions holds: + * + * 1. If the response includes the "s-maxage" cache-control + * directive, the cache MAY use that response in replying to a + * subsequent request. But (if the specified maximum age has + * passed) a proxy cache MUST first revalidate it with the origin + * server, using the request-headers from the new request to allow + * the origin server to authenticate the new request. (This is the + * defined behavior for s-maxage.) If the response includes "s- + * maxage=0", the proxy MUST always revalidate it before re-using + * it. + * + * 2. If the response includes the "must-revalidate" cache-control + * directive, the cache MAY use that response in replying to a + * subsequent request. But if the response is stale, all caches + * MUST first revalidate it with the origin server, using the + * request-headers from the new request to allow the origin server + * to authenticate the new request. + * + * 3. If the response includes the "public" cache-control directive, + * it MAY be returned in reply to any subsequent request. + */ + protected void testSharedCacheRevalidatesAuthorizedResponse( + HttpResponse authorizedResponse, int minTimes, int maxTimes) throws Exception, + IOException { + if (impl.isSharedCache()) { + String authorization = "Basic dXNlcjpwYXNzd2Q="; + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + req1.setHeader("Authorization",authorization); + + backendExpectsAnyRequest().andReturn(authorizedResponse); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + HttpResponse resp2 = make200Response(); + resp2.setHeader("Cache-Control","max-age=3600"); + + if (maxTimes > 0) { + // this request MUST happen + backendExpectsAnyRequest().andReturn(resp2) + .times(minTimes,maxTimes); + } + + replayMocks(); + impl.execute(host, req1); + impl.execute(host, req2); + verifyMocks(); + } + } + + @Test + public void testSharedCacheMustNotNormallyCacheAuthorizedResponses() + throws Exception { + HttpResponse resp = make200Response(); + resp.setHeader("Cache-Control","max-age=3600"); + resp.setHeader("ETag","\"etag\""); + testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1); + } + + @Test + public void testSharedCacheMayCacheAuthorizedResponsesWithSMaxAgeHeader() + throws Exception { + HttpResponse resp = make200Response(); + resp.setHeader("Cache-Control","s-maxage=3600"); + resp.setHeader("ETag","\"etag\""); + testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1); + } + + @Test + public void testSharedCacheMustRevalidateAuthorizedResponsesWhenSMaxAgeIsZero() + throws Exception { + HttpResponse resp = make200Response(); + resp.setHeader("Cache-Control","s-maxage=0"); + resp.setHeader("ETag","\"etag\""); + testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1); + } + + @Test + public void testSharedCacheMayCacheAuthorizedResponsesWithMustRevalidate() + throws Exception { + HttpResponse resp = make200Response(); + resp.setHeader("Cache-Control","must-revalidate"); + resp.setHeader("ETag","\"etag\""); + testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1); + } + + @Test + public void testSharedCacheMayCacheAuthorizedResponsesWithCacheControlPublic() + throws Exception { + HttpResponse resp = make200Response(); + resp.setHeader("Cache-Control","public"); + testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1); + } + + protected void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse( + HttpResponse authorizedResponse) throws Exception, IOException, + ClientProtocolException { + if (impl.isSharedCache()) { + String authorization1 = "Basic dXNlcjpwYXNzd2Q="; + String authorization2 = "Basic dXNlcjpwYXNzd2Qy"; + + HttpRequest req1 = new BasicHttpRequest("GET","/",HTTP_1_1); + req1.setHeader("Authorization",authorization1); + + backendExpectsAnyRequest().andReturn(authorizedResponse); + + HttpRequest req2 = new BasicHttpRequest("GET","/",HTTP_1_1); + req2.setHeader("Authorization",authorization2); + + HttpResponse resp2 = make200Response(); + + Capture cap = new Capture(); + EasyMock.expect(mockBackend.execute(EasyMock.eq(host), + EasyMock.capture(cap), + (HttpContext)EasyMock.isNull())) + .andReturn(resp2); + + replayMocks(); + impl.execute(host,req1); + impl.execute(host,req2); + verifyMocks(); + + HttpRequest captured = cap.getValue(); + Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(req2, "Authorization"), + HttpTestUtils.getCanonicalHeaderValue(captured, "Authorization")); + } + } + + @Test + public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithSMaxAge() + throws Exception { + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + HttpResponse resp1 = make200Response(); + resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo)); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Cache-Control","s-maxage=5"); + + testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1); + } + + @Test + public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithMustRevalidate() + throws Exception { + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + HttpResponse resp1 = make200Response(); + resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo)); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Cache-Control","maxage=5, must-revalidate"); + + testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1); + } + 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 958667) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java (working copy) @@ -579,4 +579,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 958667) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java (working copy) @@ -927,6 +927,11 @@ Assert.assertTrue(gotException); } + @Test + public void testIsSharedCache() throws Exception { + Assert.assertTrue(impl.isSharedCache()); + } + private byte[] readResponse(HttpResponse response) { try { ByteArrayOutputStream s1 = new ByteArrayOutputStream(); 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 958667) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java (working copy) @@ -153,22 +153,28 @@ return false; } - protected boolean isExplicitlyCacheable(HttpResponse response) { - if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) - return true; + protected boolean hasCacheControlParameterFrom(HttpResponse response, String[] params) { 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; - } + for (String param : params) { + if (param.equals(elem.getName())) { + return true; + } + } } } return false; } + + protected boolean isExplicitlyCacheable(HttpResponse response) { + if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) + return true; + String[] cacheableParams = { "max-age", "s-maxage", + "must-revalidate", "proxy-revalidate", "public" + }; + return hasCacheControlParameterFrom(response, cacheableParams); + } /** * Determine if the {@link HttpResponse} gotten from the origin is a @@ -189,6 +195,14 @@ return false; } + Header[] authNHeaders = request.getHeaders("Authorization"); + if (authNHeaders != null && authNHeaders.length > 0) { + String[] authCacheableParams = { + "s-maxage", "must-revalidate", "public" + }; + return hasCacheControlParameterFrom(response, authCacheableParams); + } + String method = request.getRequestLine().getMethod(); return isResponseCacheable(method, response); } 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 958667) +++ 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,7 +55,7 @@ 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"); } @@ -64,8 +64,47 @@ public void testIsGetCacheable() { Assert.assertTrue(policy.isResponseCacheable("GET", response)); } + + @Test + public void testResponsesToRequestsWithAuthorizationHeadersAreNotCacheable() { + request = new BasicHttpRequest("GET","/",HTTP_1_1); + request.setHeader("Authorization","Basic dXNlcjpwYXNzd2Q="); + Assert.assertFalse(policy.isResponseCacheable(request,response)); + } @Test + public void testAuthorizedResponsesWithSMaxAgeAreCacheable() { + request = new BasicHttpRequest("GET","/",HTTP_1_1); + request.setHeader("Authorization","Basic dXNlcjpwYXNzd2Q="); + response.setHeader("Cache-Control","s-maxage=3600"); + Assert.assertTrue(policy.isResponseCacheable(request,response)); + } + + @Test + public void testAuthorizedResponsesWithMustRevalidateAreCacheable() { + request = new BasicHttpRequest("GET","/",HTTP_1_1); + request.setHeader("Authorization","Basic dXNlcjpwYXNzd2Q="); + response.setHeader("Cache-Control","must-revalidate"); + Assert.assertTrue(policy.isResponseCacheable(request,response)); + } + + @Test + public void testAuthorizedResponsesWithCacheControlPublicAreCacheable() { + request = new BasicHttpRequest("GET","/",HTTP_1_1); + request.setHeader("Authorization","Basic dXNlcjpwYXNzd2Q="); + response.setHeader("Cache-Control","public"); + Assert.assertTrue(policy.isResponseCacheable(request,response)); + } + + @Test + public void testAuthorizedResponsesWithCacheControlMaxAgeAreNotCacheable() { + request = new BasicHttpRequest("GET","/",HTTP_1_1); + request.setHeader("Authorization","Basic dXNlcjpwYXNzd2Q="); + response.setHeader("Cache-Control","max-age=3600"); + Assert.assertFalse(policy.isResponseCacheable(request,response)); + } + + @Test public void test203ResponseCodeIsCacheable() { response.setStatusCode(HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION); Assert.assertTrue(policy.isResponseCacheable("GET", response)); @@ -219,7 +258,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 +268,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));