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.http.impl.client.cache;
28  
29  import java.util.Date;
30  import java.util.Random;
31  
32  import org.apache.http.HttpEntity;
33  import org.apache.http.HttpEntityEnclosingRequest;
34  import org.apache.http.HttpHost;
35  import org.apache.http.HttpRequest;
36  import org.apache.http.HttpResponse;
37  import org.apache.http.HttpStatus;
38  import org.apache.http.ProtocolVersion;
39  import org.apache.http.client.ClientProtocolException;
40  import org.apache.http.client.cache.HttpCacheContext;
41  import org.apache.http.client.methods.CloseableHttpResponse;
42  import org.apache.http.client.methods.HttpExecutionAware;
43  import org.apache.http.client.methods.HttpRequestWrapper;
44  import org.apache.http.client.protocol.HttpClientContext;
45  import org.apache.http.client.utils.DateUtils;
46  import org.apache.http.conn.routing.HttpRoute;
47  import org.apache.http.entity.ByteArrayEntity;
48  import org.apache.http.impl.execchain.ClientExecChain;
49  import org.apache.http.message.BasicHttpEntityEnclosingRequest;
50  import org.apache.http.message.BasicHttpRequest;
51  import org.apache.http.message.BasicHttpResponse;
52  import org.easymock.Capture;
53  import org.easymock.EasyMock;
54  import org.junit.Assert;
55  import org.junit.Before;
56  import org.junit.Ignore;
57  import org.junit.Test;
58  
59  /**
60   * We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
61   * of the rules for proxies apply to us, as far as proper operation of the
62   * requests that pass through us. Generally speaking, we want to make sure that
63   * any response returned from our HttpClient.execute() methods is conditionally
64   * compliant with the rules for an HTTP/1.1 server, and that any requests we
65   * pass downstream to the backend HttpClient are are conditionally compliant
66   * with the rules for an HTTP/1.1 client.
67   *
68   * There are some cases where strictly behaving as a compliant caching proxy
69   * would result in strange behavior, since we're attached as part of a client
70   * and are expected to be a drop-in replacement. The test cases captured here
71   * document the places where we differ from the HTTP RFC.
72   */
73  @SuppressWarnings("boxing") // test code
74  public class TestProtocolDeviations {
75  
76      private static ProtocolVersion HTTP_1_1 = new ProtocolVersion("HTTP", 1, 1);
77  
78      private static final int MAX_BYTES = 1024;
79      private static final int MAX_ENTRIES = 100;
80      private final int entityLength = 128;
81  
82      private HttpHost host;
83      private HttpRoute route;
84      private HttpEntity body;
85      private HttpEntity mockEntity;
86      private ClientExecChain mockBackend;
87      private HttpCache mockCache;
88      private HttpRequest request;
89      private HttpCacheContext context;
90      private CloseableHttpResponse originResponse;
91  
92      private ClientExecChain impl;
93  
94      @Before
95      public void setUp() {
96          host = new HttpHost("foo.example.com", 80);
97  
98          route = new HttpRoute(host);
99  
100         body = makeBody(entityLength);
101 
102         request = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
103 
104         context = HttpCacheContext.create();
105         context.setTargetHost(host);
106 
107         originResponse = Proxies.enhanceResponse(make200Response());
108 
109         final CacheConfig config = CacheConfig.custom()
110                 .setMaxCacheEntries(MAX_ENTRIES)
111                 .setMaxObjectSize(MAX_BYTES)
112                 .build();
113 
114         final HttpCache cache = new BasicHttpCache(config);
115         mockBackend = EasyMock.createNiceMock(ClientExecChain.class);
116         mockEntity = EasyMock.createNiceMock(HttpEntity.class);
117         mockCache = EasyMock.createNiceMock(HttpCache.class);
118 
119         impl = createCachingExecChain(mockBackend, cache, config);
120     }
121 
122     protected ClientExecChain createCachingExecChain(
123             final ClientExecChain backend, final HttpCache cache, final CacheConfig config) {
124         return new CachingExec(backend, cache, config);
125     }
126 
127     private HttpResponse make200Response() {
128         final HttpResponse out = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
129         out.setHeader("Date", DateUtils.formatDate(new Date()));
130         out.setHeader("Server", "MockOrigin/1.0");
131         out.setEntity(makeBody(128));
132         return out;
133     }
134 
135     private void replayMocks() {
136         EasyMock.replay(mockBackend);
137         EasyMock.replay(mockCache);
138         EasyMock.replay(mockEntity);
139     }
140 
141     private void verifyMocks() {
142         EasyMock.verify(mockBackend);
143         EasyMock.verify(mockCache);
144         EasyMock.verify(mockEntity);
145     }
146 
147     private HttpEntity makeBody(final int nbytes) {
148         final byte[] bytes = new byte[nbytes];
149         new Random().nextBytes(bytes);
150         return new ByteArrayEntity(bytes);
151     }
152 
153     public static HttpRequest eqRequest(final HttpRequest in) {
154         org.easymock.EasyMock.reportMatcher(new RequestEquivalent(in));
155         return null;
156     }
157 
158     /*
159      * "For compatibility with HTTP/1.0 applications, HTTP/1.1 requests
160      * containing a message-body MUST include a valid Content-Length header
161      * field unless the server is known to be HTTP/1.1 compliant. If a request
162      * contains a message-body and a Content-Length is not given, the server
163      * SHOULD respond with 400 (bad request) if it cannot determine the length
164      * of the message, or with 411 (length required) if it wishes to insist on
165      * receiving a valid Content-Length."
166      *
167      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
168      *
169      * 8/23/2010 JRC - This test has been moved to Ignore.  The caching client
170      * was set to return status code 411 on a missing content-length header when
171      * a request had a body.  It seems that somewhere deeper in the client stack
172      * this header is added automatically for us - so the caching client shouldn't
173      * specifically be worried about this requirement.
174      */
175     @Ignore
176     public void testHTTP1_1RequestsWithBodiesOfKnownLengthMustHaveContentLength() throws Exception {
177         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
178                 HTTP_1_1);
179         post.setEntity(mockEntity);
180 
181         replayMocks();
182 
183         final HttpResponse response = impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
184 
185         verifyMocks();
186 
187         Assert
188                 .assertEquals(HttpStatus.SC_LENGTH_REQUIRED, response.getStatusLine()
189                         .getStatusCode());
190     }
191 
192     /*
193      * Discussion: if an incoming request has a body, but the HttpEntity
194      * attached has an unknown length (meaning entity.getContentLength() is
195      * negative), we have two choices if we want to be conditionally compliant.
196      * (1) we can slurp the whole body into a bytearray and compute its length
197      * before sending; or (2) we can push responsibility for (1) back onto the
198      * client by just generating a 411 response
199      *
200      * There is a third option, which is that we delegate responsibility for (1)
201      * onto the backend HttpClient, but because that is an injected dependency,
202      * we can't rely on it necessarily being conditionally compliant with
203      * HTTP/1.1. Currently, option (2) seems like the safest bet, as this
204      * exposes to the client application that the slurping required for (1)
205      * needs to happen in order to compute the content length.
206      *
207      * In any event, this test just captures the behavior required.
208      *
209      * 8/23/2010 JRC - This test has been moved to Ignore.  The caching client
210      * was set to return status code 411 on a missing content-length header when
211      * a request had a body.  It seems that somewhere deeper in the client stack
212      * this header is added automatically for us - so the caching client shouldn't
213      * specifically be worried about this requirement.
214      */
215     @Ignore
216     public void testHTTP1_1RequestsWithUnknownBodyLengthAreRejectedOrHaveContentLengthAdded()
217             throws Exception {
218         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
219                 HTTP_1_1);
220 
221         final byte[] bytes = new byte[128];
222         new Random().nextBytes(bytes);
223 
224         final HttpEntity mockBody = org.easymock.classextension.EasyMock.createMockBuilder(ByteArrayEntity.class).withConstructor(
225                 new Object[] { bytes }).addMockedMethods("getContentLength").createNiceMock();
226         org.easymock.EasyMock.expect(mockBody.getContentLength()).andReturn(-1L).anyTimes();
227         post.setEntity(mockBody);
228 
229         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
230         EasyMock.expect(
231                 mockBackend.execute(
232                         EasyMock.eq(route),
233                         EasyMock.capture(reqCap),
234                         EasyMock.isA(HttpClientContext.class),
235                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
236                                 originResponse).times(0, 1);
237 
238         replayMocks();
239         EasyMock.replay(mockBody);
240 
241         final HttpResponse result = impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
242 
243         verifyMocks();
244         EasyMock.verify(mockBody);
245 
246         if (reqCap.hasCaptured()) {
247             // backend request was made
248             final HttpRequest forwarded = reqCap.getValue();
249             Assert.assertNotNull(forwarded.getFirstHeader("Content-Length"));
250         } else {
251             final int status = result.getStatusLine().getStatusCode();
252             Assert.assertTrue(HttpStatus.SC_LENGTH_REQUIRED == status
253                     || HttpStatus.SC_BAD_REQUEST == status);
254         }
255     }
256 
257     /*
258      * "If the OPTIONS request includes an entity-body (as indicated by the
259      * presence of Content-Length or Transfer-Encoding), then the media type
260      * MUST be indicated by a Content-Type field."
261      *
262      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
263      */
264     @Test
265     public void testOPTIONSRequestsWithBodiesAndNoContentTypeHaveOneSupplied() throws Exception {
266         final BasicHttpEntityEnclosingRequest options = new BasicHttpEntityEnclosingRequest("OPTIONS",
267                 "/", HTTP_1_1);
268         options.setEntity(body);
269         options.setHeader("Content-Length", "1");
270 
271         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
272         EasyMock.expect(
273                 mockBackend.execute(
274                         EasyMock.eq(route),
275                         EasyMock.capture(reqCap),
276                         EasyMock.isA(HttpClientContext.class),
277                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
278         replayMocks();
279 
280         impl.execute(route, HttpRequestWrapper.wrap(options), context, null);
281 
282         verifyMocks();
283 
284         final HttpRequest forwarded = reqCap.getValue();
285         Assert.assertTrue(forwarded instanceof HttpEntityEnclosingRequest);
286         final HttpEntityEnclosingRequest reqWithBody = (HttpEntityEnclosingRequest) forwarded;
287         final HttpEntity reqBody = reqWithBody.getEntity();
288         Assert.assertNotNull(reqBody);
289         Assert.assertNotNull(reqBody.getContentType());
290     }
291 
292     /*
293      * "10.2.7 206 Partial Content ... The request MUST have included a Range
294      * header field (section 14.35) indicating the desired range, and MAY have
295      * included an If-Range header field (section 14.27) to make the request
296      * conditional."
297      *
298      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
299      */
300     @Test
301     public void testPartialContentIsNotReturnedToAClientThatDidNotAskForIt() throws Exception {
302 
303         // tester's note: I don't know what the cache will *do* in
304         // this situation, but it better not just pass the response
305         // on.
306         request.removeHeaders("Range");
307         originResponse = Proxies.enhanceResponse(
308                 new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
309                 "Partial Content"));
310         originResponse.setHeader("Content-Range", "bytes 0-499/1234");
311         originResponse.setEntity(makeBody(500));
312 
313         EasyMock.expect(
314                 mockBackend.execute(
315                         EasyMock.eq(route),
316                         EasyMock.isA(HttpRequestWrapper.class),
317                         EasyMock.isA(HttpClientContext.class),
318                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
319 
320         replayMocks();
321         try {
322             final HttpResponse result = impl.execute(route, HttpRequestWrapper.wrap(request), context, null);
323             Assert.assertTrue(HttpStatus.SC_PARTIAL_CONTENT != result.getStatusLine()
324                     .getStatusCode());
325         } catch (final ClientProtocolException acceptableBehavior) {
326             // this is probably ok
327         }
328     }
329 
330     /*
331      * "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
332      * header field (section 14.47) containing a challenge applicable to the
333      * requested resource."
334      *
335      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
336      */
337     @Test
338     public void testPassesOnOrigin401ResponseWithoutWWWAuthenticateHeader() throws Exception {
339 
340         originResponse = Proxies.enhanceResponse(
341                 new BasicHttpResponse(HTTP_1_1, 401, "Unauthorized"));
342 
343         EasyMock.expect(
344                 mockBackend.execute(
345                         EasyMock.eq(route),
346                         EasyMock.isA(HttpRequestWrapper.class),
347                         EasyMock.isA(HttpClientContext.class),
348                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
349         replayMocks();
350         final HttpResponse result = impl.execute(route, HttpRequestWrapper.wrap(request), context, null);
351         verifyMocks();
352         Assert.assertSame(originResponse, result);
353     }
354 
355     /*
356      * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
357      * header containing a list of valid methods for the requested resource.
358      *
359      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
360      */
361     @Test
362     public void testPassesOnOrigin405WithoutAllowHeader() throws Exception {
363         originResponse = Proxies.enhanceResponse(
364                 new BasicHttpResponse(HTTP_1_1, 405, "Method Not Allowed"));
365 
366         EasyMock.expect(
367                 mockBackend.execute(
368                         EasyMock.eq(route),
369                         EasyMock.isA(HttpRequestWrapper.class),
370                         EasyMock.isA(HttpClientContext.class),
371                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
372         replayMocks();
373         final HttpResponse result = impl.execute(route, HttpRequestWrapper.wrap(request), context, null);
374         verifyMocks();
375         Assert.assertSame(originResponse, result);
376     }
377 
378     /*
379      * "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
380      * Proxy-Authenticate header field (section 14.33) containing a challenge
381      * applicable to the proxy for the requested resource."
382      *
383      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
384      */
385     @Test
386     public void testPassesOnOrigin407WithoutAProxyAuthenticateHeader() throws Exception {
387         originResponse = Proxies.enhanceResponse(
388                 new BasicHttpResponse(HTTP_1_1, 407, "Proxy Authentication Required"));
389 
390         EasyMock.expect(
391                 mockBackend.execute(
392                         EasyMock.eq(route),
393                         EasyMock.isA(HttpRequestWrapper.class),
394                         EasyMock.isA(HttpClientContext.class),
395                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
396         replayMocks();
397         final HttpResponse result = impl.execute(route, HttpRequestWrapper.wrap(request), context, null);
398         verifyMocks();
399         Assert.assertSame(originResponse, result);
400     }
401 
402 }