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