View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.net.SocketTimeoutException;
32  import java.util.Arrays;
33  import java.util.Date;
34  import java.util.Iterator;
35  import java.util.Random;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  import org.apache.hc.client5.http.ClientProtocolException;
40  import org.apache.hc.client5.http.HttpRoute;
41  import org.apache.hc.client5.http.auth.StandardAuthScheme;
42  import org.apache.hc.client5.http.cache.HttpCacheEntry;
43  import org.apache.hc.client5.http.classic.ExecChain;
44  import org.apache.hc.client5.http.utils.DateUtils;
45  import org.apache.hc.core5.http.ClassicHttpRequest;
46  import org.apache.hc.core5.http.ClassicHttpResponse;
47  import org.apache.hc.core5.http.Header;
48  import org.apache.hc.core5.http.HeaderElement;
49  import org.apache.hc.core5.http.HeaderElements;
50  import org.apache.hc.core5.http.HttpHeaders;
51  import org.apache.hc.core5.http.HttpHost;
52  import org.apache.hc.core5.http.HttpStatus;
53  import org.apache.hc.core5.http.HttpVersion;
54  import org.apache.hc.core5.http.ProtocolVersion;
55  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
56  import org.apache.hc.core5.http.io.entity.StringEntity;
57  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
58  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
59  import org.apache.hc.core5.http.message.BasicHeader;
60  import org.apache.hc.core5.http.message.MessageSupport;
61  import org.apache.hc.core5.util.ByteArrayBuffer;
62  import org.easymock.Capture;
63  import org.easymock.EasyMock;
64  import org.junit.Assert;
65  import org.junit.Ignore;
66  import org.junit.Test;
67  
68  /**
69   * We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
70   * of the rules for proxies apply to us, as far as proper operation of the
71   * requests that pass through us. Generally speaking, we want to make sure that
72   * any response returned from our HttpClient.execute() methods is conditionally
73   * compliant with the rules for an HTTP/1.1 server, and that any requests we
74   * pass downstream to the backend HttpClient are are conditionally compliant
75   * with the rules for an HTTP/1.1 client.
76   */
77  public class TestProtocolRequirements extends AbstractProtocolTest {
78  
79      @Test
80      public void testCacheMissOnGETUsesOriginResponse() throws Exception {
81          EasyMock.expect(
82                  mockExecChain.proceed(
83                          eqRequest(request),
84                          EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
85          replayMocks();
86  
87          final ClassicHttpResponse result = execute(request);
88  
89          verifyMocks();
90          Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
91      }
92  
93      /*
94       * "Proxy and gateway applications need to be careful when forwarding
95       * messages in protocol versions different from that of the application.
96       * Since the protocol version indicates the protocol capability of the
97       * sender, a proxy/gateway MUST NOT send a message with a version indicator
98       * which is greater than its actual version. If a higher version request is
99       * received, the proxy/gateway MUST either downgrade the request version, or
100      * respond with an error, or switch to tunnel behavior."
101      *
102      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
103      */
104     @Test
105     public void testHigherMajorProtocolVersionsOnRequestSwitchToTunnelBehavior() throws Exception {
106 
107         // tunnel behavior: I don't muck with request or response in
108         // any way
109         request = new BasicClassicHttpRequest("GET", "/foo");
110         request.setVersion(new ProtocolVersion("HTTP", 2, 13));
111 
112         EasyMock.expect(
113                 mockExecChain.proceed(
114                         eqRequest(request),
115                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
116         replayMocks();
117 
118         final ClassicHttpResponse result = execute(request);
119 
120         verifyMocks();
121         Assert.assertSame(originResponse, result);
122     }
123 
124     @Test
125     public void testHigher1_XProtocolVersionsDowngradeTo1_1() throws Exception {
126 
127         request = new BasicClassicHttpRequest("GET", "/foo");
128         request.setVersion(new ProtocolVersion("HTTP", 1, 2));
129 
130         final ClassicHttpRequest downgraded = new BasicClassicHttpRequest("GET", "/foo");
131         downgraded.setVersion(HttpVersion.HTTP_1_1);
132 
133         EasyMock.expect(
134                 mockExecChain.proceed(
135                         eqRequest(downgraded),
136                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
137 
138         replayMocks();
139         final ClassicHttpResponse result = execute(request);
140 
141         verifyMocks();
142         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
143     }
144 
145     /*
146      * "Due to interoperability problems with HTTP/1.0 proxies discovered since
147      * the publication of RFC 2068[33], caching proxies MUST, gateways MAY, and
148      * tunnels MUST NOT upgrade the request to the highest version they support.
149      * The proxy/gateway's response to that request MUST be in the same major
150      * version as the request."
151      *
152      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
153      */
154     @Test
155     public void testRequestsWithLowerProtocolVersionsGetUpgradedTo1_1() throws Exception {
156 
157         request = new BasicClassicHttpRequest("GET", "/foo");
158         request.setVersion(new ProtocolVersion("HTTP", 1, 0));
159         final ClassicHttpRequest upgraded = new BasicClassicHttpRequest("GET", "/foo");
160         upgraded.setVersion(HttpVersion.HTTP_1_1);
161 
162         EasyMock.expect(
163                 mockExecChain.proceed(
164                         eqRequest(upgraded),
165                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
166         replayMocks();
167 
168         final ClassicHttpResponse result = execute(request);
169 
170         verifyMocks();
171         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
172     }
173 
174     /*
175      * "An HTTP server SHOULD send a response version equal to the highest
176      * version for which the server is at least conditionally compliant, and
177      * whose major version is less than or equal to the one received in the
178      * request."
179      *
180      * http://www.ietf.org/rfc/rfc2145.txt
181      */
182     @Test
183     public void testLowerOriginResponsesUpgradedToOurVersion1_1() throws Exception {
184         originResponse = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
185         originResponse.setVersion(new ProtocolVersion("HTTP", 1, 2));
186         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
187         originResponse.setHeader("Server", "MockOrigin/1.0");
188         originResponse.setEntity(body);
189 
190         // not testing this internal behavior in this test, just want
191         // to check the protocol version that comes out the other end
192         EasyMock.expect(
193                 mockExecChain.proceed(
194                         EasyMock.isA(ClassicHttpRequest.class),
195                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
196         replayMocks();
197 
198         final ClassicHttpResponse result = execute(request);
199 
200         verifyMocks();
201         Assert.assertEquals(HttpVersion.HTTP_1_1, result.getVersion());
202     }
203 
204     @Test
205     public void testResponseToA1_0RequestShouldUse1_1() throws Exception {
206         request = new BasicClassicHttpRequest("GET", "/foo");
207         request.setVersion(new ProtocolVersion("HTTP", 1, 0));
208 
209         EasyMock.expect(
210                 mockExecChain.proceed(
211                         EasyMock.isA(ClassicHttpRequest.class),
212                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
213         replayMocks();
214 
215         final ClassicHttpResponse result = execute(request);
216 
217         verifyMocks();
218         Assert.assertEquals(HttpVersion.HTTP_1_1, result.getVersion());
219     }
220 
221     /*
222      * "A proxy MUST forward an unknown header, unless it is protected by a
223      * Connection header." http://www.ietf.org/rfc/rfc2145.txt
224      */
225     @Test
226     public void testForwardsUnknownHeadersOnRequestsFromHigherProtocolVersions() throws Exception {
227         request = new BasicClassicHttpRequest("GET", "/foo");
228         request.setVersion(new ProtocolVersion("HTTP", 1, 2));
229         request.removeHeaders("Connection");
230         request.addHeader("X-Unknown-Header", "some-value");
231 
232         final ClassicHttpRequest downgraded = new BasicClassicHttpRequest("GET", "/foo");
233         downgraded.setVersion(HttpVersion.HTTP_1_1);
234         downgraded.removeHeaders("Connection");
235         downgraded.addHeader("X-Unknown-Header", "some-value");
236 
237         EasyMock.expect(
238                 mockExecChain.proceed(
239                         eqRequest(downgraded),
240                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
241         replayMocks();
242 
243         execute(request);
244 
245         verifyMocks();
246     }
247 
248     /*
249      * "A server MUST NOT send transfer-codings to an HTTP/1.0 client."
250      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6
251      */
252     @Test
253     public void testTransferCodingsAreNotSentToAnHTTP_1_0Client() throws Exception {
254 
255         originResponse.setHeader("Transfer-Encoding", "identity");
256 
257         final ClassicHttpRequest originalRequest = new BasicClassicHttpRequest("GET", "/foo");
258         originalRequest.setVersion(new ProtocolVersion("HTTP", 1, 0));
259 
260         EasyMock.expect(
261                 mockExecChain.proceed(
262                         EasyMock.isA(ClassicHttpRequest.class),
263                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
264         replayMocks();
265 
266         final ClassicHttpResponse result = execute(originalRequest);
267 
268         verifyMocks();
269 
270         Assert.assertNull(result.getFirstHeader("TE"));
271         Assert.assertNull(result.getFirstHeader("Transfer-Encoding"));
272     }
273 
274     /*
275      * "Multiple message-header fields with the same field-name MAY be present
276      * in a message if and only if the entire field-value for that header field
277      * is defined as a comma-separated list [i.e., #(values)]. It MUST be
278      * possible to combine the multiple header fields into one
279      * "field-name: field-value" pair, without changing the semantics of the
280      * message, by appending each subsequent field-value to the first, each
281      * separated by a comma. The order in which header fields with the same
282      * field-name are received is therefore significant to the interpretation of
283      * the combined field value, and thus a proxy MUST NOT change the order of
284      * these field values when a message is forwarded."
285      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
286      */
287     private void testOrderOfMultipleHeadersIsPreservedOnRequests(final String h, final ClassicHttpRequest request) throws Exception {
288         final Capture<ClassicHttpRequest> reqCapture = EasyMock.newCapture();
289 
290         EasyMock.expect(
291                 mockExecChain.proceed(
292                         EasyMock.capture(reqCapture),
293                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
294         replayMocks();
295 
296         execute(request);
297 
298         verifyMocks();
299 
300         final ClassicHttpRequest forwarded = reqCapture.getValue();
301         Assert.assertNotNull(forwarded);
302         final String expected = HttpTestUtils.getCanonicalHeaderValue(request, h);
303         final String actual = HttpTestUtils.getCanonicalHeaderValue(forwarded, h);
304         if (!actual.contains(expected)) {
305             Assert.assertEquals(expected, actual);
306         }
307 
308     }
309 
310     @Test
311     public void testOrderOfMultipleAcceptHeaderValuesIsPreservedOnRequests() throws Exception {
312         request.addHeader("Accept", "audio/*; q=0.2, audio/basic");
313         request.addHeader("Accept", "text/*, text/html, text/html;level=1, */*");
314         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept", request);
315     }
316 
317     @Test
318     public void testOrderOfMultipleAcceptCharsetHeadersIsPreservedOnRequests() throws Exception {
319         request.addHeader("Accept-Charset", "iso-8859-5");
320         request.addHeader("Accept-Charset", "unicode-1-1;q=0.8");
321         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Charset", request);
322     }
323 
324     @Test
325     public void testOrderOfMultipleAcceptEncodingHeadersIsPreservedOnRequests() throws Exception {
326         request.addHeader("Accept-Encoding", "identity");
327         request.addHeader("Accept-Encoding", "compress, gzip");
328         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
329     }
330 
331     @Test
332     public void testOrderOfMultipleAcceptLanguageHeadersIsPreservedOnRequests() throws Exception {
333         request.addHeader("Accept-Language", "da, en-gb;q=0.8, en;q=0.7");
334         request.addHeader("Accept-Language", "i-cherokee");
335         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
336     }
337 
338     @Test
339     public void testOrderOfMultipleAllowHeadersIsPreservedOnRequests() throws Exception {
340         final BasicClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
341         put.setEntity(body);
342         put.addHeader("Allow", "GET, HEAD");
343         put.addHeader("Allow", "DELETE");
344         put.addHeader("Content-Length", "128");
345         testOrderOfMultipleHeadersIsPreservedOnRequests("Allow", put);
346     }
347 
348     @Test
349     public void testOrderOfMultipleCacheControlHeadersIsPreservedOnRequests() throws Exception {
350         request.addHeader("Cache-Control", "max-age=5");
351         request.addHeader("Cache-Control", "min-fresh=10");
352         testOrderOfMultipleHeadersIsPreservedOnRequests("Cache-Control", request);
353     }
354 
355     @Test
356     public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnRequests() throws Exception {
357         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
358         post.setEntity(body);
359         post.addHeader("Content-Encoding", "gzip");
360         post.addHeader("Content-Encoding", "compress");
361         post.addHeader("Content-Length", "128");
362         testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Encoding", post);
363     }
364 
365     @Test
366     public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnRequests() throws Exception {
367         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
368         post.setEntity(body);
369         post.addHeader("Content-Language", "mi");
370         post.addHeader("Content-Language", "en");
371         post.addHeader("Content-Length", "128");
372         testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Language", post);
373     }
374 
375     @Test
376     public void testOrderOfMultipleExpectHeadersIsPreservedOnRequests() throws Exception {
377         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
378         post.setEntity(body);
379         post.addHeader("Expect", "100-continue");
380         post.addHeader("Expect", "x-expect=true");
381         post.addHeader("Content-Length", "128");
382         testOrderOfMultipleHeadersIsPreservedOnRequests("Expect", post);
383     }
384 
385     @Test
386     public void testOrderOfMultiplePragmaHeadersIsPreservedOnRequests() throws Exception {
387         request.addHeader("Pragma", "no-cache");
388         request.addHeader("Pragma", "x-pragma-1, x-pragma-2");
389         testOrderOfMultipleHeadersIsPreservedOnRequests("Pragma", request);
390     }
391 
392     @Test
393     public void testOrderOfMultipleViaHeadersIsPreservedOnRequests() throws Exception {
394         request.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
395         request.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
396         testOrderOfMultipleHeadersIsPreservedOnRequests("Via", request);
397     }
398 
399     @Test
400     public void testOrderOfMultipleWarningHeadersIsPreservedOnRequests() throws Exception {
401         request.addHeader("Warning", "199 fred \"bargle\"");
402         request.addHeader("Warning", "199 barney \"bungle\"");
403         testOrderOfMultipleHeadersIsPreservedOnRequests("Warning", request);
404     }
405 
406     private void testOrderOfMultipleHeadersIsPreservedOnResponses(final String h) throws Exception {
407         EasyMock.expect(
408                 mockExecChain.proceed(
409                         EasyMock.isA(ClassicHttpRequest.class),
410                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
411         replayMocks();
412 
413         final ClassicHttpResponse result = execute(request);
414 
415         verifyMocks();
416 
417         Assert.assertNotNull(result);
418         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse, h), HttpTestUtils
419                 .getCanonicalHeaderValue(result, h));
420 
421     }
422 
423     @Test
424     public void testOrderOfMultipleAllowHeadersIsPreservedOnResponses() throws Exception {
425         originResponse = new BasicClassicHttpResponse(405, "Method Not Allowed");
426         originResponse.addHeader("Allow", "HEAD");
427         originResponse.addHeader("Allow", "DELETE");
428         testOrderOfMultipleHeadersIsPreservedOnResponses("Allow");
429     }
430 
431     @Test
432     public void testOrderOfMultipleCacheControlHeadersIsPreservedOnResponses() throws Exception {
433         originResponse.addHeader("Cache-Control", "max-age=0");
434         originResponse.addHeader("Cache-Control", "no-store, must-revalidate");
435         testOrderOfMultipleHeadersIsPreservedOnResponses("Cache-Control");
436     }
437 
438     @Test
439     public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnResponses() throws Exception {
440         originResponse.addHeader("Content-Encoding", "gzip");
441         originResponse.addHeader("Content-Encoding", "compress");
442         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Encoding");
443     }
444 
445     @Test
446     public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnResponses() throws Exception {
447         originResponse.addHeader("Content-Language", "mi");
448         originResponse.addHeader("Content-Language", "en");
449         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Language");
450     }
451 
452     @Test
453     public void testOrderOfMultiplePragmaHeadersIsPreservedOnResponses() throws Exception {
454         originResponse.addHeader("Pragma", "no-cache, x-pragma-2");
455         originResponse.addHeader("Pragma", "x-pragma-1");
456         testOrderOfMultipleHeadersIsPreservedOnResponses("Pragma");
457     }
458 
459     @Test
460     public void testOrderOfMultipleViaHeadersIsPreservedOnResponses() throws Exception {
461         originResponse.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
462         originResponse.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
463         testOrderOfMultipleHeadersIsPreservedOnResponses("Via");
464     }
465 
466     @Test
467     public void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() throws Exception {
468         originResponse.addHeader("WWW-Authenticate", "x-challenge-1");
469         originResponse.addHeader("WWW-Authenticate", "x-challenge-2");
470         testOrderOfMultipleHeadersIsPreservedOnResponses("WWW-Authenticate");
471     }
472 
473     /*
474      * "However, applications MUST understand the class of any status code, as
475      * indicated by the first digit, and treat any unrecognized response as
476      * being equivalent to the x00 status code of that class, with the exception
477      * that an unrecognized response MUST NOT be cached."
478      *
479      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
480      */
481     private void testUnknownResponseStatusCodeIsNotCached(final int code) throws Exception {
482 
483         emptyMockCacheExpectsNoPuts();
484 
485         originResponse = new BasicClassicHttpResponse(code, "Moo");
486         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
487         originResponse.setHeader("Server", "MockOrigin/1.0");
488         originResponse.setHeader("Cache-Control", "max-age=3600");
489         originResponse.setEntity(body);
490 
491         EasyMock.expect(
492                 mockExecChain.proceed(
493                         EasyMock.isA(ClassicHttpRequest.class),
494                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
495 
496         replayMocks();
497 
498         execute(request);
499 
500         // in particular, there were no storage calls on the cache
501         verifyMocks();
502     }
503 
504     @Test
505     public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
506         for (int i = 102; i <= 199; i++) {
507             testUnknownResponseStatusCodeIsNotCached(i);
508         }
509         for (int i = 207; i <= 299; i++) {
510             testUnknownResponseStatusCodeIsNotCached(i);
511         }
512         for (int i = 308; i <= 399; i++) {
513             testUnknownResponseStatusCodeIsNotCached(i);
514         }
515         for (int i = 418; i <= 499; i++) {
516             testUnknownResponseStatusCodeIsNotCached(i);
517         }
518         for (int i = 506; i <= 999; i++) {
519             testUnknownResponseStatusCodeIsNotCached(i);
520         }
521     }
522 
523     /*
524      * "Unrecognized header fields SHOULD be ignored by the recipient and MUST
525      * be forwarded by transparent proxies."
526      *
527      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1
528      */
529     @Test
530     public void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
531         request.addHeader("X-Unknown-Header", "blahblah");
532         final Capture<ClassicHttpRequest> reqCap = EasyMock.newCapture();
533         EasyMock.expect(
534                 mockExecChain.proceed(
535                         EasyMock.capture(reqCap),
536                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
537 
538         replayMocks();
539 
540         execute(request);
541 
542         verifyMocks();
543         final ClassicHttpRequest forwarded = reqCap.getValue();
544         final Header[] hdrs = forwarded.getHeaders("X-Unknown-Header");
545         Assert.assertEquals(1, hdrs.length);
546         Assert.assertEquals("blahblah", hdrs[0].getValue());
547     }
548 
549     @Test
550     public void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
551         originResponse.addHeader("X-Unknown-Header", "blahblah");
552         EasyMock.expect(
553                 mockExecChain.proceed(
554                         EasyMock.isA(ClassicHttpRequest.class),
555                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
556 
557         replayMocks();
558 
559         final ClassicHttpResponse result = execute(request);
560 
561         verifyMocks();
562         final Header[] hdrs = result.getHeaders("X-Unknown-Header");
563         Assert.assertEquals(1, hdrs.length);
564         Assert.assertEquals("blahblah", hdrs[0].getValue());
565     }
566 
567     /*
568      * "If a client will wait for a 100 (Continue) response before sending the
569      * request body, it MUST send an Expect request-header field (section 14.20)
570      * with the '100-continue' expectation."
571      *
572      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
573      */
574     @Test
575     public void testRequestsExpecting100ContinueBehaviorShouldSetExpectHeader() throws Exception {
576         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
577         post.setHeader(HttpHeaders.EXPECT, HeaderElements.CONTINUE);
578         post.setHeader("Content-Length", "128");
579         post.setEntity(new StringEntity("whatever"));
580 
581         final Capture<ClassicHttpRequest> reqCap = EasyMock.newCapture();
582 
583         EasyMock.expect(
584                 mockExecChain.proceed(
585                         EasyMock.capture(reqCap),
586                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
587 
588         replayMocks();
589 
590         execute(post);
591 
592         verifyMocks();
593 
594         final ClassicHttpRequest forwarded = reqCap.getValue();
595         boolean foundExpect = false;
596         final Iterator<HeaderElement> it = MessageSupport.iterate(forwarded, HttpHeaders.EXPECT);
597         while (it.hasNext()) {
598             final HeaderElement elt = it.next();
599             if ("100-continue".equalsIgnoreCase(elt.getName())) {
600                 foundExpect = true;
601                 break;
602             }
603         }
604         Assert.assertTrue(foundExpect);
605     }
606 
607     /*
608      * "If a client will wait for a 100 (Continue) response before sending the
609      * request body, it MUST send an Expect request-header field (section 14.20)
610      * with the '100-continue' expectation."
611      *
612      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
613      */
614     @Test
615     public void testRequestsNotExpecting100ContinueBehaviorShouldNotSetExpectContinueHeader() throws Exception {
616         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
617         post.setHeader("Content-Length", "128");
618         post.setEntity(new StringEntity("whatever"));
619 
620         final Capture<ClassicHttpRequest> reqCap = EasyMock.newCapture();
621 
622         EasyMock.expect(
623                 mockExecChain.proceed(
624                         EasyMock.capture(reqCap),
625                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
626 
627         replayMocks();
628 
629         execute(post);
630 
631         verifyMocks();
632 
633         final ClassicHttpRequest forwarded = reqCap.getValue();
634         boolean foundExpect = false;
635         final Iterator<HeaderElement> it = MessageSupport.iterate(forwarded, HttpHeaders.EXPECT);
636         while (it.hasNext()) {
637             final HeaderElement elt = it.next();
638             if ("100-continue".equalsIgnoreCase(elt.getName())) {
639                 foundExpect = true;
640                 break;
641             }
642         }
643         Assert.assertFalse(foundExpect);
644     }
645 
646     /*
647      * "If a proxy receives a request that includes an Expect request- header
648      * field with the '100-continue' expectation, and the proxy either knows
649      * that the next-hop server complies with HTTP/1.1 or higher, or does not
650      * know the HTTP version of the next-hop server, it MUST forward the
651      * request, including the Expect header field.
652      *
653      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
654      */
655     @Test
656     public void testExpectHeadersAreForwardedOnRequests() throws Exception {
657         // This would mostly apply to us if we were part of an
658         // application that was a proxy, and would be the
659         // responsibility of the greater application. Our
660         // responsibility is to make sure that if we get an
661         // entity-enclosing request that we properly set (or unset)
662         // the Expect header per the request.expectContinue() flag,
663         // which is tested by the previous few tests.
664     }
665 
666     /*
667      * "A proxy MUST NOT forward a 100 (Continue) response if the request
668      * message was received from an HTTP/1.0 (or earlier) client and did not
669      * include an Expect request-header field with the '100-continue'
670      * expectation. This requirement overrides the general rule for forwarding
671      * of 1xx responses (see section 10.1)."
672      *
673      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
674      */
675     @Test
676     public void test100ContinueResponsesAreNotForwardedTo1_0ClientsWhoDidNotAskForThem() throws Exception {
677 
678         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
679         post.setVersion(new ProtocolVersion("HTTP", 1, 0));
680         post.setEntity(body);
681         post.setHeader("Content-Length", "128");
682 
683         originResponse = new BasicClassicHttpResponse(100, "Continue");
684         EasyMock.expect(
685                 mockExecChain.proceed(
686                         EasyMock.isA(ClassicHttpRequest.class),
687                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
688         replayMocks();
689 
690         try {
691             // if a 100 response gets up to us from the HttpClient
692             // backend, we can't really handle it at that point
693             execute(post);
694             Assert.fail("should have thrown an exception");
695         } catch (final ClientProtocolException expected) {
696         }
697 
698         verifyMocks();
699     }
700 
701     /*
702      * "9.2 OPTIONS. ...Responses to this method are not cacheable.
703      *
704      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
705      */
706     @Test
707     public void testResponsesToOPTIONSAreNotCacheable() throws Exception {
708         emptyMockCacheExpectsNoPuts();
709         request = new BasicClassicHttpRequest("OPTIONS", "/");
710         originResponse.addHeader("Cache-Control", "max-age=3600");
711 
712         EasyMock.expect(
713                 mockExecChain.proceed(
714                         EasyMock.isA(ClassicHttpRequest.class),
715                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
716 
717         replayMocks();
718 
719         execute(request);
720 
721         verifyMocks();
722     }
723 
724     /*
725      * "A 200 response SHOULD .... If no response body is included, the response
726      * MUST include a Content-Length field with a field-value of '0'."
727      *
728      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
729      */
730     @Test
731     public void test200ResponseToOPTIONSWithNoBodyShouldIncludeContentLengthZero() throws Exception {
732 
733         request = new BasicClassicHttpRequest("OPTIONS", "/");
734         originResponse.setEntity(null);
735         originResponse.setHeader("Content-Length", "0");
736 
737         EasyMock.expect(
738                 mockExecChain.proceed(
739                         EasyMock.isA(ClassicHttpRequest.class),
740                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
741         replayMocks();
742 
743         final ClassicHttpResponse result = execute(request);
744 
745         verifyMocks();
746         final Header contentLength = result.getFirstHeader("Content-Length");
747         Assert.assertNotNull(contentLength);
748         Assert.assertEquals("0", contentLength.getValue());
749     }
750 
751     /*
752      * "When a proxy receives an OPTIONS request on an absoluteURI for which
753      * request forwarding is permitted, the proxy MUST check for a Max-Forwards
754      * field. If the Max-Forwards field-value is zero ("0"), the proxy MUST NOT
755      * forward the message; instead, the proxy SHOULD respond with its own
756      * communication options."
757      *
758      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
759      */
760     @Test
761     public void testDoesNotForwardOPTIONSWhenMaxForwardsIsZeroOnAbsoluteURIRequest() throws Exception {
762         request = new BasicClassicHttpRequest("OPTIONS", "*");
763         request.setHeader("Max-Forwards", "0");
764 
765         replayMocks();
766         execute(request);
767         verifyMocks();
768     }
769 
770     /*
771      * "If the Max-Forwards field-value is an integer greater than zero, the
772      * proxy MUST decrement the field-value when it forwards the request."
773      *
774      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
775      */
776     @Test
777     public void testDecrementsMaxForwardsWhenForwardingOPTIONSRequest() throws Exception {
778 
779         request = new BasicClassicHttpRequest("OPTIONS", "*");
780         request.setHeader("Max-Forwards", "7");
781 
782         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
783 
784         EasyMock.expect(
785                 mockExecChain.proceed(
786                         EasyMock.capture(cap),
787                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
788 
789         replayMocks();
790         execute(request);
791         verifyMocks();
792 
793         final ClassicHttpRequest captured = cap.getValue();
794         Assert.assertEquals("6", captured.getFirstHeader("Max-Forwards").getValue());
795     }
796 
797     /*
798      * "If no Max-Forwards field is present in the request, then the forwarded
799      * request MUST NOT include a Max-Forwards field."
800      *
801      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
802      */
803     @Test
804     public void testDoesNotAddAMaxForwardsHeaderToForwardedOPTIONSRequests() throws Exception {
805         request = new BasicClassicHttpRequest("OPTIONS", "/");
806         final Capture<ClassicHttpRequest> reqCap = EasyMock.newCapture();
807         EasyMock.expect(
808                 mockExecChain.proceed(
809                         EasyMock.capture(reqCap),
810                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
811 
812         replayMocks();
813         execute(request);
814         verifyMocks();
815 
816         final ClassicHttpRequest forwarded = reqCap.getValue();
817         Assert.assertNull(forwarded.getFirstHeader("Max-Forwards"));
818     }
819 
820     /*
821      * "The HEAD method is identical to GET except that the server MUST NOT
822      * return a message-body in the response."
823      *
824      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
825      */
826     @Test
827     public void testResponseToAHEADRequestMustNotHaveABody() throws Exception {
828         request = new BasicClassicHttpRequest("HEAD", "/");
829         EasyMock.expect(
830                 mockExecChain.proceed(
831                         EasyMock.isA(ClassicHttpRequest.class),
832                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
833 
834         replayMocks();
835 
836         final ClassicHttpResponse result = execute(request);
837 
838         verifyMocks();
839 
840         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
841     }
842 
843     /*
844      * "If the new field values indicate that the cached entity differs from the
845      * current entity (as would be indicated by a change in Content-Length,
846      * Content-MD5, ETag or Last-Modified), then the cache MUST treat the cache
847      * entry as stale."
848      *
849      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
850      */
851     private void testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale(final String eHeader,
852             final String oldVal, final String newVal) throws Exception {
853 
854         // put something cacheable in the cache
855         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
856         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
857         resp1.addHeader("Cache-Control", "max-age=3600");
858         resp1.setHeader(eHeader, oldVal);
859 
860         // get a head that penetrates the cache
861         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("HEAD", "/");
862         req2.addHeader("Cache-Control", "no-cache");
863         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
864         resp2.setEntity(null);
865         resp2.setHeader(eHeader, newVal);
866 
867         // next request doesn't tolerate stale entry
868         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
869         req3.addHeader("Cache-Control", "max-stale=0");
870         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
871         resp3.setHeader(eHeader, newVal);
872 
873         EasyMock.expect(
874                 mockExecChain.proceed(
875                         eqRequest(req1),
876                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
877         EasyMock.expect(
878                 mockExecChain.proceed(
879                         eqRequest(req2),
880                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
881         EasyMock.expect(
882                 mockExecChain.proceed(
883                         EasyMock.isA(ClassicHttpRequest.class),
884                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp3);
885 
886         replayMocks();
887 
888         execute(req1);
889         execute(req2);
890         execute(req3);
891 
892         verifyMocks();
893     }
894 
895     @Test
896     public void testHEADResponseWithUpdatedContentLengthFieldMakeACacheEntryStale() throws Exception {
897         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-Length", "128", "127");
898     }
899 
900     @Test
901     public void testHEADResponseWithUpdatedContentMD5FieldMakeACacheEntryStale() throws Exception {
902         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-MD5",
903                 "Q2hlY2sgSW50ZWdyaXR5IQ==", "Q2hlY2sgSW50ZWdyaXR5IR==");
904 
905     }
906 
907     @Test
908     public void testHEADResponseWithUpdatedETagFieldMakeACacheEntryStale() throws Exception {
909         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("ETag", "\"etag1\"",
910                 "\"etag2\"");
911     }
912 
913     @Test
914     public void testHEADResponseWithUpdatedLastModifiedFieldMakeACacheEntryStale() throws Exception {
915         final Date now = new Date();
916         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
917         final Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L);
918         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Last-Modified", DateUtils
919                 .formatDate(tenSecondsAgo), DateUtils.formatDate(sixSecondsAgo));
920     }
921 
922     /*
923      * "9.5 POST. Responses to this method are not cacheable, unless the
924      * response includes appropriate Cache-Control or Expires header fields."
925      *
926      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
927      */
928     @Test
929     public void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws Exception {
930         emptyMockCacheExpectsNoPuts();
931 
932         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
933         post.setHeader("Content-Length", "128");
934         post.setEntity(HttpTestUtils.makeBody(128));
935 
936         originResponse.removeHeaders("Cache-Control");
937         originResponse.removeHeaders("Expires");
938 
939         EasyMock.expect(
940                 mockExecChain.proceed(
941                         EasyMock.isA(ClassicHttpRequest.class),
942                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
943 
944         replayMocks();
945 
946         execute(post);
947 
948         verifyMocks();
949     }
950 
951     /*
952      * "9.5 PUT. ...Responses to this method are not cacheable."
953      *
954      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
955      */
956     @Test
957     public void testResponsesToPUTsAreNotCached() throws Exception {
958         emptyMockCacheExpectsNoPuts();
959 
960         final BasicClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
961         put.setEntity(HttpTestUtils.makeBody(128));
962         put.addHeader("Content-Length", "128");
963 
964         originResponse.setHeader("Cache-Control", "max-age=3600");
965 
966         EasyMock.expect(
967                 mockExecChain.proceed(
968                         EasyMock.isA(ClassicHttpRequest.class),
969                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
970 
971         replayMocks();
972 
973         execute(put);
974 
975         verifyMocks();
976     }
977 
978     /*
979      * "9.6 DELETE. ... Responses to this method are not cacheable."
980      *
981      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
982      */
983     @Test
984     public void testResponsesToDELETEsAreNotCached() throws Exception {
985         emptyMockCacheExpectsNoPuts();
986 
987         request = new BasicClassicHttpRequest("DELETE", "/");
988         originResponse.setHeader("Cache-Control", "max-age=3600");
989 
990         EasyMock.expect(
991                 mockExecChain.proceed(
992                         EasyMock.isA(ClassicHttpRequest.class),
993                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
994 
995         replayMocks();
996 
997         execute(request);
998 
999         verifyMocks();
1000     }
1001 
1002     /*
1003      * "9.8 TRACE ... Responses to this method MUST NOT be cached."
1004      *
1005      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
1006      */
1007     @Test
1008     public void testResponsesToTRACEsAreNotCached() throws Exception {
1009         emptyMockCacheExpectsNoPuts();
1010 
1011         request = new BasicClassicHttpRequest("TRACE", "/");
1012         originResponse.setHeader("Cache-Control", "max-age=3600");
1013 
1014         EasyMock.expect(
1015                 mockExecChain.proceed(
1016                         EasyMock.isA(ClassicHttpRequest.class),
1017                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1018 
1019         replayMocks();
1020 
1021         execute(request);
1022 
1023         verifyMocks();
1024     }
1025 
1026     /*
1027      * "The 204 response MUST NOT include a message-body, and thus is always
1028      * terminated by the first empty line after the header fields."
1029      *
1030      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
1031      */
1032     @Test
1033     public void test204ResponsesDoNotContainMessageBodies() throws Exception {
1034         originResponse = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1035         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1036 
1037         EasyMock.expect(
1038                 mockExecChain.proceed(
1039                         EasyMock.isA(ClassicHttpRequest.class),
1040                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1041 
1042         replayMocks();
1043 
1044         final ClassicHttpResponse result = execute(request);
1045 
1046         verifyMocks();
1047     }
1048 
1049     /*
1050      * "The [206] response MUST include the following header fields:
1051      *
1052      * - Either a Content-Range header field (section 14.16) indicating the
1053      * range included with this response, or a multipart/byteranges Content-Type
1054      * including Content-Range fields for each part. If a Content-Length header
1055      * field is present in the response, its value MUST match the actual number
1056      * of OCTETs transmitted in the message-body.
1057      *
1058      * - Date
1059      *
1060      * - ETag and/or Content-Location, if the header would have been sent in a
1061      * 200 response to the same request
1062      *
1063      * - Expires, Cache-Control, and/or Vary, if the field-value might differ
1064      * from that sent in any previous response for the same variant"
1065      *
1066      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1067      */
1068     @Test
1069     public void test206ResponseGeneratedFromCacheMustHaveContentRangeOrMultipartByteRangesContentType() throws Exception {
1070 
1071         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1072         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1073         resp1.setHeader("ETag", "\"etag\"");
1074         resp1.setHeader("Cache-Control", "max-age=3600");
1075 
1076         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1077         req2.setHeader("Range", "bytes=0-50");
1078 
1079         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1080 
1081         replayMocks();
1082         execute(req1);
1083         final ClassicHttpResponse result = execute(req2);
1084         verifyMocks();
1085 
1086         if (HttpStatus.SC_PARTIAL_CONTENT == result.getCode()) {
1087             if (result.getFirstHeader("Content-Range") == null) {
1088                 final HeaderElement elt = MessageSupport.parse(result.getFirstHeader("Content-Type"))[0];
1089                 Assert.assertTrue("multipart/byteranges".equalsIgnoreCase(elt.getName()));
1090                 Assert.assertNotNull(elt.getParameterByName("boundary"));
1091                 Assert.assertNotNull(elt.getParameterByName("boundary").getValue());
1092                 Assert.assertFalse("".equals(elt.getParameterByName("boundary").getValue().trim()));
1093             }
1094         }
1095     }
1096 
1097     @Test
1098     public void test206ResponseGeneratedFromCacheMustHaveABodyThatMatchesContentLengthHeaderIfPresent() throws Exception {
1099 
1100         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1101         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1102         resp1.setHeader("ETag", "\"etag\"");
1103         resp1.setHeader("Cache-Control", "max-age=3600");
1104 
1105         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1106         req2.setHeader("Range", "bytes=0-50");
1107 
1108         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1109 
1110         replayMocks();
1111         execute(req1);
1112         final ClassicHttpResponse result = execute(req2);
1113         verifyMocks();
1114 
1115         if (HttpStatus.SC_PARTIAL_CONTENT == result.getCode()) {
1116             final Header h = result.getFirstHeader("Content-Length");
1117             if (h != null) {
1118                 final int contentLength = Integer.parseInt(h.getValue());
1119                 int bytesRead = 0;
1120                 final InputStream i = result.getEntity().getContent();
1121                 while ((i.read()) != -1) {
1122                     bytesRead++;
1123                 }
1124                 i.close();
1125                 Assert.assertEquals(contentLength, bytesRead);
1126             }
1127         }
1128     }
1129 
1130     @Test
1131     public void test206ResponseGeneratedFromCacheMustHaveDateHeader() throws Exception {
1132         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1133         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1134         resp1.setHeader("ETag", "\"etag\"");
1135         resp1.setHeader("Cache-Control", "max-age=3600");
1136 
1137         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1138         req2.setHeader("Range", "bytes=0-50");
1139 
1140         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1141 
1142         replayMocks();
1143         execute(req1);
1144         final ClassicHttpResponse result = execute(req2);
1145         verifyMocks();
1146 
1147         if (HttpStatus.SC_PARTIAL_CONTENT == result.getCode()) {
1148             Assert.assertNotNull(result.getFirstHeader("Date"));
1149         }
1150     }
1151 
1152     @Test
1153     public void test206ResponseReturnedToClientMustHaveDateHeader() throws Exception {
1154         request.addHeader("Range", "bytes=0-50");
1155         originResponse = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
1156         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1157         originResponse.setHeader("Server", "MockOrigin/1.0");
1158         originResponse.setEntity(HttpTestUtils.makeBody(500));
1159         originResponse.setHeader("Content-Range", "bytes 0-499/1234");
1160         originResponse.removeHeaders("Date");
1161 
1162         EasyMock.expect(
1163                 mockExecChain.proceed(
1164                         EasyMock.isA(ClassicHttpRequest.class),
1165                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1166 
1167         replayMocks();
1168 
1169         final ClassicHttpResponse result = execute(request);
1170         Assert.assertTrue(result.getCode() != HttpStatus.SC_PARTIAL_CONTENT
1171                 || result.getFirstHeader("Date") != null);
1172 
1173         verifyMocks();
1174     }
1175 
1176     @Test
1177     public void test206ContainsETagIfA200ResponseWouldHaveIncludedIt() throws Exception {
1178         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1179 
1180         originResponse.addHeader("Cache-Control", "max-age=3600");
1181         originResponse.addHeader("ETag", "\"etag1\"");
1182 
1183         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1184         req2.addHeader("Range", "bytes=0-50");
1185 
1186         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1187 
1188         replayMocks();
1189 
1190         execute(req1);
1191         final ClassicHttpResponse result = execute(req2);
1192 
1193         verifyMocks();
1194 
1195         if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1196             Assert.assertNotNull(result.getFirstHeader("ETag"));
1197         }
1198     }
1199 
1200     @Test
1201     public void test206ContainsContentLocationIfA200ResponseWouldHaveIncludedIt() throws Exception {
1202         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1203 
1204         originResponse.addHeader("Cache-Control", "max-age=3600");
1205         originResponse.addHeader("Content-Location", "http://foo.example.com/other/url");
1206 
1207         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1208         req2.addHeader("Range", "bytes=0-50");
1209 
1210         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1211 
1212         replayMocks();
1213 
1214         execute(req1);
1215         final ClassicHttpResponse result = execute(req2);
1216 
1217         verifyMocks();
1218 
1219         if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1220             Assert.assertNotNull(result.getFirstHeader("Content-Location"));
1221         }
1222     }
1223 
1224     @Test
1225     public void test206ResponseIncludesVariantHeadersIfValueMightDiffer() throws Exception {
1226 
1227         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1228         req1.addHeader("Accept-Encoding", "gzip");
1229 
1230         final Date now = new Date();
1231         final Date inOneHour = new Date(now.getTime() + 3600 * 1000L);
1232         originResponse.addHeader("Cache-Control", "max-age=3600");
1233         originResponse.addHeader("Expires", DateUtils.formatDate(inOneHour));
1234         originResponse.addHeader("Vary", "Accept-Encoding");
1235 
1236         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1237         req2.addHeader("Cache-Control", "no-cache");
1238         req2.addHeader("Accept-Encoding", "gzip");
1239         final Date nextSecond = new Date(now.getTime() + 1000L);
1240         final Date inTwoHoursPlusASec = new Date(now.getTime() + 2 * 3600 * 1000L + 1000L);
1241 
1242         final ClassicHttpResponse originResponse2 = HttpTestUtils.make200Response();
1243         originResponse2.setHeader("Date", DateUtils.formatDate(nextSecond));
1244         originResponse2.setHeader("Cache-Control", "max-age=7200");
1245         originResponse2.setHeader("Expires", DateUtils.formatDate(inTwoHoursPlusASec));
1246         originResponse2.setHeader("Vary", "Accept-Encoding");
1247 
1248         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1249         req3.addHeader("Range", "bytes=0-50");
1250         req3.addHeader("Accept-Encoding", "gzip");
1251 
1252         backendExpectsAnyRequest().andReturn(originResponse);
1253         backendExpectsAnyRequestAndReturn(originResponse2).times(1, 2);
1254 
1255         replayMocks();
1256 
1257         execute(req1);
1258         execute(req2);
1259         final ClassicHttpResponse result = execute(req3);
1260 
1261         verifyMocks();
1262 
1263         if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1264             Assert.assertNotNull(result.getFirstHeader("Expires"));
1265             Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
1266             Assert.assertNotNull(result.getFirstHeader("Vary"));
1267         }
1268     }
1269 
1270     /*
1271      * "If the [206] response is the result of an If-Range request that used a
1272      * weak validator, the response MUST NOT include other entity-headers; this
1273      * prevents inconsistencies between cached entity-bodies and updated
1274      * headers."
1275      *
1276      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1277      */
1278     @Test
1279     public void test206ResponseToConditionalRangeRequestDoesNotIncludeOtherEntityHeaders() throws Exception {
1280 
1281         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1282 
1283         final Date now = new Date();
1284         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
1285         originResponse = HttpTestUtils.make200Response();
1286         originResponse.addHeader("Allow", "GET,HEAD");
1287         originResponse.addHeader("Cache-Control", "max-age=3600");
1288         originResponse.addHeader("Content-Language", "en");
1289         originResponse.addHeader("Content-Encoding", "x-coding");
1290         originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1291         originResponse.addHeader("Content-Length", "128");
1292         originResponse.addHeader("Content-Type", "application/octet-stream");
1293         originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1294         originResponse.addHeader("ETag", "W/\"weak-tag\"");
1295 
1296         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1297         req2.addHeader("If-Range", "W/\"weak-tag\"");
1298         req2.addHeader("Range", "bytes=0-50");
1299 
1300         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1301 
1302         replayMocks();
1303 
1304         execute(req1);
1305         final ClassicHttpResponse result = execute(req2);
1306 
1307         verifyMocks();
1308 
1309         if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1310             Assert.assertNull(result.getFirstHeader("Allow"));
1311             Assert.assertNull(result.getFirstHeader("Content-Encoding"));
1312             Assert.assertNull(result.getFirstHeader("Content-Language"));
1313             Assert.assertNull(result.getFirstHeader("Content-MD5"));
1314             Assert.assertNull(result.getFirstHeader("Last-Modified"));
1315         }
1316     }
1317 
1318     /*
1319      * "Otherwise, the [206] response MUST include all of the entity-headers
1320      * that would have been returned with a 200 (OK) response to the same
1321      * [If-Range] request."
1322      *
1323      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1324      */
1325     @Test
1326     public void test206ResponseToIfRangeWithStrongValidatorReturnsAllEntityHeaders() throws Exception {
1327 
1328         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1329 
1330         final Date now = new Date();
1331         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
1332         originResponse.addHeader("Allow", "GET,HEAD");
1333         originResponse.addHeader("Cache-Control", "max-age=3600");
1334         originResponse.addHeader("Content-Language", "en");
1335         originResponse.addHeader("Content-Encoding", "x-coding");
1336         originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1337         originResponse.addHeader("Content-Length", "128");
1338         originResponse.addHeader("Content-Type", "application/octet-stream");
1339         originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1340         originResponse.addHeader("ETag", "\"strong-tag\"");
1341 
1342         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1343         req2.addHeader("If-Range", "\"strong-tag\"");
1344         req2.addHeader("Range", "bytes=0-50");
1345 
1346         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1347 
1348         replayMocks();
1349 
1350         execute(req1);
1351         final ClassicHttpResponse result = execute(req2);
1352 
1353         verifyMocks();
1354 
1355         if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1356             Assert.assertEquals("GET,HEAD", result.getFirstHeader("Allow").getValue());
1357             Assert.assertEquals("max-age=3600", result.getFirstHeader("Cache-Control").getValue());
1358             Assert.assertEquals("en", result.getFirstHeader("Content-Language").getValue());
1359             Assert.assertEquals("x-coding", result.getFirstHeader("Content-Encoding").getValue());
1360             Assert.assertEquals("Q2hlY2sgSW50ZWdyaXR5IQ==", result.getFirstHeader("Content-MD5")
1361                     .getValue());
1362             Assert.assertEquals(originResponse.getFirstHeader("Last-Modified").getValue(), result
1363                     .getFirstHeader("Last-Modified").getValue());
1364         }
1365     }
1366 
1367     /*
1368      * "A cache MUST NOT combine a 206 response with other previously cached
1369      * content if the ETag or Last-Modified headers do not match exactly, see
1370      * 13.5.4."
1371      *
1372      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1373      */
1374     @Test
1375     public void test206ResponseIsNotCombinedWithPreviousContentIfETagDoesNotMatch() throws Exception {
1376 
1377         final Date now = new Date();
1378 
1379         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1380         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1381         resp1.setHeader("Cache-Control", "max-age=3600");
1382         resp1.setHeader("ETag", "\"etag1\"");
1383         final byte[] bytes1 = new byte[128];
1384         Arrays.fill(bytes1, (byte) 1);
1385         resp1.setEntity(new ByteArrayEntity(bytes1, null));
1386 
1387         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1388         req2.setHeader("Cache-Control", "no-cache");
1389         req2.setHeader("Range", "bytes=0-50");
1390 
1391         final Date inOneSecond = new Date(now.getTime() + 1000L);
1392         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT,
1393                 "Partial Content");
1394         resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
1395         resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
1396         resp2.setHeader("ETag", "\"etag2\"");
1397         resp2.setHeader("Content-Range", "bytes 0-50/128");
1398         final byte[] bytes2 = new byte[51];
1399         Arrays.fill(bytes2, (byte) 2);
1400         resp2.setEntity(new ByteArrayEntity(bytes2, null));
1401 
1402         final Date inTwoSeconds = new Date(now.getTime() + 2000L);
1403         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1404         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1405         resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
1406         resp3.setHeader("Cache-Control", "max-age=3600");
1407         resp3.setHeader("ETag", "\"etag2\"");
1408         final byte[] bytes3 = new byte[128];
1409         Arrays.fill(bytes3, (byte) 2);
1410         resp3.setEntity(new ByteArrayEntity(bytes3, null));
1411 
1412         EasyMock.expect(
1413                 mockExecChain.proceed(
1414                         EasyMock.isA(ClassicHttpRequest.class),
1415                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1);
1416         EasyMock.expect(
1417                 mockExecChain.proceed(
1418                         EasyMock.isA(ClassicHttpRequest.class),
1419                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2);
1420         EasyMock.expect(
1421                 mockExecChain.proceed(
1422                         EasyMock.isA(ClassicHttpRequest.class),
1423                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp3).times(0, 1);
1424         replayMocks();
1425 
1426         execute(req1);
1427         execute(req2);
1428         final ClassicHttpResponse result = execute(req3);
1429 
1430         verifyMocks();
1431 
1432         final InputStream i = result.getEntity().getContent();
1433         int b;
1434         boolean found1 = false;
1435         boolean found2 = false;
1436         while ((b = i.read()) != -1) {
1437             if (b == 1) {
1438                 found1 = true;
1439             }
1440             if (b == 2) {
1441                 found2 = true;
1442             }
1443         }
1444         i.close();
1445         Assert.assertFalse(found1 && found2); // mixture of content
1446     }
1447 
1448     @Test
1449     public void test206ResponseIsNotCombinedWithPreviousContentIfLastModifiedDoesNotMatch() throws Exception {
1450 
1451         final Date now = new Date();
1452 
1453         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1454         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1455         final Date oneHourAgo = new Date(now.getTime() - 3600L);
1456         resp1.setHeader("Cache-Control", "max-age=3600");
1457         resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1458         final byte[] bytes1 = new byte[128];
1459         Arrays.fill(bytes1, (byte) 1);
1460         resp1.setEntity(new ByteArrayEntity(bytes1, null));
1461 
1462         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1463         req2.setHeader("Cache-Control", "no-cache");
1464         req2.setHeader("Range", "bytes=0-50");
1465 
1466         final Date inOneSecond = new Date(now.getTime() + 1000L);
1467         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT,
1468                 "Partial Content");
1469         resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
1470         resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
1471         resp2.setHeader("Last-Modified", DateUtils.formatDate(now));
1472         resp2.setHeader("Content-Range", "bytes 0-50/128");
1473         final byte[] bytes2 = new byte[51];
1474         Arrays.fill(bytes2, (byte) 2);
1475         resp2.setEntity(new ByteArrayEntity(bytes2, null));
1476 
1477         final Date inTwoSeconds = new Date(now.getTime() + 2000L);
1478         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1479         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1480         resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
1481         resp3.setHeader("Cache-Control", "max-age=3600");
1482         resp3.setHeader("ETag", "\"etag2\"");
1483         final byte[] bytes3 = new byte[128];
1484         Arrays.fill(bytes3, (byte) 2);
1485         resp3.setEntity(new ByteArrayEntity(bytes3, null));
1486 
1487         EasyMock.expect(
1488                 mockExecChain.proceed(
1489                         EasyMock.isA(ClassicHttpRequest.class),
1490                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1);
1491         EasyMock.expect(
1492                 mockExecChain.proceed(
1493                         EasyMock.isA(ClassicHttpRequest.class),
1494                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2);
1495         EasyMock.expect(
1496                 mockExecChain.proceed(
1497                         EasyMock.isA(ClassicHttpRequest.class),
1498                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp3).times(0, 1);
1499         replayMocks();
1500 
1501         execute(req1);
1502         execute(req2);
1503         final ClassicHttpResponse result = execute(req3);
1504 
1505         verifyMocks();
1506 
1507         final InputStream i = result.getEntity().getContent();
1508         int b;
1509         boolean found1 = false;
1510         boolean found2 = false;
1511         while ((b = i.read()) != -1) {
1512             if (b == 1) {
1513                 found1 = true;
1514             }
1515             if (b == 2) {
1516                 found2 = true;
1517             }
1518         }
1519         i.close();
1520         Assert.assertFalse(found1 && found2); // mixture of content
1521     }
1522 
1523     /*
1524      * "A cache that does not support the Range and Content-Range headers MUST
1525      * NOT cache 206 (Partial) responses."
1526      *
1527      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1528      */
1529     @Test
1530     public void test206ResponsesAreNotCachedIfTheCacheDoesNotSupportRangeAndContentRangeHeaders() throws Exception {
1531 
1532         if (!supportsRangeAndContentRangeHeaders(impl)) {
1533             emptyMockCacheExpectsNoPuts();
1534 
1535             request = new BasicClassicHttpRequest("GET", "/");
1536             request.addHeader("Range", "bytes=0-50");
1537 
1538             originResponse = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT,"Partial Content");
1539             originResponse.setHeader("Content-Range", "bytes 0-50/128");
1540             originResponse.setHeader("Cache-Control", "max-age=3600");
1541             final byte[] bytes = new byte[51];
1542             new Random().nextBytes(bytes);
1543             originResponse.setEntity(new ByteArrayEntity(bytes, null));
1544 
1545             EasyMock.expect(
1546                     mockExecChain.proceed(
1547                             EasyMock.isA(ClassicHttpRequest.class),
1548                             EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1549 
1550             replayMocks();
1551             execute(request);
1552             verifyMocks();
1553         }
1554     }
1555 
1556     /*
1557      * "10.3.4 303 See Other ... The 303 response MUST NOT be cached, but the
1558      * response to the second (redirected) request might be cacheable."
1559      *
1560      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
1561      */
1562     @Test
1563     public void test303ResponsesAreNotCached() throws Exception {
1564         emptyMockCacheExpectsNoPuts();
1565 
1566         request = new BasicClassicHttpRequest("GET", "/");
1567 
1568         originResponse = new BasicClassicHttpResponse(HttpStatus.SC_SEE_OTHER, "See Other");
1569         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1570         originResponse.setHeader("Server", "MockServer/1.0");
1571         originResponse.setHeader("Cache-Control", "max-age=3600");
1572         originResponse.setHeader("Content-Type", "application/x-cachingclient-test");
1573         originResponse.setHeader("Location", "http://foo.example.com/other");
1574         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1575 
1576         EasyMock.expect(
1577                 mockExecChain.proceed(
1578                         EasyMock.isA(ClassicHttpRequest.class),
1579                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1580 
1581         replayMocks();
1582         execute(request);
1583         verifyMocks();
1584     }
1585 
1586     /*
1587      * "The 304 response MUST NOT contain a message-body, and thus is always
1588      * terminated by the first empty line after the header fields."
1589      *
1590      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1591      */
1592     @Test
1593     public void test304ResponseDoesNotContainABody() throws Exception {
1594         request.setHeader("If-None-Match", "\"etag\"");
1595 
1596         originResponse = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED,"Not Modified");
1597         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1598         originResponse.setHeader("Server", "MockServer/1.0");
1599         originResponse.setHeader("Content-Length", "128");
1600         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1601 
1602         EasyMock.expect(
1603                 mockExecChain.proceed(
1604                         EasyMock.isA(ClassicHttpRequest.class),
1605                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1606 
1607         replayMocks();
1608 
1609         final ClassicHttpResponse result = execute(request);
1610 
1611         verifyMocks();
1612     }
1613 
1614     /*
1615      * "The [304] response MUST include the following header fields: - Date,
1616      * unless its omission is required by section 14.18.1 [clockless origin
1617      * servers]."
1618      *
1619      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1620      */
1621     @Test
1622     public void test304ResponseWithDateHeaderForwardedFromOriginIncludesDateHeader() throws Exception {
1623 
1624         request.setHeader("If-None-Match", "\"etag\"");
1625 
1626         originResponse = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED,"Not Modified");
1627         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1628         originResponse.setHeader("Server", "MockServer/1.0");
1629         originResponse.setHeader("ETag", "\"etag\"");
1630 
1631         EasyMock.expect(
1632                 mockExecChain.proceed(
1633                         EasyMock.isA(ClassicHttpRequest.class),
1634                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1635         replayMocks();
1636 
1637         final ClassicHttpResponse result = execute(request);
1638 
1639         verifyMocks();
1640         Assert.assertNotNull(result.getFirstHeader("Date"));
1641     }
1642 
1643     @Test
1644     public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
1645 
1646         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1647         originResponse.setHeader("Cache-Control", "max-age=3600");
1648         originResponse.setHeader("ETag", "\"etag\"");
1649 
1650         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1651         req2.setHeader("If-None-Match", "\"etag\"");
1652 
1653         EasyMock.expect(
1654                 mockExecChain.proceed(
1655                         EasyMock.isA(ClassicHttpRequest.class),
1656                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse).times(1, 2);
1657         replayMocks();
1658 
1659         execute(req1);
1660         final ClassicHttpResponse result = execute(req2);
1661 
1662         verifyMocks();
1663         if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
1664             Assert.assertNotNull(result.getFirstHeader("Date"));
1665         }
1666     }
1667 
1668     /*
1669      * "The [304] response MUST include the following header fields: - ETag
1670      * and/or Content-Location, if the header would have been sent in a 200
1671      * response to the same request."
1672      *
1673      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1674      */
1675     @Test
1676     public void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
1677         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1678         originResponse.setHeader("Cache-Control", "max-age=3600");
1679         originResponse.setHeader("ETag", "\"etag\"");
1680 
1681         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1682         req2.setHeader("If-None-Match", "\"etag\"");
1683 
1684         EasyMock.expect(
1685                 mockExecChain.proceed(
1686                         EasyMock.isA(ClassicHttpRequest.class),
1687                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse).times(1, 2);
1688         replayMocks();
1689 
1690         execute(req1);
1691         final ClassicHttpResponse result = execute(req2);
1692 
1693         verifyMocks();
1694         if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
1695             Assert.assertNotNull(result.getFirstHeader("ETag"));
1696         }
1697     }
1698 
1699     @Test
1700     public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid() throws Exception {
1701         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1702         originResponse.setHeader("Cache-Control", "max-age=3600");
1703         originResponse.setHeader("Content-Location", "http://foo.example.com/other");
1704         originResponse.setHeader("ETag", "\"etag\"");
1705 
1706         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1707         req2.setHeader("If-None-Match", "\"etag\"");
1708 
1709         EasyMock.expect(
1710                 mockExecChain.proceed(
1711                         EasyMock.isA(ClassicHttpRequest.class),
1712                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse).times(1, 2);
1713         replayMocks();
1714 
1715         execute(req1);
1716         final ClassicHttpResponse result = execute(req2);
1717 
1718         verifyMocks();
1719         if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
1720             Assert.assertNotNull(result.getFirstHeader("Content-Location"));
1721         }
1722     }
1723 
1724     /*
1725      * "The [304] response MUST include the following header fields: ... -
1726      * Expires, Cache-Control, and/or Vary, if the field-value might differ from
1727      * that sent in any previous response for the same variant
1728      *
1729      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1730      */
1731     @Test
1732     public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer() throws Exception {
1733 
1734         final Date now = new Date();
1735         final Date inTwoHours = new Date(now.getTime() + 2 * 3600 * 1000L);
1736 
1737         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1738         req1.setHeader("Accept-Encoding", "gzip");
1739 
1740         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1741         resp1.setHeader("ETag", "\"v1\"");
1742         resp1.setHeader("Cache-Control", "max-age=7200");
1743         resp1.setHeader("Expires", DateUtils.formatDate(inTwoHours));
1744         resp1.setHeader("Vary", "Accept-Encoding");
1745         resp1.setEntity(HttpTestUtils.makeBody(entityLength));
1746 
1747         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1748         req1.setHeader("Accept-Encoding", "gzip");
1749         req1.setHeader("Cache-Control", "no-cache");
1750 
1751         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1752         resp2.setHeader("ETag", "\"v2\"");
1753         resp2.setHeader("Cache-Control", "max-age=3600");
1754         resp2.setHeader("Expires", DateUtils.formatDate(inTwoHours));
1755         resp2.setHeader("Vary", "Accept-Encoding");
1756         resp2.setEntity(HttpTestUtils.makeBody(entityLength));
1757 
1758         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1759         req3.setHeader("Accept-Encoding", "gzip");
1760         req3.setHeader("If-None-Match", "\"v2\"");
1761 
1762         EasyMock.expect(
1763                 mockExecChain.proceed(
1764                         EasyMock.isA(ClassicHttpRequest.class),
1765                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1);
1766         EasyMock.expect(
1767                 mockExecChain.proceed(
1768                         EasyMock.isA(ClassicHttpRequest.class),
1769                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2).times(1, 2);
1770         replayMocks();
1771 
1772         execute(req1);
1773         execute(req2);
1774         final ClassicHttpResponse result = execute(req3);
1775 
1776         verifyMocks();
1777 
1778         if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
1779             Assert.assertNotNull(result.getFirstHeader("Expires"));
1780             Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
1781             Assert.assertNotNull(result.getFirstHeader("Vary"));
1782         }
1783     }
1784 
1785     /*
1786      * "Otherwise (i.e., the conditional GET used a weak validator), the
1787      * response MUST NOT include other entity-headers; this prevents
1788      * inconsistencies between cached entity-bodies and updated headers."
1789      *
1790      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1791      */
1792     @Test
1793     public void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHeaders() throws Exception {
1794 
1795         final Date now = new Date();
1796         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
1797 
1798         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1799 
1800         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1801         resp1.setHeader("ETag", "W/\"v1\"");
1802         resp1.setHeader("Allow", "GET,HEAD");
1803         resp1.setHeader("Content-Encoding", "x-coding");
1804         resp1.setHeader("Content-Language", "en");
1805         resp1.setHeader("Content-Length", "128");
1806         resp1.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1807         resp1.setHeader("Content-Type", "application/octet-stream");
1808         resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1809         resp1.setHeader("Cache-Control", "max-age=7200");
1810 
1811         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1812         req2.setHeader("If-None-Match", "W/\"v1\"");
1813 
1814         EasyMock.expect(
1815                 mockExecChain.proceed(
1816                         EasyMock.isA(ClassicHttpRequest.class),
1817                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1).times(1, 2);
1818         replayMocks();
1819 
1820         execute(req1);
1821         final ClassicHttpResponse result = execute(req2);
1822 
1823         verifyMocks();
1824 
1825         if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
1826             Assert.assertNull(result.getFirstHeader("Allow"));
1827             Assert.assertNull(result.getFirstHeader("Content-Encoding"));
1828             Assert.assertNull(result.getFirstHeader("Content-Length"));
1829             Assert.assertNull(result.getFirstHeader("Content-MD5"));
1830             Assert.assertNull(result.getFirstHeader("Content-Type"));
1831             Assert.assertNull(result.getFirstHeader("Last-Modified"));
1832         }
1833     }
1834 
1835     /*
1836      * "If a 304 response indicates an entity not currently cached, then the
1837      * cache MUST disregard the response and repeat the request without the
1838      * conditional."
1839      *
1840      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1841      */
1842     @Test
1843     public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET() throws Exception {
1844 
1845         final Date now = new Date();
1846 
1847         // load cache with cacheable entry
1848         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1849         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1850         resp1.setHeader("ETag", "\"etag1\"");
1851         resp1.setHeader("Cache-Control", "max-age=3600");
1852 
1853         // force a revalidation
1854         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1855         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
1856 
1857         // updated ETag provided to a conditional revalidation
1858         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED,
1859                 "Not Modified");
1860         resp2.setHeader("Date", DateUtils.formatDate(now));
1861         resp2.setHeader("Server", "MockServer/1.0");
1862         resp2.setHeader("ETag", "\"etag2\"");
1863 
1864         // conditional validation uses If-None-Match
1865         final ClassicHttpRequest conditionalValidation = new BasicClassicHttpRequest("GET", "/");
1866         conditionalValidation.setHeader("If-None-Match", "\"etag1\"");
1867 
1868         // unconditional validation doesn't use If-None-Match
1869         final ClassicHttpRequest unconditionalValidation = new BasicClassicHttpRequest("GET", "/");
1870         // new response to unconditional validation provides new body
1871         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1872         resp1.setHeader("ETag", "\"etag2\"");
1873         resp1.setHeader("Cache-Control", "max-age=3600");
1874 
1875         EasyMock.expect(
1876                 mockExecChain.proceed(
1877                         EasyMock.isA(ClassicHttpRequest.class),
1878                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1);
1879         // this next one will happen once if the cache tries to
1880         // conditionally validate, zero if it goes full revalidation
1881         EasyMock.expect(
1882                 mockExecChain.proceed(
1883                         eqRequest(conditionalValidation),
1884                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2).times(0, 1);
1885         EasyMock.expect(
1886                 mockExecChain.proceed(
1887                         eqRequest(unconditionalValidation),
1888                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp3);
1889         replayMocks();
1890 
1891         execute(req1);
1892         execute(req2);
1893 
1894         verifyMocks();
1895     }
1896 
1897     /*
1898      * "If a cache uses a received 304 response to processChallenge a cache entry, the
1899      * cache MUST processChallenge the entry to reflect any new field values given in the
1900      * response.
1901      *
1902      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1903      */
1904     @Test
1905     public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
1906 
1907         final Date now = new Date();
1908         final Date inFiveSeconds = new Date(now.getTime() + 5000L);
1909 
1910         final ClassicHttpRequest initialRequest = new BasicClassicHttpRequest("GET", "/");
1911 
1912         final ClassicHttpResponse cachedResponse = HttpTestUtils.make200Response();
1913         cachedResponse.setHeader("Cache-Control", "max-age=3600");
1914         cachedResponse.setHeader("ETag", "\"etag\"");
1915 
1916         final ClassicHttpRequest secondRequest = new BasicClassicHttpRequest("GET", "/");
1917         secondRequest.setHeader("Cache-Control", "max-age=0,max-stale=0");
1918 
1919         final ClassicHttpRequest conditionalValidationRequest = new BasicClassicHttpRequest("GET", "/");
1920         conditionalValidationRequest.setHeader("If-None-Match", "\"etag\"");
1921 
1922         final ClassicHttpRequest unconditionalValidationRequest = new BasicClassicHttpRequest("GET", "/");
1923 
1924         // to be used if the cache generates a conditional validation
1925         final ClassicHttpResponse conditionalResponse = HttpTestUtils.make304Response();
1926         conditionalResponse.setHeader("Date", DateUtils.formatDate(inFiveSeconds));
1927         conditionalResponse.setHeader("Server", "MockUtils/1.0");
1928         conditionalResponse.setHeader("ETag", "\"etag\"");
1929         conditionalResponse.setHeader("X-Extra", "junk");
1930 
1931         // to be used if the cache generates an unconditional validation
1932         final ClassicHttpResponse unconditionalResponse = HttpTestUtils.make200Response();
1933         unconditionalResponse.setHeader("Date", DateUtils.formatDate(inFiveSeconds));
1934         unconditionalResponse.setHeader("ETag", "\"etag\"");
1935 
1936         final Capture<ClassicHttpRequest> cap1 = EasyMock.newCapture();
1937         final Capture<ClassicHttpRequest> cap2 = EasyMock.newCapture();
1938 
1939         EasyMock.expect(
1940                 mockExecChain.proceed(
1941                         EasyMock.isA(ClassicHttpRequest.class),
1942                         EasyMock.isA(ExecChain.Scope.class))).andReturn(cachedResponse);
1943         EasyMock.expect(
1944                 mockExecChain.proceed(
1945                         EasyMock.and(eqRequest(conditionalValidationRequest), EasyMock.capture(cap1)),
1946                         EasyMock.isA(ExecChain.Scope.class))).andReturn(conditionalResponse).times(0, 1);
1947         EasyMock.expect(
1948                 mockExecChain.proceed(
1949                         EasyMock.and(eqRequest(unconditionalValidationRequest), EasyMock.capture(cap2)),
1950                         EasyMock.isA(ExecChain.Scope.class))).andReturn(unconditionalResponse).times(0, 1);
1951 
1952         replayMocks();
1953 
1954         execute(initialRequest);
1955         final ClassicHttpResponse result = execute(secondRequest);
1956 
1957         verifyMocks();
1958 
1959         Assert.assertTrue((cap1.hasCaptured() && !cap2.hasCaptured())
1960                 || (!cap1.hasCaptured() && cap2.hasCaptured()));
1961 
1962         if (cap1.hasCaptured()) {
1963             Assert.assertEquals(DateUtils.formatDate(inFiveSeconds), result.getFirstHeader("Date")
1964                     .getValue());
1965             Assert.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
1966         }
1967     }
1968 
1969     /*
1970      * "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
1971      * header field (section 14.47) containing a challenge applicable to the
1972      * requested resource."
1973      *
1974      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
1975      */
1976     @Test
1977     public void testMustIncludeWWWAuthenticateHeaderOnAnOrigin401Response() throws Exception {
1978         originResponse = new BasicClassicHttpResponse(401, "Unauthorized");
1979         originResponse.setHeader("WWW-Authenticate", "x-scheme x-param");
1980 
1981         EasyMock.expect(
1982                 mockExecChain.proceed(
1983                         EasyMock.isA(ClassicHttpRequest.class),
1984                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
1985         replayMocks();
1986 
1987         final ClassicHttpResponse result = execute(request);
1988         if (result.getCode() == 401) {
1989             Assert.assertNotNull(result.getFirstHeader("WWW-Authenticate"));
1990         }
1991 
1992         verifyMocks();
1993     }
1994 
1995     /*
1996      * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
1997      * header containing a list of valid methods for the requested resource.
1998      *
1999      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
2000      */
2001     @Test
2002     public void testMustIncludeAllowHeaderFromAnOrigin405Response() throws Exception {
2003         originResponse = new BasicClassicHttpResponse(405, "Method Not Allowed");
2004         originResponse.setHeader("Allow", "GET, HEAD");
2005 
2006         backendExpectsAnyRequest().andReturn(originResponse);
2007 
2008         replayMocks();
2009 
2010         final ClassicHttpResponse result = execute(request);
2011         if (result.getCode() == 405) {
2012             Assert.assertNotNull(result.getFirstHeader("Allow"));
2013         }
2014 
2015         verifyMocks();
2016     }
2017 
2018     /*
2019      * "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
2020      * Proxy-Authenticate header field (section 14.33) containing a challenge
2021      * applicable to the proxy for the requested resource."
2022      *
2023      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
2024      */
2025     @Test
2026     public void testMustIncludeProxyAuthenticateHeaderFromAnOrigin407Response() throws Exception {
2027         originResponse = new BasicClassicHttpResponse(407, "Proxy Authentication Required");
2028         originResponse.setHeader("Proxy-Authenticate", "x-scheme x-param");
2029 
2030         EasyMock.expect(
2031                 mockExecChain.proceed(
2032                         EasyMock.isA(ClassicHttpRequest.class),
2033                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
2034         replayMocks();
2035 
2036         final ClassicHttpResponse result = execute(request);
2037         if (result.getCode() == 407) {
2038             Assert.assertNotNull(result.getFirstHeader("Proxy-Authenticate"));
2039         }
2040 
2041         verifyMocks();
2042     }
2043 
2044     /*
2045      * "10.4.17 416 Requested Range Not Satisfiable ... This response MUST NOT
2046      * use the multipart/byteranges content-type."
2047      *
2048      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.17
2049      */
2050     @Test
2051     public void testMustNotAddMultipartByteRangeContentTypeTo416Response() throws Exception {
2052         originResponse = new BasicClassicHttpResponse(416, "Requested Range Not Satisfiable");
2053 
2054         EasyMock.expect(
2055                 mockExecChain.proceed(
2056                         EasyMock.isA(ClassicHttpRequest.class),
2057                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
2058 
2059         replayMocks();
2060         final ClassicHttpResponse result = execute(request);
2061         verifyMocks();
2062 
2063         if (result.getCode() == 416) {
2064             final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_TYPE);
2065             while (it.hasNext()) {
2066                 final HeaderElement elt = it.next();
2067                 Assert.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
2068             }
2069         }
2070     }
2071 
2072     @Test
2073     public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Responses() throws Exception {
2074 
2075         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
2076         originResponse.setHeader("Content-Length", "128");
2077         originResponse.setHeader("Cache-Control", "max-age=3600");
2078 
2079         final ClassicHttpRequest rangeReq = new BasicClassicHttpRequest("GET", "/");
2080         rangeReq.setHeader("Range", "bytes=1000-1200");
2081 
2082         final ClassicHttpResponse orig416 = new BasicClassicHttpResponse(416,
2083                 "Requested Range Not Satisfiable");
2084 
2085         EasyMock.expect(
2086                 mockExecChain.proceed(
2087                         EasyMock.isA(ClassicHttpRequest.class),
2088                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
2089         // cache may 416 me right away if it understands byte ranges,
2090         // ok to delegate to origin though
2091         EasyMock.expect(
2092                 mockExecChain.proceed(
2093                         EasyMock.isA(ClassicHttpRequest.class),
2094                         EasyMock.isA(ExecChain.Scope.class))).andReturn(orig416).times(0, 1);
2095 
2096         replayMocks();
2097         execute(request);
2098         final ClassicHttpResponse result = execute(rangeReq);
2099         verifyMocks();
2100 
2101         // might have gotten a 416 from the origin or the cache
2102         if (result.getCode() == 416) {
2103             final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_TYPE);
2104             while (it.hasNext()) {
2105                 final HeaderElement elt = it.next();
2106                 Assert.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
2107             }
2108         }
2109     }
2110 
2111     /*
2112      * "A correct cache MUST respond to a request with the most up-to-date
2113      * response held by the cache that is appropriate to the request (see
2114      * sections 13.2.5, 13.2.6, and 13.12) which meets one of the following
2115      * conditions:
2116      *
2117      * 1. It has been checked for equivalence with what the origin server would
2118      * have returned by revalidating the response with the origin server
2119      * (section 13.3);
2120      *
2121      * 2. It is "fresh enough" (see section 13.2). In the default case, this
2122      * means it meets the least restrictive freshness requirement of the client,
2123      * origin server, and cache (see section 14.9); if the origin server so
2124      * specifies, it is the freshness requirement of the origin server alone.
2125      *
2126      * If a stored response is not "fresh enough" by the most restrictive
2127      * freshness requirement of both the client and the origin server, in
2128      * carefully considered circumstances the cache MAY still return the
2129      * response with the appropriate Warning header (see section 13.1.5 and
2130      * 14.46), unless such a response is prohibited (e.g., by a "no-store"
2131      * cache-directive, or by a "no-cache" cache-request-directive; see section
2132      * 14.9).
2133      *
2134      * 3. It is an appropriate 304 (Not Modified), 305 (Proxy Redirect), or
2135      * error (4xx or 5xx) response message."
2136      *
2137      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
2138      */
2139     @Test
2140     public void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
2141 
2142         final Date now = new Date();
2143         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2144         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2145         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2146 
2147         final Header[] hdrs = new Header[] {
2148                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2149                 new BasicHeader("Cache-Control", "max-age=0"),
2150                 new BasicHeader("ETag", "\"etag\""),
2151                 new BasicHeader("Content-Length", "128")
2152         };
2153 
2154         final byte[] bytes = new byte[128];
2155         new Random().nextBytes(bytes);
2156 
2157         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2158 
2159         impl = new CachingExec(mockCache, null, config);
2160 
2161         request = new BasicClassicHttpRequest("GET", "/thing");
2162 
2163         final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/thing");
2164         validate.setHeader("If-None-Match", "\"etag\"");
2165 
2166         final ClassicHttpResponse notModified = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
2167         notModified.setHeader("Date", DateUtils.formatDate(now));
2168         notModified.setHeader("ETag", "\"etag\"");
2169 
2170         EasyMock.expect(
2171                 mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request)))
2172                 .andReturn(entry);
2173         EasyMock.expect(
2174                 mockExecChain.proceed(
2175                         eqRequest(validate),
2176                         EasyMock.isA(ExecChain.Scope.class))).andReturn(notModified);
2177         EasyMock.expect(mockCache.updateCacheEntry(
2178                 EasyMock.eq(host),
2179                 eqRequest(request),
2180                 EasyMock.eq(entry),
2181                 eqResponse(notModified),
2182                 EasyMock.isA(Date.class),
2183                 EasyMock.isA(Date.class)))
2184             .andReturn(HttpTestUtils.makeCacheEntry());
2185 
2186         replayMocks();
2187         execute(request);
2188         verifyMocks();
2189     }
2190 
2191     @Test
2192     public void testMustReturnAFreshEnoughCacheEntryIfItHasIt() throws Exception {
2193 
2194         final Date now = new Date();
2195         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2196         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2197         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2198 
2199         final Header[] hdrs = new Header[] {
2200                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2201                 new BasicHeader("Cache-Control", "max-age=3600"),
2202                 new BasicHeader("Content-Length", "128")
2203         };
2204 
2205         final byte[] bytes = new byte[128];
2206         new Random().nextBytes(bytes);
2207 
2208         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2209 
2210         impl = new CachingExec(mockCache, null, config);
2211         request = new BasicClassicHttpRequest("GET", "/thing");
2212 
2213         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2214 
2215         replayMocks();
2216         final ClassicHttpResponse result = execute(request);
2217         verifyMocks();
2218 
2219         Assert.assertEquals(200, result.getCode());
2220     }
2221 
2222     /*
2223      * "If the cache can not communicate with the origin server, then a correct
2224      * cache SHOULD respond as above if the response can be correctly served
2225      * from the cache; if not it MUST return an error or warning indicating that
2226      * there was a communication failure."
2227      *
2228      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
2229      *
2230      * "111 Revalidation failed MUST be included if a cache returns a stale
2231      * response because an attempt to revalidate the response failed, due to an
2232      * inability to reach the server."
2233      *
2234      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
2235      */
2236     @Test
2237     public void testMustServeAppropriateErrorOrWarningIfNoOriginCommunicationPossible() throws Exception {
2238 
2239         final Date now = new Date();
2240         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2241         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2242         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2243 
2244         final Header[] hdrs = new Header[] {
2245                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2246                 new BasicHeader("Cache-Control", "max-age=0"),
2247                 new BasicHeader("Content-Length", "128"),
2248                 new BasicHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo))
2249         };
2250 
2251         final byte[] bytes = new byte[128];
2252         new Random().nextBytes(bytes);
2253 
2254         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2255 
2256         impl = new CachingExec(mockCache, null, config);
2257         request = new BasicClassicHttpRequest("GET", "/thing");
2258 
2259         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2260         EasyMock.expect(
2261                 mockExecChain.proceed(
2262                         EasyMock.isA(ClassicHttpRequest.class),
2263                         EasyMock.isA(ExecChain.Scope.class))).andThrow(
2264                 new IOException("can't talk to origin!")).anyTimes();
2265 
2266         replayMocks();
2267 
2268         final ClassicHttpResponse result = execute(request);
2269 
2270         verifyMocks();
2271 
2272         final int status = result.getCode();
2273         if (status == 200) {
2274             boolean foundWarning = false;
2275             for (final Header h : result.getHeaders("Warning")) {
2276                 if (h.getValue().split(" ")[0].equals("111")) {
2277                     foundWarning = true;
2278                 }
2279             }
2280             Assert.assertTrue(foundWarning);
2281         } else {
2282             Assert.assertTrue(status >= 500 && status <= 599);
2283         }
2284     }
2285 
2286     /*
2287      * "Whenever a cache returns a response that is neither first-hand nor
2288      * "fresh enough" (in the sense of condition 2 in section 13.1.1), it MUST
2289      * attach a warning to that effect, using a Warning general-header."
2290      *
2291      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2292      */
2293     @Test
2294     public void testAttachesWarningHeaderWhenGeneratingStaleResponse() throws Exception {
2295         // covered by previous test
2296     }
2297 
2298     /*
2299      * "1xx Warnings that describe the freshness or revalidation status of the
2300      * response, and so MUST be deleted after a successful revalidation."
2301      *
2302      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2303      */
2304     @Test
2305     public void test1xxWarningsAreDeletedAfterSuccessfulRevalidation() throws Exception {
2306 
2307         final Date now = new Date();
2308         final Date tenSecondsAgo = new Date(now.getTime() - 25 * 1000L);
2309         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2310         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2311         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
2312         resp1.setHeader("ETag", "\"etag\"");
2313         resp1.setHeader("Cache-Control", "max-age=5");
2314         resp1.setHeader("Warning", "110 squid \"stale stuff\"");
2315         resp1.setHeader("Via", "1.1 fred");
2316 
2317         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2318 
2319         final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/");
2320         validate.setHeader("If-None-Match", "\"etag\"");
2321 
2322         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED,
2323                 "Not Modified");
2324         resp2.setHeader("Date", DateUtils.formatDate(now));
2325         resp2.setHeader("Server", "MockServer/1.0");
2326         resp2.setHeader("ETag", "\"etag\"");
2327         resp2.setHeader("Via", "1.1 fred");
2328 
2329         backendExpectsAnyRequestAndReturn(resp1);
2330         EasyMock.expect(
2331                 mockExecChain.proceed(
2332                         eqRequest(validate),
2333                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2);
2334 
2335         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
2336 
2337         replayMocks();
2338 
2339         final ClassicHttpResponse stale = execute(req1);
2340         Assert.assertNotNull(stale.getFirstHeader("Warning"));
2341 
2342         final ClassicHttpResponse result1 = execute(req2);
2343         final ClassicHttpResponse result2 = execute(req3);
2344 
2345         verifyMocks();
2346 
2347         boolean found1xxWarning = false;
2348         final Iterator<HeaderElement> it = MessageSupport.iterate(result1, HttpHeaders.WARNING);
2349         while (it.hasNext()) {
2350             final HeaderElement elt = it.next();
2351             if (elt.getName().startsWith("1")) {
2352                 found1xxWarning = true;
2353             }
2354         }
2355         final Iterator<HeaderElement> it2 = MessageSupport.iterate(result2, HttpHeaders.WARNING);
2356         while (it2.hasNext()) {
2357             final HeaderElement elt = it2.next();
2358             if (elt.getName().startsWith("1")) {
2359                 found1xxWarning = true;
2360             }
2361         }
2362         Assert.assertFalse(found1xxWarning);
2363     }
2364 
2365     /*
2366      * "2xx Warnings that describe some aspect of the entity body or entity
2367      * headers that is not rectified by a revalidation (for example, a lossy
2368      * compression of the entity bodies) and which MUST NOT be deleted after a
2369      * successful revalidation."
2370      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2371      */
2372     @Test
2373     public void test2xxWarningsAreNotDeletedAfterSuccessfulRevalidation() throws Exception {
2374         final Date now = new Date();
2375         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2376         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2377         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2378         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
2379         resp1.setHeader("ETag", "\"etag\"");
2380         resp1.setHeader("Cache-Control", "max-age=5");
2381         resp1.setHeader("Via", "1.1 xproxy");
2382         resp1.setHeader("Warning", "214 xproxy \"transformed stuff\"");
2383 
2384         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2385 
2386         final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/");
2387         validate.setHeader("If-None-Match", "\"etag\"");
2388 
2389         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED,
2390                 "Not Modified");
2391         resp2.setHeader("Date", DateUtils.formatDate(now));
2392         resp2.setHeader("Server", "MockServer/1.0");
2393         resp2.setHeader("ETag", "\"etag\"");
2394         resp1.setHeader("Via", "1.1 xproxy");
2395 
2396         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
2397 
2398         backendExpectsAnyRequestAndReturn(resp1);
2399 
2400         EasyMock.expect(
2401                 mockExecChain.proceed(
2402                         eqRequest(validate),
2403                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2);
2404 
2405         replayMocks();
2406 
2407         final ClassicHttpResponse stale = execute(req1);
2408         Assert.assertNotNull(stale.getFirstHeader("Warning"));
2409 
2410         final ClassicHttpResponse result1 = execute(req2);
2411         final ClassicHttpResponse result2 = execute(req3);
2412 
2413         verifyMocks();
2414 
2415         boolean found214Warning = false;
2416         final Iterator<HeaderElement> it = MessageSupport.iterate(result1, HttpHeaders.WARNING);
2417         while (it.hasNext()) {
2418             final HeaderElement elt = it.next();
2419             final String[] parts = elt.getName().split(" ");
2420             if ("214".equals(parts[0])) {
2421                 found214Warning = true;
2422             }
2423         }
2424         Assert.assertTrue(found214Warning);
2425 
2426         found214Warning = false;
2427         final Iterator<HeaderElement> it2 = MessageSupport.iterate(result2, HttpHeaders.WARNING);
2428         while (it2.hasNext()) {
2429             final HeaderElement elt = it2.next();
2430             final String[] parts = elt.getName().split(" ");
2431             if ("214".equals(parts[0])) {
2432                 found214Warning = true;
2433             }
2434         }
2435         Assert.assertTrue(found214Warning);
2436     }
2437 
2438     /*
2439      * "When a response is generated from a cache entry, the cache MUST include
2440      * a single Age header field in the response with a value equal to the cache
2441      * entry's current_age."
2442      *
2443      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.3
2444      */
2445     @Test
2446     public void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
2447 
2448         final Date now = new Date();
2449         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2450         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2451         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2452 
2453         final Header[] hdrs = new Header[] {
2454                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2455                 new BasicHeader("Cache-Control", "max-age=3600"),
2456                 new BasicHeader("Content-Length", "128")
2457         };
2458 
2459         final byte[] bytes = new byte[128];
2460         new Random().nextBytes(bytes);
2461 
2462         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2463 
2464         impl = new CachingExec(mockCache, null, config);
2465         request = new BasicClassicHttpRequest("GET", "/thing");
2466 
2467         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2468 
2469         replayMocks();
2470         final ClassicHttpResponse result = execute(request);
2471         verifyMocks();
2472 
2473         Assert.assertEquals(200, result.getCode());
2474         Assert.assertEquals("11", result.getFirstHeader("Age").getValue());
2475     }
2476 
2477     /*
2478      * "If none of Expires, Cache-Control: max-age, or Cache-Control: s-maxage
2479      * (see section 14.9.3) appears in the response, and the response does not
2480      * include other restrictions on caching, the cache MAY compute a freshness
2481      * lifetime using a heuristic. The cache MUST attach Warning 113 to any
2482      * response whose age is more than 24 hours if such warning has not already
2483      * been added."
2484      *
2485      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4
2486      *
2487      * "113 Heuristic expiration MUST be included if the cache heuristically
2488      * chose a freshness lifetime greater than 24 hours and the response's age
2489      * is greater than 24 hours."
2490      *
2491      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
2492      */
2493     @Test
2494     public void testHeuristicCacheOlderThan24HoursHasWarningAttached() throws Exception {
2495 
2496         final Date now = new Date();
2497         final Date thirtySixHoursAgo = new Date(now.getTime() - 36 * 3600 * 1000L);
2498         final Date oneYearAgo = new Date(now.getTime() - 365 * 24 * 3600 * 1000L);
2499         final Date requestTime = new Date(thirtySixHoursAgo.getTime() - 1000L);
2500         final Date responseTime = new Date(thirtySixHoursAgo.getTime() + 1000L);
2501 
2502         final Header[] hdrs = new Header[] {
2503                 new BasicHeader("Date", DateUtils.formatDate(thirtySixHoursAgo)),
2504                 new BasicHeader("Cache-Control", "public"),
2505                 new BasicHeader("Last-Modified", DateUtils.formatDate(oneYearAgo)),
2506                 new BasicHeader("Content-Length", "128")
2507         };
2508 
2509         final byte[] bytes = new byte[128];
2510         new Random().nextBytes(bytes);
2511 
2512         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(requestTime, responseTime, hdrs, bytes);
2513 
2514         impl = new CachingExec(mockCache, null, config);
2515 
2516         request = new BasicClassicHttpRequest("GET", "/thing");
2517 
2518         final ClassicHttpResponse validated = HttpTestUtils.make200Response();
2519         validated.setHeader("Cache-Control", "public");
2520         validated.setHeader("Last-Modified", DateUtils.formatDate(oneYearAgo));
2521         validated.setHeader("Content-Length", "128");
2522         validated.setEntity(new ByteArrayEntity(bytes, null));
2523 
2524         final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry();
2525 
2526         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
2527 
2528         mockCache.flushCacheEntriesInvalidatedByExchange(
2529                 EasyMock.isA(HttpHost.class),
2530                 EasyMock.isA(ClassicHttpRequest.class),
2531                 EasyMock.isA(ClassicHttpResponse.class));
2532         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2533         EasyMock.expect(
2534                 mockExecChain.proceed(
2535                         EasyMock.capture(cap),
2536                         EasyMock.isA(ExecChain.Scope.class))).andReturn(validated).times(0, 1);
2537         EasyMock.expect(mockCache.getCacheEntry(
2538                 EasyMock.isA(HttpHost.class),
2539                 EasyMock.isA(ClassicHttpRequest.class))).andReturn(entry).times(0, 1);
2540         EasyMock.expect(mockCache.createCacheEntry(
2541                 EasyMock.isA(HttpHost.class),
2542                 EasyMock.isA(ClassicHttpRequest.class),
2543                 eqCloseableResponse(validated),
2544                 EasyMock.isA(ByteArrayBuffer.class),
2545                 EasyMock.isA(Date.class),
2546                 EasyMock.isA(Date.class))).andReturn(cacheEntry).times(0, 1);
2547 
2548         replayMocks();
2549         final ClassicHttpResponse result = execute(request);
2550         verifyMocks();
2551 
2552         Assert.assertEquals(200, result.getCode());
2553         if (!cap.hasCaptured()) {
2554             // heuristic cache hit
2555             boolean found113Warning = false;
2556             final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.WARNING);
2557             while (it.hasNext()) {
2558                 final HeaderElement elt = it.next();
2559                 final String[] parts = elt.getName().split(" ");
2560                 if ("113".equals(parts[0])) {
2561                     found113Warning = true;
2562                     break;
2563                 }
2564             }
2565             Assert.assertTrue(found113Warning);
2566         }
2567     }
2568 
2569     /*
2570      * "If a cache has two fresh responses for the same representation with
2571      * different validators, it MUST use the one with the more recent Date
2572      * header."
2573      *
2574      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.5
2575      */
2576     @Test
2577     public void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
2578 
2579         final Date now = new Date();
2580         final Date inFiveSecond = new Date(now.getTime() + 5 * 1000L);
2581 
2582         // put an entry in the cache
2583         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2584 
2585         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2586         resp1.setHeader("Date", DateUtils.formatDate(inFiveSecond));
2587         resp1.setHeader("ETag", "\"etag1\"");
2588         resp1.setHeader("Cache-Control", "max-age=3600");
2589         resp1.setHeader("Content-Length", "128");
2590 
2591         // force another origin hit
2592         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2593         req2.setHeader("Cache-Control", "no-cache");
2594 
2595         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2596         resp2.setHeader("Date", DateUtils.formatDate(now)); // older
2597         resp2.setHeader("ETag", "\"etag2\"");
2598         resp2.setHeader("Cache-Control", "max-age=3600");
2599         resp2.setHeader("Content-Length", "128");
2600 
2601         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
2602 
2603         EasyMock.expect(
2604                 mockExecChain.proceed(
2605                         eqRequest(req1),
2606                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1);
2607 
2608         backendExpectsAnyRequestAndReturn(resp2);
2609 
2610         replayMocks();
2611         execute(req1);
2612         execute(req2);
2613         final ClassicHttpResponse result = execute(req3);
2614         verifyMocks();
2615         Assert.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
2616     }
2617 
2618     /*
2619      * "Clients MAY issue simple (non-subrange) GET requests with either weak
2620      * validators or strong validators. Clients MUST NOT use weak validators in
2621      * other forms of request."
2622      *
2623      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
2624      *
2625      * Note that we can't determine a priori whether a given HTTP-date is a weak
2626      * or strong validator, because that might depend on an upstream client
2627      * having a cache with a Last-Modified and Date entry that allows the date
2628      * to be a strong validator. We can tell when *we* are generating a request
2629      * for validation, but we can't tell if we receive a conditional request
2630      * from upstream.
2631      */
2632     private ClassicHttpResponse testRequestWithWeakETagValidatorIsNotAllowed(final String header) throws Exception {
2633         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
2634         EasyMock.expect(
2635                 mockExecChain.proceed(
2636                         EasyMock.capture(cap),
2637                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse).times(0, 1);
2638 
2639         replayMocks();
2640         final ClassicHttpResponse response = execute(request);
2641         verifyMocks();
2642 
2643         // it's probably ok to return a 400 (Bad Request) to this client
2644         if (cap.hasCaptured()) {
2645             final ClassicHttpRequest forwarded = cap.getValue();
2646             final Header h = forwarded.getFirstHeader(header);
2647             if (h != null) {
2648                 Assert.assertFalse(h.getValue().startsWith("W/"));
2649             }
2650         }
2651         return response;
2652 
2653     }
2654 
2655     @Test
2656     public void testSubrangeGETWithWeakETagIsNotAllowed() throws Exception {
2657         request = new BasicClassicHttpRequest("GET", "/");
2658         request.setHeader("Range", "bytes=0-500");
2659         request.setHeader("If-Range", "W/\"etag\"");
2660 
2661         final ClassicHttpResponse response = testRequestWithWeakETagValidatorIsNotAllowed("If-Range");
2662         Assert.assertTrue(response.getCode() == HttpStatus.SC_BAD_REQUEST);
2663     }
2664 
2665     @Test
2666     public void testPUTWithIfMatchWeakETagIsNotAllowed() throws Exception {
2667         final ClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
2668         put.setEntity(HttpTestUtils.makeBody(128));
2669         put.setHeader("Content-Length", "128");
2670         put.setHeader("If-Match", "W/\"etag\"");
2671         request = put;
2672 
2673         testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
2674     }
2675 
2676     @Test
2677     public void testPUTWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
2678         final ClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
2679         put.setEntity(HttpTestUtils.makeBody(128));
2680         put.setHeader("Content-Length", "128");
2681         put.setHeader("If-None-Match", "W/\"etag\"");
2682         request = put;
2683 
2684         testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
2685     }
2686 
2687     @Test
2688     public void testDELETEWithIfMatchWeakETagIsNotAllowed() throws Exception {
2689         request = new BasicClassicHttpRequest("DELETE", "/");
2690         request.setHeader("If-Match", "W/\"etag\"");
2691 
2692         testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
2693     }
2694 
2695     @Test
2696     public void testDELETEWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
2697         request = new BasicClassicHttpRequest("DELETE", "/");
2698         request.setHeader("If-None-Match", "W/\"etag\"");
2699 
2700         testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
2701     }
2702 
2703     /*
2704      * "A cache or origin server receiving a conditional request, other than a
2705      * full-body GET request, MUST use the strong comparison function to
2706      * evaluate the condition."
2707      *
2708      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
2709      */
2710     @Test
2711     public void testSubrangeGETMustUseStrongComparisonForCachedResponse() throws Exception {
2712         final Date now = new Date();
2713         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2714         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2715         resp1.setHeader("Date", DateUtils.formatDate(now));
2716         resp1.setHeader("Cache-Control", "max-age=3600");
2717         resp1.setHeader("ETag", "\"etag\"");
2718 
2719         // according to weak comparison, this would match. Strong
2720         // comparison doesn't, because the cache entry's ETag is not
2721         // marked weak. Therefore, the If-Range must fail and we must
2722         // either get an error back or the full entity, but we better
2723         // not get the conditionally-requested Partial Content (206).
2724         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2725         req2.setHeader("Range", "bytes=0-50");
2726         req2.setHeader("If-Range", "W/\"etag\"");
2727 
2728         EasyMock.expect(
2729                 mockExecChain.proceed(
2730                         EasyMock.isA(ClassicHttpRequest.class),
2731                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1).times(1, 2);
2732 
2733         replayMocks();
2734         execute(req1);
2735         final ClassicHttpResponse result = execute(req2);
2736         verifyMocks();
2737 
2738         Assert.assertFalse(HttpStatus.SC_PARTIAL_CONTENT == result.getCode());
2739     }
2740 
2741     /*
2742      * "HTTP/1.1 clients: - If an entity tag has been provided by the origin
2743      * server, MUST use that entity tag in any cache-conditional request (using
2744      * If- Match or If-None-Match)."
2745      *
2746      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
2747      */
2748     @Test
2749     public void testValidationMustUseETagIfProvidedByOriginServer() throws Exception {
2750 
2751         final Date now = new Date();
2752         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2753 
2754         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2755         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2756         resp1.setHeader("Date", DateUtils.formatDate(now));
2757         resp1.setHeader("Cache-Control", "max-age=3600");
2758         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
2759         resp1.setHeader("ETag", "W/\"etag\"");
2760 
2761         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2762         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
2763 
2764         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
2765         EasyMock.expect(
2766                 mockExecChain.proceed(
2767                         EasyMock.isA(ClassicHttpRequest.class),
2768                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1);
2769 
2770         EasyMock.expect(
2771                 mockExecChain.proceed(
2772                         EasyMock.capture(cap),
2773                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1);
2774 
2775         replayMocks();
2776         execute(req1);
2777         execute(req2);
2778         verifyMocks();
2779 
2780         final ClassicHttpRequest validation = cap.getValue();
2781         boolean isConditional = false;
2782         final String[] conditionalHeaders = { "If-Range", "If-Modified-Since", "If-Unmodified-Since",
2783                 "If-Match", "If-None-Match" };
2784 
2785         for (final String ch : conditionalHeaders) {
2786             if (validation.getFirstHeader(ch) != null) {
2787                 isConditional = true;
2788                 break;
2789             }
2790         }
2791 
2792         if (isConditional) {
2793             boolean foundETag = false;
2794             final Iterator<HeaderElement> it = MessageSupport.iterate(validation, HttpHeaders.IF_MATCH);
2795             while (it.hasNext()) {
2796                 final HeaderElement elt = it.next();
2797                 if ("W/\"etag\"".equals(elt.getName())) {
2798                     foundETag = true;
2799                 }
2800             }
2801             final Iterator<HeaderElement> it2 = MessageSupport.iterate(validation, HttpHeaders.IF_NONE_MATCH);
2802             while (it2.hasNext()) {
2803                 final HeaderElement elt = it2.next();
2804                 if ("W/\"etag\"".equals(elt.getName())) {
2805                     foundETag = true;
2806                 }
2807             }
2808             Assert.assertTrue(foundETag);
2809         }
2810     }
2811 
2812     /*
2813      * "An HTTP/1.1 caching proxy, upon receiving a conditional request that
2814      * includes both a Last-Modified date and one or more entity tags as cache
2815      * validators, MUST NOT return a locally cached response to the client
2816      * unless that cached response is consistent with all of the conditional
2817      * header fields in the request."
2818      *
2819      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
2820      */
2821     @Test
2822     public void testConditionalRequestWhereNotAllValidatorsMatchCannotBeServedFromCache() throws Exception {
2823         final Date now = new Date();
2824         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2825         final Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
2826 
2827         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2828         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2829         resp1.setHeader("Date", DateUtils.formatDate(now));
2830         resp1.setHeader("Cache-Control", "max-age=3600");
2831         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
2832         resp1.setHeader("ETag", "W/\"etag\"");
2833 
2834         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2835         req2.setHeader("If-None-Match", "W/\"etag\"");
2836         req2.setHeader("If-Modified-Since", DateUtils.formatDate(twentySecondsAgo));
2837 
2838         // must hit the origin again for the second request
2839         EasyMock.expect(
2840                 mockExecChain.proceed(
2841                         EasyMock.isA(ClassicHttpRequest.class),
2842                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1).times(2);
2843 
2844         replayMocks();
2845         execute(req1);
2846         final ClassicHttpResponse result = execute(req2);
2847         verifyMocks();
2848 
2849         Assert.assertFalse(HttpStatus.SC_NOT_MODIFIED == result.getCode());
2850     }
2851 
2852     @Test
2853     public void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache() throws Exception {
2854         final Date now = new Date();
2855         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2856 
2857         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2858         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2859         resp1.setHeader("Date", DateUtils.formatDate(now));
2860         resp1.setHeader("Cache-Control", "max-age=3600");
2861         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
2862         resp1.setHeader("ETag", "W/\"etag\"");
2863 
2864         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2865         req2.setHeader("If-None-Match", "W/\"etag\"");
2866         req2.setHeader("If-Modified-Since", DateUtils.formatDate(tenSecondsAgo));
2867 
2868         // may hit the origin again for the second request
2869         EasyMock.expect(
2870                 mockExecChain.proceed(
2871                         EasyMock.isA(ClassicHttpRequest.class),
2872                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp1).times(1,2);
2873 
2874         replayMocks();
2875         execute(req1);
2876         execute(req2);
2877         verifyMocks();
2878     }
2879 
2880 
2881     /*
2882      * "However, a cache that does not support the Range and Content-Range
2883      * headers MUST NOT cache 206 (Partial Content) responses."
2884      *
2885      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
2886      */
2887     @Test
2888     public void testCacheWithoutSupportForRangeAndContentRangeHeadersDoesNotCacheA206Response() throws Exception {
2889 
2890         if (!supportsRangeAndContentRangeHeaders(impl)) {
2891             emptyMockCacheExpectsNoPuts();
2892 
2893             final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
2894             req.setHeader("Range", "bytes=0-50");
2895 
2896             final ClassicHttpResponse resp = new BasicClassicHttpResponse(206, "Partial Content");
2897             resp.setHeader("Content-Range", "bytes 0-50/128");
2898             resp.setHeader("ETag", "\"etag\"");
2899             resp.setHeader("Cache-Control", "max-age=3600");
2900 
2901             EasyMock.expect(mockExecChain.proceed(
2902                     EasyMock.isA(ClassicHttpRequest.class),
2903                     EasyMock.isA(ExecChain.Scope.class))).andReturn(resp);
2904 
2905             replayMocks();
2906             execute(req);
2907             verifyMocks();
2908         }
2909     }
2910 
2911     /*
2912      * "A response received with any other status code (e.g. status codes 302
2913      * and 307) MUST NOT be returned in a reply to a subsequent request unless
2914      * there are cache-control directives or another header(s) that explicitly
2915      * allow it. For example, these include the following: an Expires header
2916      * (section 14.21); a 'max-age', 's-maxage', 'must-revalidate',
2917      * 'proxy-revalidate', 'public' or 'private' cache-control directive
2918      * (section 14.9)."
2919      *
2920      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
2921      */
2922     @Test
2923     public void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() throws Exception {
2924         originResponse = new BasicClassicHttpResponse(302, "Temporary Redirect");
2925         originResponse.setHeader("Location", "http://foo.example.com/other");
2926         originResponse.removeHeaders("Expires");
2927         originResponse.removeHeaders("Cache-Control");
2928 
2929         backendExpectsAnyRequest().andReturn(originResponse).times(2);
2930 
2931         replayMocks();
2932         execute(request);
2933         execute(request);
2934         verifyMocks();
2935     }
2936 
2937     /*
2938      * "A transparent proxy MUST NOT modify any of the following fields in a
2939      * request or response, and it MUST NOT add any of these fields if not
2940      * already present: - Content-Location - Content-MD5 - ETag - Last-Modified
2941      */
2942     private void testDoesNotModifyHeaderFromOrigin(final String header, final String value) throws Exception {
2943         originResponse = HttpTestUtils.make200Response();
2944         originResponse.setHeader(header, value);
2945 
2946         backendExpectsAnyRequest().andReturn(originResponse);
2947 
2948         replayMocks();
2949         final ClassicHttpResponse result = execute(request);
2950         verifyMocks();
2951 
2952         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
2953     }
2954 
2955     @Test
2956     public void testDoesNotModifyContentLocationHeaderFromOrigin() throws Exception {
2957 
2958         final String url = "http://foo.example.com/other";
2959         testDoesNotModifyHeaderFromOrigin("Content-Location", url);
2960     }
2961 
2962     @Test
2963     public void testDoesNotModifyContentMD5HeaderFromOrigin() throws Exception {
2964         testDoesNotModifyHeaderFromOrigin("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
2965     }
2966 
2967     @Test
2968     public void testDoesNotModifyEtagHeaderFromOrigin() throws Exception {
2969         testDoesNotModifyHeaderFromOrigin("Etag", "\"the-etag\"");
2970     }
2971 
2972     @Test
2973     public void testDoesNotModifyLastModifiedHeaderFromOrigin() throws Exception {
2974         final String lm = DateUtils.formatDate(new Date());
2975         testDoesNotModifyHeaderFromOrigin("Last-Modified", lm);
2976     }
2977 
2978     private void testDoesNotAddHeaderToOriginResponse(final String header) throws Exception {
2979         originResponse.removeHeaders(header);
2980 
2981         backendExpectsAnyRequest().andReturn(originResponse);
2982 
2983         replayMocks();
2984         final ClassicHttpResponse result = execute(request);
2985         verifyMocks();
2986 
2987         Assert.assertNull(result.getFirstHeader(header));
2988     }
2989 
2990     @Test
2991     public void testDoesNotAddContentLocationToOriginResponse() throws Exception {
2992         testDoesNotAddHeaderToOriginResponse("Content-Location");
2993     }
2994 
2995     @Test
2996     public void testDoesNotAddContentMD5ToOriginResponse() throws Exception {
2997         testDoesNotAddHeaderToOriginResponse("Content-MD5");
2998     }
2999 
3000     @Test
3001     public void testDoesNotAddEtagToOriginResponse() throws Exception {
3002         testDoesNotAddHeaderToOriginResponse("ETag");
3003     }
3004 
3005     @Test
3006     public void testDoesNotAddLastModifiedToOriginResponse() throws Exception {
3007         testDoesNotAddHeaderToOriginResponse("Last-Modified");
3008     }
3009 
3010     private void testDoesNotModifyHeaderFromOriginOnCacheHit(final String header, final String value) throws Exception {
3011 
3012         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3013         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3014 
3015         originResponse = HttpTestUtils.make200Response();
3016         originResponse.setHeader("Cache-Control", "max-age=3600");
3017         originResponse.setHeader(header, value);
3018 
3019         backendExpectsAnyRequest().andReturn(originResponse);
3020 
3021         replayMocks();
3022         execute(req1);
3023         final ClassicHttpResponse result = execute(req2);
3024         verifyMocks();
3025 
3026         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3027     }
3028 
3029     @Test
3030     public void testDoesNotModifyContentLocationFromOriginOnCacheHit() throws Exception {
3031         final String url = "http://foo.example.com/other";
3032         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-Location", url);
3033     }
3034 
3035     @Test
3036     public void testDoesNotModifyContentMD5FromOriginOnCacheHit() throws Exception {
3037         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3038     }
3039 
3040     @Test
3041     public void testDoesNotModifyEtagFromOriginOnCacheHit() throws Exception {
3042         testDoesNotModifyHeaderFromOriginOnCacheHit("Etag", "\"the-etag\"");
3043     }
3044 
3045     @Test
3046     public void testDoesNotModifyLastModifiedFromOriginOnCacheHit() throws Exception {
3047         final String lm = DateUtils.formatDate(new Date(System.currentTimeMillis() - 10 * 1000L));
3048         testDoesNotModifyHeaderFromOriginOnCacheHit("Last-Modified", lm);
3049     }
3050 
3051     private void testDoesNotAddHeaderOnCacheHit(final String header) throws Exception {
3052 
3053         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3054         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3055 
3056         originResponse.addHeader("Cache-Control", "max-age=3600");
3057         originResponse.removeHeaders(header);
3058 
3059         backendExpectsAnyRequest().andReturn(originResponse);
3060 
3061         replayMocks();
3062         execute(req1);
3063         final ClassicHttpResponse result = execute(req2);
3064         verifyMocks();
3065 
3066         Assert.assertNull(result.getFirstHeader(header));
3067     }
3068 
3069     @Test
3070     public void testDoesNotAddContentLocationHeaderOnCacheHit() throws Exception {
3071         testDoesNotAddHeaderOnCacheHit("Content-Location");
3072     }
3073 
3074     @Test
3075     public void testDoesNotAddContentMD5HeaderOnCacheHit() throws Exception {
3076         testDoesNotAddHeaderOnCacheHit("Content-MD5");
3077     }
3078 
3079     @Test
3080     public void testDoesNotAddETagHeaderOnCacheHit() throws Exception {
3081         testDoesNotAddHeaderOnCacheHit("ETag");
3082     }
3083 
3084     @Test
3085     public void testDoesNotAddLastModifiedHeaderOnCacheHit() throws Exception {
3086         testDoesNotAddHeaderOnCacheHit("Last-Modified");
3087     }
3088 
3089     private void testDoesNotModifyHeaderOnRequest(final String header, final String value) throws Exception {
3090         final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
3091         req.setEntity(HttpTestUtils.makeBody(128));
3092         req.setHeader("Content-Length","128");
3093         req.setHeader(header,value);
3094 
3095         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
3096 
3097         EasyMock.expect(
3098                 mockExecChain.proceed(
3099                         EasyMock.capture(cap),
3100                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
3101 
3102         replayMocks();
3103         execute(req);
3104         verifyMocks();
3105 
3106         final ClassicHttpRequest captured = cap.getValue();
3107         Assert.assertEquals(value, captured.getFirstHeader(header).getValue());
3108     }
3109 
3110     @Test
3111     public void testDoesNotModifyContentLocationHeaderOnRequest() throws Exception {
3112         final String url = "http://foo.example.com/other";
3113         testDoesNotModifyHeaderOnRequest("Content-Location",url);
3114     }
3115 
3116     @Test
3117     public void testDoesNotModifyContentMD5HeaderOnRequest() throws Exception {
3118         testDoesNotModifyHeaderOnRequest("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3119     }
3120 
3121     @Test
3122     public void testDoesNotModifyETagHeaderOnRequest() throws Exception {
3123         testDoesNotModifyHeaderOnRequest("ETag","\"etag\"");
3124     }
3125 
3126     @Test
3127     public void testDoesNotModifyLastModifiedHeaderOnRequest() throws Exception {
3128         final long tenSecondsAgo = System.currentTimeMillis() - 10 * 1000L;
3129         final String lm = DateUtils.formatDate(new Date(tenSecondsAgo));
3130         testDoesNotModifyHeaderOnRequest("Last-Modified", lm);
3131     }
3132 
3133     private void testDoesNotAddHeaderToRequestIfNotPresent(final String header) throws Exception {
3134         final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
3135         req.setEntity(HttpTestUtils.makeBody(128));
3136         req.setHeader("Content-Length","128");
3137         req.removeHeaders(header);
3138 
3139         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
3140 
3141         EasyMock.expect(
3142                 mockExecChain.proceed(
3143                         EasyMock.capture(cap),
3144                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
3145 
3146         replayMocks();
3147         execute(req);
3148         verifyMocks();
3149 
3150         final ClassicHttpRequest captured = cap.getValue();
3151         Assert.assertNull(captured.getFirstHeader(header));
3152     }
3153 
3154     @Test
3155     public void testDoesNotAddContentLocationToRequestIfNotPresent() throws Exception {
3156         testDoesNotAddHeaderToRequestIfNotPresent("Content-Location");
3157     }
3158 
3159     @Test
3160     public void testDoesNotAddContentMD5ToRequestIfNotPresent() throws Exception {
3161         testDoesNotAddHeaderToRequestIfNotPresent("Content-MD5");
3162     }
3163 
3164     @Test
3165     public void testDoesNotAddETagToRequestIfNotPresent() throws Exception {
3166         testDoesNotAddHeaderToRequestIfNotPresent("ETag");
3167     }
3168 
3169     @Test
3170     public void testDoesNotAddLastModifiedToRequestIfNotPresent() throws Exception {
3171         testDoesNotAddHeaderToRequestIfNotPresent("Last-Modified");
3172     }
3173 
3174     /* " A transparent proxy MUST NOT modify any of the following
3175      * fields in a response: - Expires
3176      * but it MAY add any of these fields if not already present. If
3177      * an Expires header is added, it MUST be given a field-value
3178      * identical to that of the Date header in that response.
3179      */
3180     @Test
3181     public void testDoesNotModifyExpiresHeaderFromOrigin() throws Exception {
3182         final long inTenSeconds = System.currentTimeMillis() + 10 * 1000L;
3183         final String expires = DateUtils.formatDate(new Date(inTenSeconds));
3184         testDoesNotModifyHeaderFromOrigin("Expires", expires);
3185     }
3186 
3187     @Test
3188     public void testDoesNotModifyExpiresHeaderFromOriginOnCacheHit() throws Exception {
3189         final long inTenSeconds = System.currentTimeMillis() + 10 * 1000L;
3190         final String expires = DateUtils.formatDate(new Date(inTenSeconds));
3191         testDoesNotModifyHeaderFromOriginOnCacheHit("Expires", expires);
3192     }
3193 
3194     @Test
3195     public void testExpiresHeaderMatchesDateIfAddedToOriginResponse() throws Exception {
3196         originResponse.removeHeaders("Expires");
3197 
3198         backendExpectsAnyRequest().andReturn(originResponse);
3199 
3200         replayMocks();
3201         final ClassicHttpResponse result = execute(request);
3202         verifyMocks();
3203 
3204         final Header expHdr = result.getFirstHeader("Expires");
3205         if (expHdr != null) {
3206             Assert.assertEquals(result.getFirstHeader("Date").getValue(),
3207                                 expHdr.getValue());
3208         }
3209     }
3210 
3211     @Test
3212     public void testExpiresHeaderMatchesDateIfAddedToCacheHit() throws Exception {
3213         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3214         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3215 
3216         originResponse.setHeader("Cache-Control","max-age=3600");
3217         originResponse.removeHeaders("Expires");
3218 
3219         backendExpectsAnyRequest().andReturn(originResponse);
3220 
3221         replayMocks();
3222         execute(req1);
3223         final ClassicHttpResponse result = execute(req2);
3224         verifyMocks();
3225 
3226         final Header expHdr = result.getFirstHeader("Expires");
3227         if (expHdr != null) {
3228             Assert.assertEquals(result.getFirstHeader("Date").getValue(),
3229                                 expHdr.getValue());
3230         }
3231     }
3232 
3233     /* "A proxy MUST NOT modify or add any of the following fields in
3234      * a message that contains the no-transform cache-control
3235      * directive, or in any request: - Content-Encoding - Content-Range
3236      * - Content-Type"
3237      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
3238      */
3239     private void testDoesNotModifyHeaderFromOriginResponseWithNoTransform(final String header, final String value) throws Exception {
3240         originResponse.addHeader("Cache-Control","no-transform");
3241         originResponse.setHeader(header, value);
3242 
3243         backendExpectsAnyRequest().andReturn(originResponse);
3244 
3245         replayMocks();
3246         final ClassicHttpResponse result = execute(request);
3247         verifyMocks();
3248 
3249         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3250     }
3251 
3252     @Test
3253     public void testDoesNotModifyContentEncodingHeaderFromOriginResponseWithNoTransform() throws Exception {
3254         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Encoding","gzip");
3255     }
3256 
3257     @Test
3258     public void testDoesNotModifyContentRangeHeaderFromOriginResponseWithNoTransform() throws Exception {
3259         request.setHeader("If-Range","\"etag\"");
3260         request.setHeader("Range","bytes=0-49");
3261 
3262         originResponse = new BasicClassicHttpResponse(206, "Partial Content");
3263         originResponse.setEntity(HttpTestUtils.makeBody(50));
3264         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Range","bytes 0-49/128");
3265     }
3266 
3267     @Test
3268     public void testDoesNotModifyContentTypeHeaderFromOriginResponseWithNoTransform() throws Exception {
3269         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
3270     }
3271 
3272     private void testDoesNotModifyHeaderOnCachedResponseWithNoTransform(final String header, final String value) throws Exception {
3273         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3274         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3275 
3276         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
3277         originResponse.setHeader(header, value);
3278 
3279         backendExpectsAnyRequest().andReturn(originResponse);
3280 
3281         replayMocks();
3282         execute(req1);
3283         final ClassicHttpResponse result = execute(req2);
3284         verifyMocks();
3285 
3286         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3287     }
3288 
3289     @Test
3290     public void testDoesNotModifyContentEncodingHeaderOnCachedResponseWithNoTransform() throws Exception {
3291         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Encoding","gzip");
3292     }
3293 
3294     @Test
3295     public void testDoesNotModifyContentTypeHeaderOnCachedResponseWithNoTransform() throws Exception {
3296         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
3297     }
3298 
3299     @Test
3300     public void testDoesNotModifyContentRangeHeaderOnCachedResponseWithNoTransform() throws Exception {
3301         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3302         req1.setHeader("If-Range","\"etag\"");
3303         req1.setHeader("Range","bytes=0-49");
3304         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3305         req2.setHeader("If-Range","\"etag\"");
3306         req2.setHeader("Range","bytes=0-49");
3307 
3308         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
3309         originResponse.setHeader("Content-Range", "bytes 0-49/128");
3310 
3311         backendExpectsAnyRequest().andReturn(originResponse).times(1,2);
3312 
3313         replayMocks();
3314         execute(req1);
3315         final ClassicHttpResponse result = execute(req2);
3316         verifyMocks();
3317 
3318         Assert.assertEquals("bytes 0-49/128",
3319                             result.getFirstHeader("Content-Range").getValue());
3320     }
3321 
3322     @Test
3323     public void testDoesNotAddContentEncodingHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3324         originResponse.addHeader("Cache-Control","no-transform");
3325         testDoesNotAddHeaderToOriginResponse("Content-Encoding");
3326     }
3327 
3328     @Test
3329     public void testDoesNotAddContentRangeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3330         originResponse.addHeader("Cache-Control","no-transform");
3331         testDoesNotAddHeaderToOriginResponse("Content-Range");
3332     }
3333 
3334     @Test
3335     public void testDoesNotAddContentTypeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3336         originResponse.addHeader("Cache-Control","no-transform");
3337         testDoesNotAddHeaderToOriginResponse("Content-Type");
3338     }
3339 
3340     /* no add on cache hit with no-transform */
3341     @Test
3342     public void testDoesNotAddContentEncodingHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3343         originResponse.addHeader("Cache-Control","no-transform");
3344         testDoesNotAddHeaderOnCacheHit("Content-Encoding");
3345     }
3346 
3347     @Test
3348     public void testDoesNotAddContentRangeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3349         originResponse.addHeader("Cache-Control","no-transform");
3350         testDoesNotAddHeaderOnCacheHit("Content-Range");
3351     }
3352 
3353     @Test
3354     public void testDoesNotAddContentTypeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3355         originResponse.addHeader("Cache-Control","no-transform");
3356         testDoesNotAddHeaderOnCacheHit("Content-Type");
3357     }
3358 
3359     /* no modify on request */
3360     @Test
3361     public void testDoesNotAddContentEncodingToRequestIfNotPresent() throws Exception {
3362         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
3363     }
3364 
3365     @Test
3366     public void testDoesNotAddContentRangeToRequestIfNotPresent() throws Exception {
3367         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
3368     }
3369 
3370     @Test
3371     public void testDoesNotAddContentTypeToRequestIfNotPresent() throws Exception {
3372         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
3373     }
3374 
3375     @Test
3376     public void testDoesNotAddContentEncodingHeaderToRequestIfNotPresent() throws Exception {
3377         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
3378     }
3379 
3380     @Test
3381     public void testDoesNotAddContentRangeHeaderToRequestIfNotPresent() throws Exception {
3382         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
3383     }
3384 
3385     @Test
3386     public void testDoesNotAddContentTypeHeaderToRequestIfNotPresent() throws Exception {
3387         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
3388     }
3389 
3390     /* "When a cache makes a validating request to a server, and the
3391      * server provides a 304 (Not Modified) response or a 206 (Partial
3392      * Content) response, the cache then constructs a response to send
3393      * to the requesting client.
3394      *
3395      * If the status code is 304 (Not Modified), the cache uses the
3396      * entity-body stored in the cache entry as the entity-body of
3397      * this outgoing response.
3398      *
3399      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.3
3400      */
3401     public void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
3402         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3403         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
3404         resp1.setHeader("Cache-Control","max-age=3600");
3405         resp1.setHeader("ETag","\"etag\"");
3406 
3407         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3408         req2.setHeader("Cache-Control","max-age=0, max-stale=0");
3409         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3410 
3411         backendExpectsAnyRequestAndReturn(resp1);
3412         backendExpectsAnyRequestAndReturn(resp2);
3413 
3414         replayMocks();
3415         execute(req1);
3416         final ClassicHttpResponse result = execute(req2);
3417         verifyMocks();
3418 
3419         final InputStream i1 = resp1.getEntity().getContent();
3420         final InputStream i2 = result.getEntity().getContent();
3421         int b1, b2;
3422         while((b1 = i1.read()) != -1) {
3423             b2 = i2.read();
3424             Assert.assertEquals(b1, b2);
3425         }
3426         b2 = i2.read();
3427         Assert.assertEquals(-1, b2);
3428         i1.close();
3429         i2.close();
3430     }
3431 
3432     /* "The end-to-end headers stored in the cache entry are used for
3433      * the constructed response, except that ...
3434      *
3435      * - any end-to-end headers provided in the 304 or 206 response MUST
3436      *  replace the corresponding headers from the cache entry.
3437      *
3438      * Unless the cache decides to remove the cache entry, it MUST
3439      * also replace the end-to-end headers stored with the cache entry
3440      * with corresponding headers received in the incoming response,
3441      * except for Warning headers as described immediately above."
3442      */
3443     private void decorateWithEndToEndHeaders(final ClassicHttpResponse r) {
3444         r.setHeader("Allow","GET");
3445         r.setHeader("Content-Encoding","gzip");
3446         r.setHeader("Content-Language","en");
3447         r.setHeader("Content-Length", "128");
3448         r.setHeader("Content-Location","http://foo.example.com/other");
3449         r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3450         r.setHeader("Content-Type", "text/html;charset=utf-8");
3451         r.setHeader("Expires", DateUtils.formatDate(new Date(System.currentTimeMillis() + 10 * 1000L)));
3452         r.setHeader("Last-Modified", DateUtils.formatDate(new Date(System.currentTimeMillis() - 10 * 1000L)));
3453         r.setHeader("Location", "http://foo.example.com/other2");
3454         r.setHeader("Pragma", "x-pragma");
3455         r.setHeader("Retry-After","180");
3456     }
3457 
3458     @Test
3459     public void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
3460         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3461         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
3462         resp1.setHeader("Cache-Control","max-age=3600");
3463         resp1.setHeader("ETag","\"etag\"");
3464         decorateWithEndToEndHeaders(resp1);
3465 
3466         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3467         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3468         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3469         resp2.setHeader("Date", DateUtils.formatDate(new Date()));
3470         resp2.setHeader("Server", "MockServer/1.0");
3471 
3472         backendExpectsAnyRequestAndReturn(resp1);
3473         backendExpectsAnyRequestAndReturn(resp2);
3474 
3475         replayMocks();
3476         execute(req1);
3477         final ClassicHttpResponse result = execute(req2);
3478         verifyMocks();
3479 
3480         final String[] endToEndHeaders = {
3481             "Cache-Control", "ETag", "Allow", "Content-Encoding",
3482             "Content-Language", "Content-Length", "Content-Location",
3483             "Content-MD5", "Content-Type", "Expires", "Last-Modified",
3484             "Location", "Pragma", "Retry-After"
3485         };
3486         for(final String h : endToEndHeaders) {
3487             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
3488                                 HttpTestUtils.getCanonicalHeaderValue(result, h));
3489         }
3490     }
3491 
3492     @Test
3493     public void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
3494 
3495         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3496         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
3497         resp1.setHeader("Cache-Control","max-age=3600");
3498         resp1.setHeader("ETag","\"etag\"");
3499         decorateWithEndToEndHeaders(resp1);
3500 
3501         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3502         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3503         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3504         resp2.setHeader("Cache-Control", "max-age=1800");
3505         resp2.setHeader("Date", DateUtils.formatDate(new Date()));
3506         resp2.setHeader("Server", "MockServer/1.0");
3507         resp2.setHeader("Allow", "GET,HEAD");
3508         resp2.setHeader("Content-Language", "en,en-us");
3509         resp2.setHeader("Content-Location", "http://foo.example.com/new");
3510         resp2.setHeader("Content-Type","text/html");
3511         resp2.setHeader("Expires", DateUtils.formatDate(new Date(System.currentTimeMillis() + 5 * 1000L)));
3512         resp2.setHeader("Location", "http://foo.example.com/new2");
3513         resp2.setHeader("Pragma","x-new-pragma");
3514         resp2.setHeader("Retry-After","120");
3515 
3516         backendExpectsAnyRequestAndReturn(resp1);
3517         backendExpectsAnyRequestAndReturn(resp2);
3518 
3519         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3520 
3521         replayMocks();
3522         execute(req1);
3523         final ClassicHttpResponse result1 = execute(req2);
3524         final ClassicHttpResponse result2 = execute(req3);
3525         verifyMocks();
3526 
3527         final String[] endToEndHeaders = {
3528             "Date", "Cache-Control", "Allow", "Content-Language",
3529             "Content-Location", "Content-Type", "Expires", "Location",
3530             "Pragma", "Retry-After"
3531         };
3532         for(final String h : endToEndHeaders) {
3533             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3534                                 HttpTestUtils.getCanonicalHeaderValue(result1, h));
3535             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3536                                 HttpTestUtils.getCanonicalHeaderValue(result2, h));
3537         }
3538     }
3539 
3540     /* "If a header field-name in the incoming response matches more
3541      * than one header in the cache entry, all such old headers MUST
3542      * be replaced."
3543      */
3544     @Test
3545     public void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
3546         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3547         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
3548         resp1.addHeader("Cache-Control","max-age=3600");
3549         resp1.addHeader("Cache-Control","public");
3550         resp1.setHeader("ETag","\"etag\"");
3551 
3552         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3553         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3554         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3555         resp2.setHeader("Cache-Control", "max-age=1800");
3556 
3557         backendExpectsAnyRequestAndReturn(resp1);
3558         backendExpectsAnyRequestAndReturn(resp2);
3559 
3560         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3561 
3562         replayMocks();
3563         execute(req1);
3564         final ClassicHttpResponse result1 = execute(req2);
3565         final ClassicHttpResponse result2 = execute(req3);
3566         verifyMocks();
3567 
3568         final String h = "Cache-Control";
3569         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3570                             HttpTestUtils.getCanonicalHeaderValue(result1, h));
3571         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3572                             HttpTestUtils.getCanonicalHeaderValue(result2, h));
3573     }
3574 
3575     /* "If a cache has a stored non-empty set of subranges for an
3576      * entity, and an incoming response transfers another subrange,
3577      * the cache MAY combine the new subrange with the existing set if
3578      * both the following conditions are met:
3579      *
3580      * - Both the incoming response and the cache entry have a cache
3581      * validator.
3582      *
3583      * - The two cache validators match using the strong comparison
3584      * function (see section 13.3.3).
3585      *
3586      * If either requirement is not met, the cache MUST use only the
3587      * most recent partial response (based on the Date values
3588      * transmitted with every response, and using the incoming
3589      * response if these values are equal or missing), and MUST
3590      * discard the other partial information."
3591      *
3592      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.4
3593      */
3594     @Test
3595     public void testCannotCombinePartialResponseIfIncomingResponseDoesNotHaveACacheValidator() throws Exception {
3596 
3597         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3598         req1.setHeader("Range","bytes=0-49");
3599 
3600         final Date now = new Date();
3601         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3602         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
3603 
3604         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3605         resp1.setEntity(HttpTestUtils.makeBody(50));
3606         resp1.setHeader("Server","MockServer/1.0");
3607         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
3608         resp1.setHeader("Cache-Control","max-age=3600");
3609         resp1.setHeader("Content-Range","bytes 0-49/128");
3610         resp1.setHeader("ETag","\"etag1\"");
3611 
3612         backendExpectsAnyRequestAndReturn(resp1);
3613 
3614         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3615         req2.setHeader("Range","bytes=50-127");
3616 
3617         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3618         resp2.setEntity(HttpTestUtils.makeBody(78));
3619         resp2.setHeader("Cache-Control","max-age=3600");
3620         resp2.setHeader("Content-Range","bytes 50-127/128");
3621         resp2.setHeader("Server","MockServer/1.0");
3622         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3623 
3624         backendExpectsAnyRequestAndReturn(resp2);
3625 
3626         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3627 
3628         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3629         resp3.setEntity(HttpTestUtils.makeBody(128));
3630         resp3.setHeader("Server","MockServer/1.0");
3631         resp3.setHeader("Date", DateUtils.formatDate(now));
3632 
3633         backendExpectsAnyRequestAndReturn(resp3);
3634 
3635         replayMocks();
3636         execute(req1);
3637         execute(req2);
3638         execute(req3);
3639         verifyMocks();
3640     }
3641 
3642     @Test
3643     public void testCannotCombinePartialResponseIfCacheEntryDoesNotHaveACacheValidator() throws Exception {
3644 
3645         final Date now = new Date();
3646         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3647         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
3648 
3649         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3650         req1.setHeader("Range","bytes=0-49");
3651 
3652         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3653         resp1.setEntity(HttpTestUtils.makeBody(50));
3654         resp1.setHeader("Cache-Control","max-age=3600");
3655         resp1.setHeader("Content-Range","bytes 0-49/128");
3656         resp1.setHeader("Server","MockServer/1.0");
3657         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
3658 
3659         backendExpectsAnyRequestAndReturn(resp1);
3660 
3661         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3662         req2.setHeader("Range","bytes=50-127");
3663 
3664         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3665         resp2.setEntity(HttpTestUtils.makeBody(78));
3666         resp2.setHeader("Cache-Control","max-age=3600");
3667         resp2.setHeader("Content-Range","bytes 50-127/128");
3668         resp2.setHeader("ETag","\"etag1\"");
3669         resp2.setHeader("Server","MockServer/1.0");
3670         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3671 
3672         backendExpectsAnyRequestAndReturn(resp2);
3673 
3674         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3675 
3676         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3677         resp3.setEntity(HttpTestUtils.makeBody(128));
3678         resp3.setHeader("Server","MockServer/1.0");
3679         resp3.setHeader("Date", DateUtils.formatDate(now));
3680 
3681         backendExpectsAnyRequestAndReturn(resp3);
3682 
3683         replayMocks();
3684         execute(req1);
3685         execute(req2);
3686         execute(req3);
3687         verifyMocks();
3688     }
3689 
3690     @Test
3691     public void testCannotCombinePartialResponseIfCacheValidatorsDoNotStronglyMatch() throws Exception {
3692 
3693         final Date now = new Date();
3694         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3695         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
3696 
3697         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3698         req1.setHeader("Range","bytes=0-49");
3699 
3700         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3701         resp1.setEntity(HttpTestUtils.makeBody(50));
3702         resp1.setHeader("Cache-Control","max-age=3600");
3703         resp1.setHeader("Content-Range","bytes 0-49/128");
3704         resp1.setHeader("ETag","\"etag1\"");
3705         resp1.setHeader("Server","MockServer/1.0");
3706         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
3707 
3708         backendExpectsAnyRequestAndReturn(resp1);
3709 
3710         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3711         req2.setHeader("Range","bytes=50-127");
3712 
3713         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3714         resp2.setEntity(HttpTestUtils.makeBody(78));
3715         resp2.setHeader("Cache-Control","max-age=3600");
3716         resp2.setHeader("Content-Range","bytes 50-127/128");
3717         resp2.setHeader("ETag","\"etag2\"");
3718         resp2.setHeader("Server","MockServer/1.0");
3719         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3720 
3721         backendExpectsAnyRequestAndReturn(resp2);
3722 
3723         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3724 
3725         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3726         resp3.setEntity(HttpTestUtils.makeBody(128));
3727         resp3.setHeader("Server","MockServer/1.0");
3728         resp3.setHeader("Date", DateUtils.formatDate(now));
3729 
3730         backendExpectsAnyRequestAndReturn(resp3);
3731 
3732         replayMocks();
3733         execute(req1);
3734         execute(req2);
3735         execute(req3);
3736         verifyMocks();
3737     }
3738 
3739     @Test
3740     public void testMustDiscardLeastRecentPartialResponseIfIncomingRequestDoesNotHaveCacheValidator() throws Exception {
3741 
3742         final Date now = new Date();
3743         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3744         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
3745 
3746         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3747         req1.setHeader("Range","bytes=0-49");
3748 
3749         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3750         resp1.setEntity(HttpTestUtils.makeBody(50));
3751         resp1.setHeader("Cache-Control","max-age=3600");
3752         resp1.setHeader("Content-Range","bytes 0-49/128");
3753         resp1.setHeader("ETag","\"etag1\"");
3754         resp1.setHeader("Server","MockServer/1.0");
3755         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
3756 
3757         backendExpectsAnyRequestAndReturn(resp1);
3758 
3759         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3760         req2.setHeader("Range","bytes=50-127");
3761 
3762         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3763         resp2.setEntity(HttpTestUtils.makeBody(78));
3764         resp2.setHeader("Cache-Control","max-age=3600");
3765         resp2.setHeader("Content-Range","bytes 50-127/128");
3766         resp2.setHeader("Server","MockServer/1.0");
3767         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3768 
3769         backendExpectsAnyRequestAndReturn(resp2);
3770 
3771         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3772         req3.setHeader("Range","bytes=0-49");
3773 
3774         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3775         resp3.setEntity(HttpTestUtils.makeBody(128));
3776         resp3.setHeader("Server","MockServer/1.0");
3777         resp3.setHeader("Date", DateUtils.formatDate(now));
3778 
3779         // must make this request; cannot serve from cache
3780         backendExpectsAnyRequestAndReturn(resp3);
3781 
3782         replayMocks();
3783         execute(req1);
3784         execute(req2);
3785         execute(req3);
3786         verifyMocks();
3787     }
3788 
3789     @Test
3790     public void testMustDiscardLeastRecentPartialResponseIfCachedResponseDoesNotHaveCacheValidator() throws Exception {
3791 
3792         final Date now = new Date();
3793         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3794         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
3795 
3796         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3797         req1.setHeader("Range","bytes=0-49");
3798 
3799         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3800         resp1.setEntity(HttpTestUtils.makeBody(50));
3801         resp1.setHeader("Cache-Control","max-age=3600");
3802         resp1.setHeader("Content-Range","bytes 0-49/128");
3803         resp1.setHeader("Server","MockServer/1.0");
3804         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
3805 
3806         backendExpectsAnyRequestAndReturn(resp1);
3807 
3808         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3809         req2.setHeader("Range","bytes=50-127");
3810 
3811         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3812         resp2.setEntity(HttpTestUtils.makeBody(78));
3813         resp2.setHeader("Cache-Control","max-age=3600");
3814         resp2.setHeader("Content-Range","bytes 50-127/128");
3815         resp2.setHeader("ETag","\"etag1\"");
3816         resp2.setHeader("Server","MockServer/1.0");
3817         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3818 
3819         backendExpectsAnyRequestAndReturn(resp2);
3820 
3821         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3822         req3.setHeader("Range","bytes=0-49");
3823 
3824         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3825         resp3.setEntity(HttpTestUtils.makeBody(128));
3826         resp3.setHeader("Server","MockServer/1.0");
3827         resp3.setHeader("Date", DateUtils.formatDate(now));
3828 
3829         // must make this request; cannot serve from cache
3830         backendExpectsAnyRequestAndReturn(resp3);
3831 
3832         replayMocks();
3833         execute(req1);
3834         execute(req2);
3835         execute(req3);
3836         verifyMocks();
3837     }
3838 
3839     @Test
3840     public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatch() throws Exception {
3841 
3842         final Date now = new Date();
3843         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3844         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
3845 
3846         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3847         req1.setHeader("Range","bytes=0-49");
3848 
3849         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3850         resp1.setEntity(HttpTestUtils.makeBody(50));
3851         resp1.setHeader("Cache-Control","max-age=3600");
3852         resp1.setHeader("Content-Range","bytes 0-49/128");
3853         resp1.setHeader("Etag","\"etag1\"");
3854         resp1.setHeader("Server","MockServer/1.0");
3855         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
3856 
3857         backendExpectsAnyRequestAndReturn(resp1);
3858 
3859         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3860         req2.setHeader("Range","bytes=50-127");
3861 
3862         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3863         resp2.setEntity(HttpTestUtils.makeBody(78));
3864         resp2.setHeader("Cache-Control","max-age=3600");
3865         resp2.setHeader("Content-Range","bytes 50-127/128");
3866         resp2.setHeader("ETag","\"etag2\"");
3867         resp2.setHeader("Server","MockServer/1.0");
3868         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3869 
3870         backendExpectsAnyRequestAndReturn(resp2);
3871 
3872         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3873         req3.setHeader("Range","bytes=0-49");
3874 
3875         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3876         resp3.setEntity(HttpTestUtils.makeBody(128));
3877         resp3.setHeader("Server","MockServer/1.0");
3878         resp3.setHeader("Date", DateUtils.formatDate(now));
3879 
3880         // must make this request; cannot serve from cache
3881         backendExpectsAnyRequestAndReturn(resp3);
3882 
3883         replayMocks();
3884         execute(req1);
3885         execute(req2);
3886         execute(req3);
3887         verifyMocks();
3888     }
3889 
3890     @Test
3891     public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatchEvenIfResponsesOutOfOrder() throws Exception {
3892 
3893         final Date now = new Date();
3894         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3895         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
3896 
3897         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3898         req1.setHeader("Range","bytes=0-49");
3899 
3900         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3901         resp1.setEntity(HttpTestUtils.makeBody(50));
3902         resp1.setHeader("Cache-Control","max-age=3600");
3903         resp1.setHeader("Content-Range","bytes 0-49/128");
3904         resp1.setHeader("Etag","\"etag1\"");
3905         resp1.setHeader("Server","MockServer/1.0");
3906         resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3907 
3908         backendExpectsAnyRequestAndReturn(resp1);
3909 
3910         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3911         req2.setHeader("Range","bytes=50-127");
3912 
3913         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3914         resp2.setEntity(HttpTestUtils.makeBody(78));
3915         resp2.setHeader("Cache-Control","max-age=3600");
3916         resp2.setHeader("Content-Range","bytes 50-127/128");
3917         resp2.setHeader("ETag","\"etag2\"");
3918         resp2.setHeader("Server","MockServer/1.0");
3919         resp2.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
3920 
3921         backendExpectsAnyRequestAndReturn(resp2);
3922 
3923         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3924         req3.setHeader("Range","bytes=50-127");
3925 
3926         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3927         resp3.setEntity(HttpTestUtils.makeBody(128));
3928         resp3.setHeader("Server","MockServer/1.0");
3929         resp3.setHeader("Date", DateUtils.formatDate(now));
3930 
3931         // must make this request; cannot serve from cache
3932         backendExpectsAnyRequestAndReturn(resp3);
3933 
3934         replayMocks();
3935         execute(req1);
3936         execute(req2);
3937         execute(req3);
3938         verifyMocks();
3939     }
3940 
3941     @Test
3942     public void testMustDiscardCachedPartialResponseIfCacheValidatorsDoNotStronglyMatchAndDateHeadersAreEqual() throws Exception {
3943 
3944         final Date now = new Date();
3945         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
3946 
3947         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
3948         req1.setHeader("Range","bytes=0-49");
3949 
3950         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3951         resp1.setEntity(HttpTestUtils.makeBody(50));
3952         resp1.setHeader("Cache-Control","max-age=3600");
3953         resp1.setHeader("Content-Range","bytes 0-49/128");
3954         resp1.setHeader("Etag","\"etag1\"");
3955         resp1.setHeader("Server","MockServer/1.0");
3956         resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3957 
3958         backendExpectsAnyRequestAndReturn(resp1);
3959 
3960         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
3961         req2.setHeader("Range","bytes=50-127");
3962 
3963         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
3964         resp2.setEntity(HttpTestUtils.makeBody(78));
3965         resp2.setHeader("Cache-Control","max-age=3600");
3966         resp2.setHeader("Content-Range","bytes 50-127/128");
3967         resp2.setHeader("ETag","\"etag2\"");
3968         resp2.setHeader("Server","MockServer/1.0");
3969         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
3970 
3971         backendExpectsAnyRequestAndReturn(resp2);
3972 
3973         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
3974         req3.setHeader("Range","bytes=0-49");
3975 
3976         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
3977         resp3.setEntity(HttpTestUtils.makeBody(128));
3978         resp3.setHeader("Server","MockServer/1.0");
3979         resp3.setHeader("Date", DateUtils.formatDate(now));
3980 
3981         // must make this request; cannot serve from cache
3982         backendExpectsAnyRequestAndReturn(resp3);
3983 
3984         replayMocks();
3985         execute(req1);
3986         execute(req2);
3987         execute(req3);
3988         verifyMocks();
3989     }
3990 
3991     /* "When the cache receives a subsequent request whose Request-URI
3992      * specifies one or more cache entries including a Vary header
3993      * field, the cache MUST NOT use such a cache entry to construct a
3994      * response to the new request unless all of the selecting
3995      * request-headers present in the new request match the
3996      * corresponding stored request-headers in the original request."
3997      *
3998      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
3999      */
4000     @Test
4001     public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch() throws Exception {
4002 
4003         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4004         req1.setHeader("Accept-Encoding","gzip");
4005 
4006         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4007         resp1.setHeader("ETag","\"etag1\"");
4008         resp1.setHeader("Cache-Control","max-age=3600");
4009         resp1.setHeader("Vary","Accept-Encoding");
4010 
4011         backendExpectsAnyRequestAndReturn(resp1);
4012 
4013         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4014         req2.removeHeaders("Accept-Encoding");
4015 
4016         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4017         resp2.setHeader("ETag","\"etag1\"");
4018         resp2.setHeader("Cache-Control","max-age=3600");
4019 
4020         // not allowed to have a cache hit; must forward request
4021         backendExpectsAnyRequestAndReturn(resp2);
4022 
4023         replayMocks();
4024         execute(req1);
4025         execute(req2);
4026         verifyMocks();
4027     }
4028 
4029     /* "A Vary header field-value of "*" always fails to match and
4030      * subsequent requests on that resource can only be properly
4031      * interpreted by the origin server."
4032      *
4033      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4034      */
4035     @Test
4036     public void testCannotServeFromCacheForVaryStar() throws Exception {
4037         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4038 
4039         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4040         resp1.setHeader("ETag","\"etag1\"");
4041         resp1.setHeader("Cache-Control","max-age=3600");
4042         resp1.setHeader("Vary","*");
4043 
4044         backendExpectsAnyRequestAndReturn(resp1);
4045 
4046         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4047 
4048         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4049         resp2.setHeader("ETag","\"etag1\"");
4050         resp2.setHeader("Cache-Control","max-age=3600");
4051 
4052         // not allowed to have a cache hit; must forward request
4053         backendExpectsAnyRequestAndReturn(resp2);
4054 
4055         replayMocks();
4056         execute(req1);
4057         execute(req2);
4058         verifyMocks();
4059     }
4060 
4061     /* " If the selecting request header fields for the cached entry
4062      * do not match the selecting request header fields of the new
4063      * request, then the cache MUST NOT use a cached entry to satisfy
4064      * the request unless it first relays the new request to the
4065      * origin server in a conditional request and the server responds
4066      * with 304 (Not Modified), including an entity tag or
4067      * Content-Location that indicates the entity to be used.
4068      *
4069      * If an entity tag was assigned to a cached representation, the
4070      * forwarded request SHOULD be conditional and include the entity
4071      * tags in an If-None-Match header field from all its cache
4072      * entries for the resource. This conveys to the server the set of
4073      * entities currently held by the cache, so that if any one of
4074      * these entities matches the requested entity, the server can use
4075      * the ETag header field in its 304 (Not Modified) response to
4076      * tell the cache which entry is appropriate. If the entity-tag of
4077      * the new response matches that of an existing entry, the new
4078      * response SHOULD be used to processChallenge the header fields of the
4079      * existing entry, and the result MUST be returned to the client.
4080      *
4081      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4082      */
4083     @Test
4084     public void testNonmatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated() throws Exception {
4085 
4086         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4087         req1.setHeader("User-Agent","MyBrowser/1.0");
4088 
4089         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4090         resp1.setHeader("ETag","\"etag1\"");
4091         resp1.setHeader("Cache-Control","max-age=3600");
4092         resp1.setHeader("Vary","User-Agent");
4093         resp1.setHeader("Content-Type","application/octet-stream");
4094 
4095         backendExpectsAnyRequestAndReturn(resp1);
4096 
4097         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4098         req2.setHeader("User-Agent","MyBrowser/1.5");
4099 
4100         final ClassicHttpRequest conditional = new BasicClassicHttpRequest("GET", "/");
4101         conditional.setHeader("User-Agent","MyBrowser/1.5");
4102         conditional.setHeader("If-None-Match","\"etag1\"");
4103 
4104         final ClassicHttpResponse resp200 = HttpTestUtils.make200Response();
4105         resp200.setHeader("ETag","\"etag1\"");
4106         resp200.setHeader("Vary","User-Agent");
4107 
4108         final ClassicHttpResponse resp304 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
4109         resp304.setHeader("ETag","\"etag1\"");
4110         resp304.setHeader("Vary","User-Agent");
4111 
4112         final Capture<ClassicHttpRequest> condCap = EasyMock.newCapture();
4113         final Capture<ClassicHttpRequest> uncondCap = EasyMock.newCapture();
4114 
4115         EasyMock.expect(
4116                 mockExecChain.proceed(
4117                         EasyMock.and(eqRequest(conditional), EasyMock.capture(condCap)),
4118                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp304).times(0,1);
4119         EasyMock.expect(
4120                 mockExecChain.proceed(
4121                         EasyMock.and(eqRequest(req2), EasyMock.capture(uncondCap)),
4122                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp200).times(0,1);
4123 
4124         replayMocks();
4125         execute(req1);
4126         final ClassicHttpResponse result = execute(req2);
4127         verifyMocks();
4128 
4129         if (HttpStatus.SC_OK == result.getCode()) {
4130             Assert.assertTrue(condCap.hasCaptured()
4131                               || uncondCap.hasCaptured());
4132             if (uncondCap.hasCaptured()) {
4133                 Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
4134             }
4135         }
4136     }
4137 
4138     /* "Some HTTP methods MUST cause a cache to invalidate an
4139      * entity. This is either the entity referred to by the
4140      * Request-URI, or by the Location or Content-Location headers (if
4141      * present). These methods are:
4142      * - PUT
4143      * - DELETE
4144      * - POST
4145      *
4146      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
4147      */
4148     protected void testUnsafeOperationInvalidatesCacheForThatUri(
4149             final ClassicHttpRequest unsafeReq) throws Exception {
4150         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4151         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4152         resp1.setHeader("Cache-Control","public, max-age=3600");
4153 
4154         backendExpectsAnyRequestAndReturn(resp1);
4155 
4156         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
4157 
4158         backendExpectsAnyRequestAndReturn(resp2);
4159 
4160         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
4161         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
4162         resp3.setHeader("Cache-Control","public, max-age=3600");
4163 
4164         // this origin request MUST happen due to invalidation
4165         backendExpectsAnyRequestAndReturn(resp3);
4166 
4167         replayMocks();
4168         execute(req1);
4169         execute(unsafeReq);
4170         execute(req3);
4171         verifyMocks();
4172     }
4173 
4174     @Test
4175     public void testPutToUriInvalidatesCacheForThatUri() throws Exception {
4176         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
4177         testUnsafeOperationInvalidatesCacheForThatUri(req);
4178     }
4179 
4180     @Test
4181     public void testDeleteToUriInvalidatesCacheForThatUri() throws Exception {
4182         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE","/");
4183         testUnsafeOperationInvalidatesCacheForThatUri(req);
4184     }
4185 
4186     @Test
4187     public void testPostToUriInvalidatesCacheForThatUri() throws Exception {
4188         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
4189         testUnsafeOperationInvalidatesCacheForThatUri(req);
4190     }
4191 
4192     protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
4193             final ClassicHttpRequest unsafeReq) throws Exception {
4194         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/content");
4195         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4196         resp1.setHeader("Cache-Control","public, max-age=3600");
4197 
4198         backendExpectsAnyRequestAndReturn(resp1);
4199 
4200         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
4201 
4202         backendExpectsAnyRequestAndReturn(resp2);
4203 
4204         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/content");
4205         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
4206         resp3.setHeader("Cache-Control","public, max-age=3600");
4207 
4208         // this origin request MUST happen due to invalidation
4209         backendExpectsAnyRequestAndReturn(resp3);
4210 
4211         replayMocks();
4212         execute(req1);
4213         execute(unsafeReq);
4214         execute(req3);
4215         verifyMocks();
4216     }
4217 
4218     protected void testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(
4219             final ClassicHttpRequest unsafeReq) throws Exception {
4220         unsafeReq.setHeader("Content-Location","http://foo.example.com/content");
4221         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4222     }
4223 
4224     protected void testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(
4225             final ClassicHttpRequest unsafeReq) throws Exception {
4226         unsafeReq.setHeader("Content-Location","/content");
4227         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4228     }
4229 
4230     protected void testUnsafeMethodInvalidatesCacheForUriInLocationHeader(
4231             final ClassicHttpRequest unsafeReq) throws Exception {
4232         unsafeReq.setHeader("Location","http://foo.example.com/content");
4233         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4234     }
4235 
4236     @Test
4237     public void testPutInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4238         final ClassicHttpRequest req2 = makeRequestWithBody("PUT","/");
4239         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req2);
4240     }
4241 
4242     @Test
4243     public void testPutInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4244         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
4245         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4246     }
4247 
4248     @Test
4249     public void testPutInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
4250         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
4251         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4252     }
4253 
4254     @Test
4255     public void testDeleteInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4256         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
4257         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
4258     }
4259 
4260     @Test
4261     public void testDeleteInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
4262         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
4263         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4264     }
4265 
4266     @Test
4267     public void testDeleteInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4268         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
4269         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4270     }
4271 
4272     @Test
4273     public void testPostInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4274         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
4275         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
4276     }
4277 
4278     @Test
4279     public void testPostInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4280         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
4281         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4282     }
4283 
4284     @Test
4285     public void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() throws Exception {
4286         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
4287         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4288     }
4289 
4290     /* "In order to prevent denial of service attacks, an invalidation based on the URI
4291      *  in a Location or Content-Location header MUST only be performed if the host part
4292      *  is the same as in the Request-URI."
4293      *
4294      *  http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
4295      */
4296     protected void testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(
4297             final ClassicHttpRequest unsafeReq) throws Exception {
4298 
4299         final HttpHost otherHost = new HttpHost("bar.example.com", 80);
4300         final HttpRoute otherRoute = new HttpRoute(otherHost);
4301         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/content");
4302         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4303         resp1.setHeader("Cache-Control","public, max-age=3600");
4304 
4305         backendExpectsAnyRequestAndReturn(resp1);
4306 
4307         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
4308 
4309         backendExpectsAnyRequestAndReturn(resp2);
4310 
4311         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/content");
4312 
4313         replayMocks();
4314         execute(req1);
4315         execute(unsafeReq);
4316         execute(req3);
4317         verifyMocks();
4318     }
4319 
4320     protected void testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(
4321             final ClassicHttpRequest unsafeReq) throws Exception {
4322         unsafeReq.setHeader("Content-Location","http://bar.example.com/content");
4323         testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
4324     }
4325 
4326     protected void testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(
4327             final ClassicHttpRequest unsafeReq) throws Exception {
4328         unsafeReq.setHeader("Location","http://bar.example.com/content");
4329         testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
4330     }
4331 
4332     protected ClassicHttpRequest makeRequestWithBody(final String method, final String requestUri) {
4333         final ClassicHttpRequest req =
4334             new BasicClassicHttpRequest(method, requestUri);
4335         final int nbytes = 128;
4336         req.setEntity(HttpTestUtils.makeBody(nbytes));
4337         req.setHeader("Content-Length",""+nbytes);
4338         return req;
4339     }
4340 
4341     @Test
4342     public void testPutDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4343         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
4344         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4345     }
4346 
4347     @Test
4348     public void testPutDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4349         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
4350         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4351     }
4352 
4353     @Test
4354     public void testPostDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4355         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
4356         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4357     }
4358 
4359     @Test
4360     public void testPostDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4361         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
4362         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4363     }
4364 
4365     @Test
4366     public void testDeleteDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4367         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
4368         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4369     }
4370 
4371     @Test
4372     public void testDeleteDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4373         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
4374         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4375     }
4376 
4377     /* "All methods that might be expected to cause modifications to the origin
4378      * server's resources MUST be written through to the origin server. This
4379      * currently includes all methods except for GET and HEAD. A cache MUST NOT
4380      * reply to such a request from a client before having transmitted the
4381      * request to the inbound server, and having received a corresponding
4382      * response from the inbound server."
4383      *
4384      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.11
4385      */
4386     private void testRequestIsWrittenThroughToOrigin(final ClassicHttpRequest req) throws Exception {
4387         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
4388         final ClassicHttpRequest wrapper = req;
4389         EasyMock.expect(
4390                 mockExecChain.proceed(
4391                         eqRequest(wrapper),
4392                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp);
4393 
4394         replayMocks();
4395         execute(wrapper);
4396         verifyMocks();
4397     }
4398 
4399     @Test @Ignore
4400     public void testOPTIONSRequestsAreWrittenThroughToOrigin() throws Exception {
4401         final ClassicHttpRequest req = new BasicClassicHttpRequest("OPTIONS","*");
4402         testRequestIsWrittenThroughToOrigin(req);
4403     }
4404 
4405     @Test
4406     public void testPOSTRequestsAreWrittenThroughToOrigin() throws Exception {
4407         final ClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
4408         req.setEntity(HttpTestUtils.makeBody(128));
4409         req.setHeader("Content-Length","128");
4410         testRequestIsWrittenThroughToOrigin(req);
4411     }
4412 
4413     @Test
4414     public void testPUTRequestsAreWrittenThroughToOrigin() throws Exception {
4415         final ClassicHttpRequest req = new BasicClassicHttpRequest("PUT","/");
4416         req.setEntity(HttpTestUtils.makeBody(128));
4417         req.setHeader("Content-Length","128");
4418         testRequestIsWrittenThroughToOrigin(req);
4419     }
4420 
4421     @Test
4422     public void testDELETERequestsAreWrittenThroughToOrigin() throws Exception {
4423         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
4424         testRequestIsWrittenThroughToOrigin(req);
4425     }
4426 
4427     @Test
4428     public void testTRACERequestsAreWrittenThroughToOrigin() throws Exception {
4429         final ClassicHttpRequest req = new BasicClassicHttpRequest("TRACE","/");
4430         testRequestIsWrittenThroughToOrigin(req);
4431     }
4432 
4433     @Test
4434     public void testCONNECTRequestsAreWrittenThroughToOrigin() throws Exception {
4435         final ClassicHttpRequest req = new BasicClassicHttpRequest("CONNECT","/");
4436         testRequestIsWrittenThroughToOrigin(req);
4437     }
4438 
4439     @Test
4440     public void testUnknownMethodRequestsAreWrittenThroughToOrigin() throws Exception {
4441         final ClassicHttpRequest req = new BasicClassicHttpRequest("UNKNOWN","/");
4442         testRequestIsWrittenThroughToOrigin(req);
4443     }
4444 
4445     /* "If a cache receives a value larger than the largest positive
4446      * integer it can represent, or if any of its age calculations
4447      * overflows, it MUST transmit an Age header with a value of
4448      * 2147483648 (2^31)."
4449      *
4450      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.6
4451      */
4452     @Test
4453     public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig() throws Exception {
4454         final String reallyOldAge = "1" + Long.MAX_VALUE;
4455         originResponse.setHeader("Age",reallyOldAge);
4456 
4457         backendExpectsAnyRequest().andReturn(originResponse);
4458 
4459         replayMocks();
4460         final ClassicHttpResponse result = execute(request);
4461         verifyMocks();
4462 
4463         Assert.assertEquals("2147483648",
4464                             result.getFirstHeader("Age").getValue());
4465     }
4466 
4467     /* "A proxy MUST NOT modify the Allow header field even if it does not
4468      * understand all the methods specified, since the user agent might
4469      * have other means of communicating with the origin server.
4470      *
4471      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
4472      */
4473     @Test
4474     public void testDoesNotModifyAllowHeaderWithUnknownMethods() throws Exception {
4475         final String allowHeaderValue = "GET, HEAD, FOOBAR";
4476         originResponse.setHeader("Allow",allowHeaderValue);
4477         backendExpectsAnyRequest().andReturn(originResponse);
4478         replayMocks();
4479         final ClassicHttpResponse result = execute(request);
4480         verifyMocks();
4481         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse,"Allow"),
4482                             HttpTestUtils.getCanonicalHeaderValue(result, "Allow"));
4483     }
4484 
4485     /* "When a shared cache (see section 13.7) receives a request
4486      * containing an Authorization field, it MUST NOT return the
4487      * corresponding response as a reply to any other request, unless one
4488      * of the following specific exceptions holds:
4489      *
4490      * 1. If the response includes the "s-maxage" cache-control
4491      *    directive, the cache MAY use that response in replying to a
4492      *    subsequent request. But (if the specified maximum age has
4493      *    passed) a proxy cache MUST first revalidate it with the origin
4494      *    server, using the request-headers from the new request to allow
4495      *    the origin server to authenticate the new request. (This is the
4496      *    defined behavior for s-maxage.) If the response includes "s-
4497      *    maxage=0", the proxy MUST always revalidate it before re-using
4498      *    it.
4499      *
4500      * 2. If the response includes the "must-revalidate" cache-control
4501      *    directive, the cache MAY use that response in replying to a
4502      *    subsequent request. But if the response is stale, all caches
4503      *    MUST first revalidate it with the origin server, using the
4504      *    request-headers from the new request to allow the origin server
4505      *    to authenticate the new request.
4506      *
4507      * 3. If the response includes the "public" cache-control directive,
4508      *    it MAY be returned in reply to any subsequent request.
4509      */
4510     protected void testSharedCacheRevalidatesAuthorizedResponse(
4511             final ClassicHttpResponse authorizedResponse, final int minTimes, final int maxTimes) throws Exception {
4512         if (config.isSharedCache()) {
4513             final String authorization = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
4514             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4515             req1.setHeader("Authorization",authorization);
4516 
4517             backendExpectsAnyRequestAndReturn(authorizedResponse);
4518 
4519             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4520             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4521             resp2.setHeader("Cache-Control","max-age=3600");
4522 
4523             if (maxTimes > 0) {
4524                 // this request MUST happen
4525                 backendExpectsAnyRequest().andReturn(resp2).times(minTimes,maxTimes);
4526             }
4527 
4528             replayMocks();
4529             execute(req1);
4530             execute(req2);
4531             verifyMocks();
4532         }
4533     }
4534 
4535     @Test
4536     public void testSharedCacheMustNotNormallyCacheAuthorizedResponses() throws Exception {
4537         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
4538         resp.setHeader("Cache-Control","max-age=3600");
4539         resp.setHeader("ETag","\"etag\"");
4540         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
4541     }
4542 
4543     @Test
4544     public void testSharedCacheMayCacheAuthorizedResponsesWithSMaxAgeHeader() throws Exception {
4545         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
4546         resp.setHeader("Cache-Control","s-maxage=3600");
4547         resp.setHeader("ETag","\"etag\"");
4548         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
4549     }
4550 
4551     @Test
4552     public void testSharedCacheMustRevalidateAuthorizedResponsesWhenSMaxAgeIsZero() throws Exception {
4553         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
4554         resp.setHeader("Cache-Control","s-maxage=0");
4555         resp.setHeader("ETag","\"etag\"");
4556         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
4557     }
4558 
4559     @Test
4560     public void testSharedCacheMayCacheAuthorizedResponsesWithMustRevalidate() throws Exception {
4561         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
4562         resp.setHeader("Cache-Control","must-revalidate");
4563         resp.setHeader("ETag","\"etag\"");
4564         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
4565     }
4566 
4567     @Test
4568     public void testSharedCacheMayCacheAuthorizedResponsesWithCacheControlPublic() throws Exception {
4569         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
4570         resp.setHeader("Cache-Control","public");
4571         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
4572     }
4573 
4574     protected void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(
4575             final ClassicHttpResponse authorizedResponse) throws Exception {
4576         if (config.isSharedCache()) {
4577             final String authorization1 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
4578             final String authorization2 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Qy";
4579 
4580             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4581             req1.setHeader("Authorization",authorization1);
4582 
4583             backendExpectsAnyRequestAndReturn(authorizedResponse);
4584 
4585             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4586             req2.setHeader("Authorization",authorization2);
4587 
4588             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4589 
4590             final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
4591             EasyMock.expect(
4592                     mockExecChain.proceed(
4593                             EasyMock.capture(cap),
4594                             EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2);
4595 
4596             replayMocks();
4597             execute(req1);
4598             execute(req2);
4599             verifyMocks();
4600 
4601             final ClassicHttpRequest captured = cap.getValue();
4602             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(req2, "Authorization"),
4603                     HttpTestUtils.getCanonicalHeaderValue(captured, "Authorization"));
4604         }
4605     }
4606 
4607     @Test
4608     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithSMaxAge() throws Exception {
4609         final Date now = new Date();
4610         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
4611         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4612         resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
4613         resp1.setHeader("ETag","\"etag\"");
4614         resp1.setHeader("Cache-Control","s-maxage=5");
4615 
4616         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
4617     }
4618 
4619     @Test
4620     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithMustRevalidate() throws Exception {
4621         final Date now = new Date();
4622         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
4623         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4624         resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
4625         resp1.setHeader("ETag","\"etag\"");
4626         resp1.setHeader("Cache-Control","maxage=5, must-revalidate");
4627 
4628         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
4629     }
4630 
4631     /* "If a cache returns a stale response, either because of a max-stale
4632      * directive on a request, or because the cache is configured to
4633      * override the expiration time of a response, the cache MUST attach a
4634      * Warning header to the stale response, using Warning 110 (Response
4635      * is stale).
4636      *
4637      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
4638      *
4639      * "110 Response is stale MUST be included whenever the returned
4640      * response is stale."
4641      *
4642      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
4643      */
4644     @Test
4645     public void testWarning110IsAddedToStaleResponses() throws Exception {
4646         final Date now = new Date();
4647         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
4648         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4649         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4650         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
4651         resp1.setHeader("Cache-Control","max-age=5");
4652         resp1.setHeader("Etag","\"etag\"");
4653 
4654         backendExpectsAnyRequestAndReturn(resp1);
4655 
4656         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4657         req2.setHeader("Cache-Control","max-stale=60");
4658         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4659 
4660         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
4661         EasyMock.expect(
4662                 mockExecChain.proceed(
4663                         EasyMock.capture(cap),
4664                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2).times(0,1);
4665 
4666         replayMocks();
4667         execute(req1);
4668         final ClassicHttpResponse result = execute(req2);
4669         verifyMocks();
4670 
4671         if (!cap.hasCaptured()) {
4672             boolean found110Warning = false;
4673             final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.WARNING);
4674             while (it.hasNext()) {
4675                 final HeaderElement elt = it.next();
4676                 final String[] parts = elt.getName().split("\\s");
4677                 if ("110".equals(parts[0])) {
4678                     found110Warning = true;
4679                     break;
4680                 }
4681             }
4682             Assert.assertTrue(found110Warning);
4683         }
4684     }
4685 
4686     /* "Field names MUST NOT be included with the no-cache directive in a
4687      * request."
4688      *
4689      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
4690      */
4691     @Test
4692     public void testDoesNotTransmitNoCacheDirectivesWithFieldsDownstream() throws Exception {
4693         request.setHeader("Cache-Control","no-cache=\"X-Field\"");
4694         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
4695         EasyMock.expect(mockExecChain.proceed(
4696                 EasyMock.capture(cap),
4697                 EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse).times(0,1);
4698 
4699         replayMocks();
4700         try {
4701             execute(request);
4702         } catch (final ClientProtocolException acceptable) {
4703         }
4704         verifyMocks();
4705 
4706         if (cap.hasCaptured()) {
4707             final ClassicHttpRequest captured = cap.getValue();
4708             final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.CACHE_CONTROL);
4709             while (it.hasNext()) {
4710                 final HeaderElement elt = it.next();
4711                 if ("no-cache".equals(elt.getName())) {
4712                     Assert.assertNull(elt.getValue());
4713                 }
4714             }
4715         }
4716     }
4717 
4718     /* "The request includes a "no-cache" cache-control directive or, for
4719      * compatibility with HTTP/1.0 clients, "Pragma: no-cache".... The
4720      * server MUST NOT use a cached copy when responding to such a request."
4721      *
4722      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
4723      */
4724     protected void testCacheIsNotUsedWhenRespondingToRequest(final ClassicHttpRequest req) throws Exception {
4725         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4726         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4727         resp1.setHeader("Etag","\"etag\"");
4728         resp1.setHeader("Cache-Control","max-age=3600");
4729 
4730         backendExpectsAnyRequestAndReturn(resp1);
4731 
4732         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4733         resp2.setHeader("Etag","\"etag2\"");
4734         resp2.setHeader("Cache-Control","max-age=1200");
4735 
4736         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
4737         EasyMock.expect(mockExecChain.proceed(
4738                 EasyMock.capture(cap),
4739                 EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2);
4740 
4741         replayMocks();
4742         execute(req1);
4743         final ClassicHttpResponse result = execute(req);
4744         verifyMocks();
4745 
4746         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
4747         final ClassicHttpRequest captured = cap.getValue();
4748         Assert.assertTrue(HttpTestUtils.equivalent(req, captured));
4749     }
4750 
4751     @Test
4752     public void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache() throws Exception {
4753         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
4754         req.setHeader("Cache-Control","no-cache");
4755         testCacheIsNotUsedWhenRespondingToRequest(req);
4756     }
4757 
4758     @Test
4759     public void testCacheIsNotUsedWhenRespondingToRequestWithPragmaNoCache() throws Exception {
4760         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
4761         req.setHeader("Pragma","no-cache");
4762         testCacheIsNotUsedWhenRespondingToRequest(req);
4763     }
4764 
4765     /* "When the must-revalidate directive is present in a response received
4766      * by a cache, that cache MUST NOT use the entry after it becomes stale
4767      * to respond to a subsequent request without first revalidating it with
4768      * the origin server. (I.e., the cache MUST do an end-to-end
4769      * revalidation every time, if, based solely on the origin server's
4770      * Expires or max-age value, the cached response is stale.)"
4771      *
4772      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
4773      */
4774     protected void testStaleCacheResponseMustBeRevalidatedWithOrigin(
4775             final ClassicHttpResponse staleResponse) throws Exception {
4776         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4777 
4778         backendExpectsAnyRequestAndReturn(staleResponse);
4779 
4780         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4781         req2.setHeader("Cache-Control","max-stale=3600");
4782         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4783         resp2.setHeader("ETag","\"etag2\"");
4784         resp2.setHeader("Cache-Control","max-age=5, must-revalidate");
4785 
4786         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
4787         // this request MUST happen
4788         EasyMock.expect(
4789                 mockExecChain.proceed(
4790                         EasyMock.capture(cap),
4791                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2);
4792 
4793         replayMocks();
4794         execute(req1);
4795         execute(req2);
4796         verifyMocks();
4797 
4798         final ClassicHttpRequest reval = cap.getValue();
4799         boolean foundMaxAge0 = false;
4800         final Iterator<HeaderElement> it = MessageSupport.iterate(reval, HttpHeaders.CACHE_CONTROL);
4801         while (it.hasNext()) {
4802             final HeaderElement elt = it.next();
4803             if ("max-age".equalsIgnoreCase(elt.getName())
4804                     && "0".equals(elt.getValue())) {
4805                 foundMaxAge0 = true;
4806             }
4807         }
4808         Assert.assertTrue(foundMaxAge0);
4809     }
4810 
4811     @Test
4812     public void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
4813         final ClassicHttpResponse response = HttpTestUtils.make200Response();
4814         final Date now = new Date();
4815         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
4816         response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
4817         response.setHeader("ETag","\"etag1\"");
4818         response.setHeader("Cache-Control","max-age=5, must-revalidate");
4819 
4820         testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
4821     }
4822 
4823 
4824     /* "In all circumstances an HTTP/1.1 cache MUST obey the must-revalidate
4825      * directive; in particular, if the cache cannot reach the origin server
4826      * for any reason, it MUST generate a 504 (Gateway Timeout) response."
4827      */
4828     protected void testGenerates504IfCannotRevalidateStaleResponse(
4829             final ClassicHttpResponse staleResponse) throws Exception {
4830         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4831 
4832         backendExpectsAnyRequestAndReturn(staleResponse);
4833 
4834         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4835 
4836         backendExpectsAnyRequest().andThrow(new SocketTimeoutException());
4837 
4838         replayMocks();
4839         execute(req1);
4840         final ClassicHttpResponse result = execute(req2);
4841         verifyMocks();
4842 
4843         Assert.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
4844                             result.getCode());
4845     }
4846 
4847     @Test
4848     public void testGenerates504IfCannotRevalidateAMustRevalidateEntry() throws Exception {
4849         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4850         final Date now = new Date();
4851         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
4852         resp1.setHeader("ETag","\"etag\"");
4853         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
4854         resp1.setHeader("Cache-Control","max-age=5,must-revalidate");
4855 
4856         testGenerates504IfCannotRevalidateStaleResponse(resp1);
4857     }
4858 
4859     /* "The proxy-revalidate directive has the same meaning as the must-
4860      * revalidate directive, except that it does not apply to non-shared
4861      * user agent caches."
4862      *
4863      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
4864      */
4865     @Test
4866     public void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
4867         if (config.isSharedCache()) {
4868             final ClassicHttpResponse response = HttpTestUtils.make200Response();
4869             final Date now = new Date();
4870             final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
4871             response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
4872             response.setHeader("ETag","\"etag1\"");
4873             response.setHeader("Cache-Control","max-age=5, proxy-revalidate");
4874 
4875             testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
4876         }
4877     }
4878 
4879     @Test
4880     public void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry() throws Exception {
4881         if (config.isSharedCache()) {
4882             final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4883             final Date now = new Date();
4884             final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
4885             resp1.setHeader("ETag","\"etag\"");
4886             resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
4887             resp1.setHeader("Cache-Control","max-age=5,proxy-revalidate");
4888 
4889             testGenerates504IfCannotRevalidateStaleResponse(resp1);
4890         }
4891     }
4892 
4893     /* "[The cache control directive] "private" Indicates that all or part of
4894      * the response message is intended for a single user and MUST NOT be
4895      * cached by a shared cache."
4896      *
4897      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
4898      */
4899     @Test
4900     public void testCacheControlPrivateIsNotCacheableBySharedCache() throws Exception {
4901        if (config.isSharedCache()) {
4902                final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4903                final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4904                resp1.setHeader("Cache-Control","private,max-age=3600");
4905 
4906                backendExpectsAnyRequestAndReturn(resp1);
4907 
4908                final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4909                final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4910                // this backend request MUST happen
4911                backendExpectsAnyRequestAndReturn(resp2);
4912 
4913                replayMocks();
4914                execute(req1);
4915                execute(req2);
4916                verifyMocks();
4917        }
4918     }
4919 
4920     @Test
4921     public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache() throws Exception {
4922        if (config.isSharedCache()) {
4923                final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4924                final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4925                resp1.setHeader("X-Personal","stuff");
4926                resp1.setHeader("Cache-Control","private=\"X-Personal\",s-maxage=3600");
4927 
4928                backendExpectsAnyRequestAndReturn(resp1);
4929 
4930                final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4931                final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4932 
4933                // this backend request MAY happen
4934                backendExpectsAnyRequestAndReturn(resp2).times(0,1);
4935 
4936                replayMocks();
4937                execute(req1);
4938                final ClassicHttpResponse result = execute(req2);
4939                verifyMocks();
4940                Assert.assertNull(result.getFirstHeader("X-Personal"));
4941        }
4942     }
4943 
4944     /* "If the no-cache directive does not specify a field-name, then a
4945      * cache MUST NOT use the response to satisfy a subsequent request
4946      * without successful revalidation with the origin server. This allows
4947      * an origin server to prevent caching even by caches that have been
4948      * configured to return stale responses to client requests."
4949      *
4950      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
4951      */
4952     @Test
4953     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation() throws Exception {
4954         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4955         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4956         resp1.setHeader("ETag","\"etag\"");
4957         resp1.setHeader("Cache-Control","no-cache");
4958 
4959         backendExpectsAnyRequestAndReturn(resp1);
4960 
4961         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4962         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4963 
4964         // this MUST happen
4965         backendExpectsAnyRequestAndReturn(resp2);
4966 
4967         replayMocks();
4968         execute(req1);
4969         execute(req2);
4970         verifyMocks();
4971     }
4972 
4973     @Test
4974     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications() throws Exception {
4975         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
4976         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
4977         resp1.setHeader("ETag","\"etag\"");
4978         resp1.setHeader("Cache-Control","no-cache,s-maxage=3600");
4979 
4980         backendExpectsAnyRequestAndReturn(resp1);
4981 
4982         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
4983         req2.setHeader("Cache-Control","max-stale=7200");
4984         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
4985 
4986         // this MUST happen
4987         backendExpectsAnyRequestAndReturn(resp2);
4988 
4989         replayMocks();
4990         execute(req1);
4991         execute(req2);
4992         verifyMocks();
4993     }
4994 
4995     /* "If the no-cache directive does specify one or more field-names, then
4996      * a cache MAY use the response to satisfy a subsequent request, subject
4997      * to any other restrictions on caching. However, the specified
4998      * field-name(s) MUST NOT be sent in the response to a subsequent request
4999      * without successful revalidation with the origin server."
5000      */
5001     @Test
5002     public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exception {
5003         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
5004         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
5005         resp1.setHeader("ETag","\"etag\"");
5006         resp1.setHeader("X-Stuff","things");
5007         resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600");
5008 
5009         backendExpectsAnyRequestAndReturn(resp1);
5010 
5011         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
5012         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
5013         resp2.setHeader("ETag","\"etag\"");
5014         resp2.setHeader("X-Stuff","things");
5015         resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600");
5016 
5017         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
5018         EasyMock.expect(
5019                 mockExecChain.proceed(
5020                         EasyMock.capture(cap),
5021                         EasyMock.isA(ExecChain.Scope.class))).andReturn(resp2).times(0,1);
5022 
5023         replayMocks();
5024         execute(req1);
5025         final ClassicHttpResponse result = execute(req2);
5026         verifyMocks();
5027 
5028         if (!cap.hasCaptured()) {
5029             Assert.assertNull(result.getFirstHeader("X-Stuff"));
5030         }
5031     }
5032 
5033     /* "The purpose of the no-store directive is to prevent the inadvertent
5034      * release or retention of sensitive information (for example, on backup
5035      * tapes). The no-store directive applies to the entire message, and MAY
5036      * be sent either in a response or in a request. If sent in a request, a
5037      * cache MUST NOT store any part of either this request or any response
5038      * to it. If sent in a response, a cache MUST NOT store any part of
5039      * either this response or the request that elicited it. This directive
5040      * applies to both non- shared and shared caches. "MUST NOT store" in
5041      * this context means that the cache MUST NOT intentionally store the
5042      * information in non-volatile storage, and MUST make a best-effort
5043      * attempt to remove the information from volatile storage as promptly
5044      * as possible after forwarding it."
5045      *
5046      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
5047      */
5048     @Test
5049     public void testNoStoreOnRequestIsNotStoredInCache() throws Exception {
5050         emptyMockCacheExpectsNoPuts();
5051         request.setHeader("Cache-Control","no-store");
5052         backendExpectsAnyRequest().andReturn(originResponse);
5053 
5054         replayMocks();
5055         execute(request);
5056         verifyMocks();
5057     }
5058 
5059     @Test
5060     public void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable() throws Exception {
5061         emptyMockCacheExpectsNoPuts();
5062         request.setHeader("Cache-Control","no-store");
5063         originResponse.setHeader("Cache-Control","max-age=3600");
5064         backendExpectsAnyRequest().andReturn(originResponse);
5065 
5066         replayMocks();
5067         execute(request);
5068         verifyMocks();
5069     }
5070 
5071     @Test
5072     public void testNoStoreOnResponseIsNotStoredInCache() throws Exception {
5073         emptyMockCacheExpectsNoPuts();
5074         originResponse.setHeader("Cache-Control","no-store");
5075         backendExpectsAnyRequest().andReturn(originResponse);
5076 
5077         replayMocks();
5078         execute(request);
5079         verifyMocks();
5080     }
5081 
5082     @Test
5083     public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators() throws Exception {
5084         emptyMockCacheExpectsNoPuts();
5085         originResponse.setHeader("Cache-Control","no-store,max-age=3600");
5086         backendExpectsAnyRequest().andReturn(originResponse);
5087 
5088         replayMocks();
5089         execute(request);
5090         verifyMocks();
5091     }
5092 
5093     /* "If multiple encodings have been applied to an entity, the content
5094      * codings MUST be listed in the order in which they were applied."
5095      *
5096      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
5097      */
5098     @Test
5099     public void testOrderOfMultipleContentEncodingHeaderValuesIsPreserved() throws Exception {
5100         originResponse.addHeader("Content-Encoding","gzip");
5101         originResponse.addHeader("Content-Encoding","deflate");
5102         backendExpectsAnyRequest().andReturn(originResponse);
5103 
5104         replayMocks();
5105         final ClassicHttpResponse result = execute(request);
5106         verifyMocks();
5107         int total_encodings = 0;
5108         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
5109         while (it.hasNext()) {
5110             final HeaderElement elt = it.next();
5111             switch(total_encodings) {
5112                 case 0:
5113                     Assert.assertEquals("gzip", elt.getName());
5114                     break;
5115                 case 1:
5116                     Assert.assertEquals("deflate", elt.getName());
5117                     break;
5118                 default:
5119                     Assert.fail("too many encodings");
5120             }
5121             total_encodings++;
5122         }
5123         Assert.assertEquals(2, total_encodings);
5124     }
5125 
5126     @Test
5127     public void testOrderOfMultipleParametersInContentEncodingHeaderIsPreserved() throws Exception {
5128         originResponse.addHeader("Content-Encoding","gzip,deflate");
5129         backendExpectsAnyRequest().andReturn(originResponse);
5130 
5131         replayMocks();
5132         final ClassicHttpResponse result = execute(request);
5133         verifyMocks();
5134         int total_encodings = 0;
5135         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
5136         while (it.hasNext()) {
5137             final HeaderElement elt = it.next();
5138             switch(total_encodings) {
5139                 case 0:
5140                     Assert.assertEquals("gzip", elt.getName());
5141                     break;
5142                 case 1:
5143                     Assert.assertEquals("deflate", elt.getName());
5144                     break;
5145                 default:
5146                     Assert.fail("too many encodings");
5147             }
5148             total_encodings++;
5149         }
5150         Assert.assertEquals(2, total_encodings);
5151     }
5152 
5153     /* "A cache cannot assume that an entity with a Content-Location
5154      * different from the URI used to retrieve it can be used to respond
5155      * to later requests on that Content-Location URI."
5156      *
5157      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.14
5158      */
5159     @Test
5160     public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheableResource() throws Exception {
5161         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/foo");
5162         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
5163         resp1.setHeader("Cache-Control","public,max-age=3600");
5164         resp1.setHeader("Etag","\"etag\"");
5165         resp1.setHeader("Content-Location","http://foo.example.com/bar");
5166 
5167         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/bar");
5168         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
5169         resp2.setHeader("Cache-Control","public,max-age=3600");
5170         resp2.setHeader("Etag","\"etag\"");
5171 
5172         backendExpectsAnyRequestAndReturn(resp1);
5173         backendExpectsAnyRequestAndReturn(resp2);
5174 
5175         replayMocks();
5176         execute(req1);
5177         execute(req2);
5178         verifyMocks();
5179     }
5180 
5181     /* "A received message that does not have a Date header field MUST be
5182      * assigned one by the recipient if the message will be cached by that
5183      * recipient or gatewayed via a protocol which requires a Date."
5184      *
5185      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18
5186      */
5187     @Test
5188     public void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne() throws Exception {
5189         originResponse.removeHeaders("Date");
5190         originResponse.setHeader("Cache-Control","public");
5191         originResponse.setHeader("ETag","\"etag\"");
5192 
5193         backendExpectsAnyRequest().andReturn(originResponse);
5194 
5195         replayMocks();
5196         final ClassicHttpResponse result = execute(request);
5197         verifyMocks();
5198         Assert.assertNotNull(result.getFirstHeader("Date"));
5199     }
5200 
5201     /* "The Expires entity-header field gives the date/time after which the
5202      * response is considered stale.... HTTP/1.1 clients and caches MUST
5203      * treat other invalid date formats, especially including the value '0',
5204      * as in the past (i.e., 'already expired')."
5205      *
5206      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
5207      */
5208     private void testInvalidExpiresHeaderIsTreatedAsStale(
5209             final String expiresHeader) throws Exception {
5210         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
5211         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
5212         resp1.setHeader("Cache-Control","public");
5213         resp1.setHeader("ETag","\"etag\"");
5214         resp1.setHeader("Expires", expiresHeader);
5215 
5216         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
5217         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
5218 
5219         backendExpectsAnyRequestAndReturn(resp1);
5220         // second request to origin MUST happen
5221         backendExpectsAnyRequestAndReturn(resp2);
5222 
5223         replayMocks();
5224         execute(req1);
5225         execute(req2);
5226         verifyMocks();
5227     }
5228 
5229     @Test
5230     public void testMalformedExpiresHeaderIsTreatedAsStale() throws Exception {
5231         testInvalidExpiresHeaderIsTreatedAsStale("garbage");
5232     }
5233 
5234     @Test
5235     public void testExpiresZeroHeaderIsTreatedAsStale() throws Exception {
5236         testInvalidExpiresHeaderIsTreatedAsStale("0");
5237     }
5238 
5239     /* "To mark a response as 'already expired,' an origin server sends
5240      * an Expires date that is equal to the Date header value."
5241      *
5242      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
5243      */
5244     @Test
5245     public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale() throws Exception {
5246         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
5247         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
5248         resp1.setHeader("Cache-Control","public");
5249         resp1.setHeader("ETag","\"etag\"");
5250         resp1.setHeader("Expires", resp1.getFirstHeader("Date").getValue());
5251 
5252         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
5253         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
5254 
5255         backendExpectsAnyRequestAndReturn(resp1);
5256         // second request to origin MUST happen
5257         backendExpectsAnyRequestAndReturn(resp2);
5258 
5259         replayMocks();
5260         execute(req1);
5261         execute(req2);
5262         verifyMocks();
5263     }
5264 
5265     /* "If the response is being forwarded through a proxy, the proxy
5266      * application MUST NOT modify the Server response-header."
5267      *
5268      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.38
5269      */
5270     @Test
5271     public void testDoesNotModifyServerResponseHeader() throws Exception {
5272         final String server = "MockServer/1.0";
5273         originResponse.setHeader("Server", server);
5274 
5275         backendExpectsAnyRequest().andReturn(originResponse);
5276 
5277         replayMocks();
5278         final ClassicHttpResponse result = execute(request);
5279         verifyMocks();
5280         Assert.assertEquals(server, result.getFirstHeader("Server").getValue());
5281     }
5282 
5283     /* "If multiple encodings have been applied to an entity, the transfer-
5284      * codings MUST be listed in the order in which they were applied."
5285      *
5286      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41
5287      */
5288     @Test
5289     public void testOrderOfMultipleTransferEncodingHeadersIsPreserved() throws Exception {
5290         originResponse.addHeader("Transfer-Encoding","chunked");
5291         originResponse.addHeader("Transfer-Encoding","x-transfer");
5292 
5293         backendExpectsAnyRequest().andReturn(originResponse);
5294 
5295         replayMocks();
5296         final ClassicHttpResponse result = execute(request);
5297         verifyMocks();
5298         int transfer_encodings = 0;
5299         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.TRANSFER_ENCODING);
5300         while (it.hasNext()) {
5301             final HeaderElement elt = it.next();
5302             switch(transfer_encodings) {
5303                 case 0:
5304                     Assert.assertEquals("chunked",elt.getName());
5305                     break;
5306                 case 1:
5307                     Assert.assertEquals("x-transfer",elt.getName());
5308                     break;
5309                 default:
5310                     Assert.fail("too many transfer encodings");
5311             }
5312             transfer_encodings++;
5313         }
5314         Assert.assertEquals(2, transfer_encodings);
5315     }
5316 
5317     @Test
5318     public void testOrderOfMultipleTransferEncodingsInSingleHeadersIsPreserved() throws Exception {
5319         originResponse.addHeader("Transfer-Encoding","chunked, x-transfer");
5320 
5321         backendExpectsAnyRequest().andReturn(originResponse);
5322 
5323         replayMocks();
5324         final ClassicHttpResponse result = execute(request);
5325         verifyMocks();
5326         int transfer_encodings = 0;
5327         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.TRANSFER_ENCODING);
5328         while (it.hasNext()) {
5329             final HeaderElement elt = it.next();
5330             switch(transfer_encodings) {
5331                 case 0:
5332                     Assert.assertEquals("chunked",elt.getName());
5333                     break;
5334                 case 1:
5335                     Assert.assertEquals("x-transfer",elt.getName());
5336                     break;
5337                 default:
5338                     Assert.fail("too many transfer encodings");
5339             }
5340             transfer_encodings++;
5341         }
5342         Assert.assertEquals(2, transfer_encodings);
5343     }
5344 
5345     /* "A Vary field value of '*' signals that unspecified parameters
5346      * not limited to the request-headers (e.g., the network address
5347      * of the client), play a role in the selection of the response
5348      * representation. The '*' value MUST NOT be generated by a proxy
5349      * server; it may only be generated by an origin server."
5350      *
5351      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
5352      */
5353     @Test
5354     public void testVaryStarIsNotGeneratedByProxy() throws Exception {
5355         request.setHeader("User-Agent","my-agent/1.0");
5356         originResponse.setHeader("Cache-Control","public, max-age=3600");
5357         originResponse.setHeader("Vary","User-Agent");
5358         originResponse.setHeader("ETag","\"etag\"");
5359 
5360         backendExpectsAnyRequest().andReturn(originResponse);
5361 
5362         replayMocks();
5363         final ClassicHttpResponse result = execute(request);
5364         verifyMocks();
5365         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.VARY);
5366         while (it.hasNext()) {
5367             final HeaderElement elt = it.next();
5368             Assert.assertFalse("*".equals(elt.getName()));
5369         }
5370     }
5371 
5372     /* "The Via general-header field MUST be used by gateways and proxies
5373      * to indicate the intermediate protocols and recipients between the
5374      * user agent and the server on requests, and between the origin server
5375      * and the client on responses."
5376      *
5377      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
5378      */
5379     @Test
5380     public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception {
5381         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
5382         request.removeHeaders("Via");
5383         EasyMock.expect(
5384                 mockExecChain.proceed(
5385                         EasyMock.capture(cap),
5386                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
5387 
5388         replayMocks();
5389         execute(request);
5390         verifyMocks();
5391 
5392         final ClassicHttpRequest captured = cap.getValue();
5393         final String via = captured.getFirstHeader("Via").getValue();
5394         assertValidViaHeader(via);
5395     }
5396 
5397     @Test
5398     public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception {
5399         originResponse.removeHeaders("Via");
5400         backendExpectsAnyRequest().andReturn(originResponse);
5401         replayMocks();
5402         final ClassicHttpResponse result = execute(request);
5403         verifyMocks();
5404         assertValidViaHeader(result.getFirstHeader("Via").getValue());
5405     }
5406 
5407 
5408     private void assertValidViaHeader(final String via) {
5409         //        Via =  "Via" ":" 1#( received-protocol received-by [ comment ] )
5410         //        received-protocol = [ protocol-name "/" ] protocol-version
5411         //        protocol-name     = token
5412         //        protocol-version  = token
5413         //        received-by       = ( host [ ":" port ] ) | pseudonym
5414         //        pseudonym         = token
5415 
5416         final String[] parts = via.split("\\s+");
5417         Assert.assertTrue(parts.length >= 2);
5418 
5419         // received protocol
5420         final String receivedProtocol = parts[0];
5421         final String[] protocolParts = receivedProtocol.split("/");
5422         Assert.assertTrue(protocolParts.length >= 1);
5423         Assert.assertTrue(protocolParts.length <= 2);
5424 
5425         final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+";
5426         for(final String protocolPart : protocolParts) {
5427             Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart));
5428         }
5429 
5430         // received-by
5431         if (!Pattern.matches(tokenRegexp, parts[1])) {
5432             // host : port
5433             new HttpHost(parts[1]); // TODO - unused - is this a test bug? else use Assert.assertNotNull
5434         }
5435 
5436         // comment
5437         if (parts.length > 2) {
5438             final StringBuilder buf = new StringBuilder(parts[2]);
5439             for(int i=3; i<parts.length; i++) {
5440                 buf.append(" "); buf.append(parts[i]);
5441             }
5442             Assert.assertTrue(isValidComment(buf.toString()));
5443         }
5444     }
5445 
5446     private boolean isValidComment(final String s) {
5447         final String leafComment = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
5448         final String nestedPrefix = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\(";
5449         final String nestedSuffix = "\\)([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
5450 
5451         if (Pattern.matches(leafComment,s)) {
5452             return true;
5453         }
5454         final Matcher pref = Pattern.compile(nestedPrefix).matcher(s);
5455         final Matcher suff = Pattern.compile(nestedSuffix).matcher(s);
5456         if (!pref.find()) {
5457             return false;
5458         }
5459         if (!suff.find()) {
5460             return false;
5461         }
5462         return isValidComment(s.substring(pref.end() - 1, suff.start() + 1));
5463     }
5464 
5465 
5466     /*
5467      * "The received-protocol indicates the protocol version of the message
5468      * received by the server or client along each segment of the request/
5469      * response chain. The received-protocol version is appended to the Via
5470      * field value when the message is forwarded so that information about
5471      * the protocol capabilities of upstream applications remains visible
5472      * to all recipients."
5473      *
5474      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
5475      */
5476     @Test
5477     public void testViaHeaderOnRequestProperlyRecordsClientProtocol() throws Exception {
5478         final ClassicHttpRequest originalRequest = new BasicClassicHttpRequest("GET", "/");
5479         originalRequest.setVersion(HttpVersion.HTTP_1_0);
5480         request = originalRequest;
5481         request.removeHeaders("Via");
5482         final Capture<ClassicHttpRequest> cap = EasyMock.newCapture();
5483         EasyMock.expect(
5484                 mockExecChain.proceed(
5485                         EasyMock.capture(cap),
5486                         EasyMock.isA(ExecChain.Scope.class))).andReturn(originResponse);
5487 
5488         replayMocks();
5489         execute(request);
5490         verifyMocks();
5491 
5492         final ClassicHttpRequest captured = cap.getValue();
5493         final String via = captured.getFirstHeader("Via").getValue();
5494         final String protocol = via.split("\\s+")[0];
5495         final String[] protoParts = protocol.split("/");
5496         if (protoParts.length > 1) {
5497             Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
5498         }
5499         Assert.assertEquals("1.0",protoParts[protoParts.length-1]);
5500     }
5501 
5502     @Test
5503     public void testViaHeaderOnResponseProperlyRecordsOriginProtocol() throws Exception {
5504 
5505         originResponse = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
5506         originResponse.setVersion(HttpVersion.HTTP_1_0);
5507 
5508         backendExpectsAnyRequest().andReturn(originResponse);
5509 
5510         replayMocks();
5511         final ClassicHttpResponse result = execute(request);
5512         verifyMocks();
5513 
5514         final String via = result.getFirstHeader("Via").getValue();
5515         final String protocol = via.split("\\s+")[0];
5516         final String[] protoParts = protocol.split("/");
5517         Assert.assertTrue(protoParts.length >= 1);
5518         Assert.assertTrue(protoParts.length <= 2);
5519         if (protoParts.length > 1) {
5520             Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
5521         }
5522         Assert.assertEquals("1.0", protoParts[protoParts.length - 1]);
5523     }
5524 
5525     /* "A cache MUST NOT delete any Warning header that it received with
5526      * a message."
5527      *
5528      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
5529      */
5530     @Test
5531     public void testRetainsWarningHeadersReceivedFromUpstream() throws Exception {
5532         originResponse.removeHeaders("Warning");
5533         final String warning = "199 fred \"misc\"";
5534         originResponse.addHeader("Warning", warning);
5535         backendExpectsAnyRequest().andReturn(originResponse);
5536 
5537         replayMocks();
5538         final ClassicHttpResponse result = execute(request);
5539         verifyMocks();
5540         Assert.assertEquals(warning,
5541                 result.getFirstHeader("Warning").getValue());
5542     }
5543 
5544     /* "However, if a cache successfully validates a cache entry, it
5545      * SHOULD remove any Warning headers previously attached to that
5546      * entry except as specified for specific Warning codes. It MUST
5547      * then add any Warning headers received in the validating response."
5548      *
5549      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
5550      */
5551     @Test
5552     public void testUpdatesWarningHeadersOnValidation() throws Exception {
5553         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
5554         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
5555 
5556         final Date now = new Date();
5557         final Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
5558         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
5559         resp1.setHeader("Date", DateUtils.formatDate(twentySecondsAgo));
5560         resp1.setHeader("Cache-Control","public,max-age=5");
5561         resp1.setHeader("ETag", "\"etag1\"");
5562         final String oldWarning = "113 wilma \"stale\"";
5563         resp1.setHeader("Warning", oldWarning);
5564 
5565         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5566         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
5567         resp2.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
5568         resp2.setHeader("ETag", "\"etag1\"");
5569         final String newWarning = "113 betty \"stale too\"";
5570         resp2.setHeader("Warning", newWarning);
5571 
5572         backendExpectsAnyRequestAndReturn(resp1);
5573         backendExpectsAnyRequestAndReturn(resp2);
5574 
5575         replayMocks();
5576         execute(req1);
5577         final ClassicHttpResponse result = execute(req2);
5578         verifyMocks();
5579 
5580         boolean oldWarningFound = false;
5581         boolean newWarningFound = false;
5582         for(final Header h : result.getHeaders("Warning")) {
5583             for(final String warnValue : h.getValue().split("\\s*,\\s*")) {
5584                 if (oldWarning.equals(warnValue)) {
5585                     oldWarningFound = true;
5586                 } else if (newWarning.equals(warnValue)) {
5587                     newWarningFound = true;
5588                 }
5589             }
5590         }
5591         Assert.assertFalse(oldWarningFound);
5592         Assert.assertTrue(newWarningFound);
5593     }
5594 
5595     /* "If an implementation sends a message with one or more Warning
5596      * headers whose version is HTTP/1.0 or lower, then the sender MUST
5597      * include in each warning-value a warn-date that matches the date
5598      * in the response."
5599      *
5600      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
5601      */
5602     @Test
5603     public void testWarnDatesAreAddedToWarningsOnLowerProtocolVersions() throws Exception {
5604         final String dateHdr = DateUtils.formatDate(new Date());
5605         final String origWarning = "110 fred \"stale\"";
5606         originResponse.setCode(HttpStatus.SC_OK);
5607         originResponse.setVersion(HttpVersion.HTTP_1_0);
5608         originResponse.addHeader("Warning", origWarning);
5609         originResponse.setHeader("Date", dateHdr);
5610         backendExpectsAnyRequest().andReturn(originResponse);
5611         replayMocks();
5612         final ClassicHttpResponse result = execute(request);
5613         verifyMocks();
5614         // note that currently the implementation acts as an HTTP/1.1 proxy,
5615         // which means that all the responses from the caching module should
5616         // be HTTP/1.1, so we won't actually be testing anything here until
5617         // that changes.
5618         if (HttpVersion.HTTP_1_0.greaterEquals(result.getVersion())) {
5619             Assert.assertEquals(dateHdr, result.getFirstHeader("Date").getValue());
5620             boolean warningFound = false;
5621             final String targetWarning = origWarning + " \"" + dateHdr + "\"";
5622             for(final Header h : result.getHeaders("Warning")) {
5623                 for(final String warning : h.getValue().split("\\s*,\\s*")) {
5624                     if (targetWarning.equals(warning)) {
5625                         warningFound = true;
5626                         break;
5627                     }
5628                 }
5629             }
5630             Assert.assertTrue(warningFound);
5631         }
5632     }
5633 
5634     /* "If an implementation receives a message with a warning-value that
5635      * includes a warn-date, and that warn-date is different from the Date
5636      * value in the response, then that warning-value MUST be deleted from
5637      * the message before storing, forwarding, or using it. (This prevents
5638      * bad consequences of naive caching of Warning header fields.) If all
5639      * of the warning-values are deleted for this reason, the Warning
5640      * header MUST be deleted as well."
5641      *
5642      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
5643      */
5644     @Test
5645     public void testStripsBadlyDatedWarningsFromForwardedResponses() throws Exception {
5646         final Date now = new Date();
5647         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5648         originResponse.setHeader("Date", DateUtils.formatDate(now));
5649         originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
5650                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
5651         originResponse.setHeader("Cache-Control","no-cache,no-store");
5652         backendExpectsAnyRequest().andReturn(originResponse);
5653 
5654         replayMocks();
5655         final ClassicHttpResponse result = execute(request);
5656         verifyMocks();
5657 
5658         for(final Header h : result.getHeaders("Warning")) {
5659             Assert.assertFalse(h.getValue().contains("wilma"));
5660         }
5661     }
5662 
5663     @Test
5664     public void testStripsBadlyDatedWarningsFromStoredResponses() throws Exception {
5665         final Date now = new Date();
5666         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5667         originResponse.setHeader("Date", DateUtils.formatDate(now));
5668         originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
5669                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
5670         originResponse.setHeader("Cache-Control","public,max-age=3600");
5671         backendExpectsAnyRequest().andReturn(originResponse);
5672 
5673         replayMocks();
5674         final ClassicHttpResponse result = execute(request);
5675         verifyMocks();
5676 
5677         for(final Header h : result.getHeaders("Warning")) {
5678             Assert.assertFalse(h.getValue().contains("wilma"));
5679         }
5680     }
5681 
5682     @Test
5683     public void testRemovesWarningHeaderIfAllWarnValuesAreBadlyDated() throws Exception {
5684         final Date now = new Date();
5685         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5686         originResponse.setHeader("Date", DateUtils.formatDate(now));
5687         originResponse.addHeader("Warning", "110 wilma \"stale\" \""
5688                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
5689         backendExpectsAnyRequest().andReturn(originResponse);
5690 
5691         replayMocks();
5692         final ClassicHttpResponse result = execute(request);
5693         verifyMocks();
5694 
5695         final Header[] warningHeaders = result.getHeaders("Warning");
5696         Assert.assertTrue(warningHeaders == null || warningHeaders.length == 0);
5697     }
5698 
5699 }