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  
28  package org.apache.hc.client5.testing.sync;
29  
30  import java.io.ByteArrayInputStream;
31  import java.io.ByteArrayOutputStream;
32  import java.io.IOException;
33  import java.io.OutputStream;
34  import java.nio.charset.StandardCharsets;
35  import java.util.ArrayList;
36  import java.util.Iterator;
37  import java.util.List;
38  import java.util.concurrent.CountDownLatch;
39  import java.util.concurrent.ExecutorService;
40  import java.util.concurrent.Executors;
41  import java.util.function.Consumer;
42  import java.util.zip.Deflater;
43  import java.util.zip.GZIPOutputStream;
44  
45  import org.apache.hc.client5.http.classic.methods.HttpGet;
46  import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler;
47  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
48  import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
49  import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
50  import org.apache.hc.client5.testing.sync.extension.TestClientResources;
51  import org.apache.hc.core5.http.ClassicHttpRequest;
52  import org.apache.hc.core5.http.ClassicHttpResponse;
53  import org.apache.hc.core5.http.HeaderElement;
54  import org.apache.hc.core5.http.HttpException;
55  import org.apache.hc.core5.http.HttpHost;
56  import org.apache.hc.core5.http.HttpStatus;
57  import org.apache.hc.core5.http.URIScheme;
58  import org.apache.hc.core5.http.io.HttpRequestHandler;
59  import org.apache.hc.core5.http.io.entity.EntityUtils;
60  import org.apache.hc.core5.http.io.entity.InputStreamEntity;
61  import org.apache.hc.core5.http.io.entity.StringEntity;
62  import org.apache.hc.core5.http.message.MessageSupport;
63  import org.apache.hc.core5.http.protocol.HttpContext;
64  import org.apache.hc.core5.testing.classic.ClassicTestServer;
65  import org.apache.hc.core5.util.Timeout;
66  import org.junit.jupiter.api.Assertions;
67  import org.junit.jupiter.api.Test;
68  import org.junit.jupiter.api.extension.RegisterExtension;
69  
70  /**
71   * Test case for how Content Codings are processed. By default, we want to do the right thing and
72   * require no intervention from the user of HttpClient, but we still want to let clients do their
73   * own thing if they so wish.
74   */
75  public abstract class TestContentCodings {
76  
77      public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
78  
79      @RegisterExtension
80      private TestClientResources testResources;
81  
82      protected TestContentCodings(final URIScheme scheme) {
83          this.testResources = new TestClientResources(scheme, TIMEOUT);
84      }
85  
86      public URIScheme scheme() {
87          return testResources.scheme();
88      }
89  
90      public ClassicTestServer startServer() throws IOException {
91          return testResources.startServer(null, null, null);
92      }
93  
94      public CloseableHttpClient startClient(final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
95          return testResources.startClient(clientCustomizer);
96      }
97  
98      public CloseableHttpClient startClient() throws Exception {
99          return testResources.startClient(builder -> {});
100     }
101 
102     public HttpHost targetHost() {
103         return testResources.targetHost();
104     }
105 
106     /**
107      * Test for when we don't get an entity back; e.g. for a 204 or 304 response; nothing blows
108      * up with the new behaviour.
109      *
110      * @throws Exception
111      *             if there was a problem
112      */
113     @Test
114     public void testResponseWithNoContent() throws Exception {
115         final ClassicTestServer server = startServer();
116         server.registerHandler("*", new HttpRequestHandler() {
117 
118             /**
119              * {@inheritDoc}
120              */
121             @Override
122             public void handle(
123                     final ClassicHttpRequest request,
124                     final ClassicHttpResponse response,
125                     final HttpContext context) throws HttpException, IOException {
126                 response.setCode(HttpStatus.SC_NO_CONTENT);
127             }
128         });
129 
130         final HttpHost target = targetHost();
131 
132         final CloseableHttpClient client = startClient();
133 
134         final HttpGet request = new HttpGet("/some-resource");
135         client.execute(target, request, response -> {
136             Assertions.assertEquals(HttpStatus.SC_NO_CONTENT, response.getCode());
137             Assertions.assertNull(response.getEntity());
138             return null;
139         });
140     }
141 
142     /**
143      * Test for when we are handling content from a server that has correctly interpreted RFC2616
144      * to return RFC1950 streams for {@code deflate} content coding.
145      *
146      * @throws Exception
147      */
148     @Test
149     public void testDeflateSupportForServerReturningRfc1950Stream() throws Exception {
150         final String entityText = "Hello, this is some plain text coming back.";
151 
152         final ClassicTestServer server = startServer();
153         server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, false));
154 
155         final HttpHost target = targetHost();
156 
157         final CloseableHttpClient client = startClient();
158 
159         final HttpGet request = new HttpGet("/some-resource");
160         client.execute(target, request, response -> {
161             Assertions.assertEquals(entityText, EntityUtils.toString(response.getEntity()),
162                     "The entity text is correctly transported");
163             return null;
164         });
165     }
166 
167     /**
168      * Test for when we are handling content from a server that has incorrectly interpreted RFC2616
169      * to return RFC1951 streams for {@code deflate} content coding.
170      *
171      * @throws Exception
172      */
173     @Test
174     public void testDeflateSupportForServerReturningRfc1951Stream() throws Exception {
175         final String entityText = "Hello, this is some plain text coming back.";
176 
177         final ClassicTestServer server = startServer();
178         server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, true));
179 
180         final HttpHost target = targetHost();
181 
182         final CloseableHttpClient client = startClient();
183 
184         final HttpGet request = new HttpGet("/some-resource");
185         client.execute(target, request, response -> {
186             Assertions.assertEquals(entityText, EntityUtils.toString(response.getEntity()),
187                     "The entity text is correctly transported");
188             return null;
189         });
190     }
191 
192     /**
193      * Test for a server returning gzipped content.
194      *
195      * @throws Exception
196      */
197     @Test
198     public void testGzipSupport() throws Exception {
199         final String entityText = "Hello, this is some plain text coming back.";
200 
201         final ClassicTestServer server = startServer();
202         server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
203 
204         final HttpHost target = targetHost();
205 
206         final CloseableHttpClient client = startClient();
207 
208         final HttpGet request = new HttpGet("/some-resource");
209         client.execute(target, request, response -> {
210             Assertions.assertEquals(entityText, EntityUtils.toString(response.getEntity()),
211                     "The entity text is correctly transported");
212             return null;
213         });
214     }
215 
216     /**
217      * Try with a bunch of client threads, to check that it's thread-safe.
218      *
219      * @throws Exception
220      *             if there was a problem
221      */
222     @Test
223     public void testThreadSafetyOfContentCodings() throws Exception {
224         final String entityText = "Hello, this is some plain text coming back.";
225 
226         final ClassicTestServer server = startServer();
227         server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
228 
229         final HttpHost target = targetHost();
230 
231         final CloseableHttpClient client = startClient();
232         final PoolingHttpClientConnectionManager connManager = testResources.connManager();
233 
234         /*
235          * Create a load of workers which will access the resource. Half will use the default
236          * gzip behaviour; half will require identity entity.
237          */
238         final int clients = 100;
239 
240         connManager.setMaxTotal(clients);
241 
242         final ExecutorService executor = Executors.newFixedThreadPool(clients);
243 
244         final CountDownLatch startGate = new CountDownLatch(1);
245         final CountDownLatch endGate = new CountDownLatch(clients);
246 
247         final List<WorkerTask> workers = new ArrayList<>();
248 
249         for (int i = 0; i < clients; ++i) {
250             workers.add(new WorkerTask(client, target, i % 2 == 0, startGate, endGate));
251         }
252 
253         for (final WorkerTask workerTask : workers) {
254 
255             /* Set them all in motion, but they will block until we call startGate.countDown(). */
256             executor.execute(workerTask);
257         }
258 
259         startGate.countDown();
260 
261         /* Wait for the workers to complete. */
262         endGate.await();
263 
264         for (final WorkerTask workerTask : workers) {
265             if (workerTask.isFailed()) {
266                 Assertions.fail("A worker failed");
267             }
268             Assertions.assertEquals(entityText, workerTask.getText());
269         }
270     }
271 
272     @Test
273     public void testHttpEntityWriteToForGzip() throws Exception {
274         final String entityText = "Hello, this is some plain text coming back.";
275 
276         final ClassicTestServer server = startServer();
277         server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
278 
279         final HttpHost target = targetHost();
280 
281         final CloseableHttpClient client = startClient();
282 
283         final HttpGet request = new HttpGet("/some-resource");
284         client.execute(target, request, response -> {
285             final ByteArrayOutputStream out = new ByteArrayOutputStream();
286             response.getEntity().writeTo(out);
287             Assertions.assertEquals(entityText, out.toString("utf-8"));
288             return null;
289         });
290 
291     }
292 
293     @Test
294     public void testHttpEntityWriteToForDeflate() throws Exception {
295         final String entityText = "Hello, this is some plain text coming back.";
296 
297         final ClassicTestServer server = startServer();
298         server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, true));
299 
300         final HttpHost target = targetHost();
301 
302         final CloseableHttpClient client = startClient();
303 
304         final HttpGet request = new HttpGet("/some-resource");
305         client.execute(target, request, response -> {
306             final ByteArrayOutputStream out = new ByteArrayOutputStream();
307             response.getEntity().writeTo(out);
308             Assertions.assertEquals(entityText, out.toString("utf-8"));
309             return out;
310         });
311     }
312 
313     @Test
314     public void gzipResponsesWorkWithBasicResponseHandler() throws Exception {
315         final String entityText = "Hello, this is some plain text coming back.";
316 
317         final ClassicTestServer server = startServer();
318         server.registerHandler("*", createGzipEncodingRequestHandler(entityText));
319 
320         final HttpHost target = targetHost();
321 
322         final CloseableHttpClient client = startClient();
323 
324         final HttpGet request = new HttpGet("/some-resource");
325         final String response = client.execute(target, request, new BasicHttpClientResponseHandler());
326         Assertions.assertEquals(entityText, response, "The entity text is correctly transported");
327     }
328 
329     @Test
330     public void deflateResponsesWorkWithBasicResponseHandler() throws Exception {
331         final String entityText = "Hello, this is some plain text coming back.";
332 
333         final ClassicTestServer server = startServer();
334         server.registerHandler("*", createDeflateEncodingRequestHandler(entityText, false));
335 
336         final HttpHost target = targetHost();
337 
338         final CloseableHttpClient client = startClient();
339 
340         final HttpGet request = new HttpGet("/some-resource");
341         final String response = client.execute(target, request, new BasicHttpClientResponseHandler());
342         Assertions.assertEquals(entityText, response, "The entity text is correctly transported");
343     }
344 
345     /**
346      * Creates a new {@link HttpRequestHandler} that will attempt to provide a deflate stream
347      * Content-Coding.
348      *
349      * @param entityText
350      *            the non-null String entity text to be returned by the server
351      * @param rfc1951
352      *            if true, then the stream returned will be a raw RFC1951 deflate stream, which
353      *            some servers return as a result of misinterpreting the HTTP 1.1 RFC. If false,
354      *            then it will return an RFC2616 compliant deflate encoded zlib stream.
355      * @return a non-null {@link HttpRequestHandler}
356      */
357     private HttpRequestHandler createDeflateEncodingRequestHandler(
358             final String entityText, final boolean rfc1951) {
359         return new HttpRequestHandler() {
360 
361             /**
362              * {@inheritDoc}
363              */
364             @Override
365             public void handle(
366                     final ClassicHttpRequest request,
367                     final ClassicHttpResponse response,
368                     final HttpContext context) throws HttpException, IOException {
369                 response.setEntity(new StringEntity(entityText));
370                 response.addHeader("Content-Type", "text/plain");
371                 final Iterator<HeaderElement> it = MessageSupport.iterate(request, "Accept-Encoding");
372                 while (it.hasNext()) {
373                     final HeaderElement element = it.next();
374                     if ("deflate".equalsIgnoreCase(element.getName())) {
375                         response.addHeader("Content-Encoding", "deflate");
376 
377                             /* Gack. DeflaterInputStream is Java 6. */
378                         // response.setEntity(new InputStreamEntity(new DeflaterInputStream(new
379                         // ByteArrayInputStream(
380                         // entityText.getBytes("utf-8"))), -1));
381                         final byte[] uncompressed = entityText.getBytes(StandardCharsets.UTF_8);
382                         final Deflater compressor = new Deflater(Deflater.DEFAULT_COMPRESSION, rfc1951);
383                         compressor.setInput(uncompressed);
384                         compressor.finish();
385                         final byte[] output = new byte[100];
386                         final int compressedLength = compressor.deflate(output);
387                         final byte[] compressed = new byte[compressedLength];
388                         System.arraycopy(output, 0, compressed, 0, compressedLength);
389                         response.setEntity(new InputStreamEntity(
390                                 new ByteArrayInputStream(compressed), compressedLength, null));
391                         return;
392                     }
393                 }
394             }
395         };
396     }
397 
398     /**
399      * Returns an {@link HttpRequestHandler} implementation that will attempt to provide a gzip
400      * Content-Encoding.
401      *
402      * @param entityText
403      *            the non-null String entity to be returned by the server
404      * @return a non-null {@link HttpRequestHandler}
405      */
406     private HttpRequestHandler createGzipEncodingRequestHandler(final String entityText) {
407         return new HttpRequestHandler() {
408 
409             /**
410              * {@inheritDoc}
411              */
412             @Override
413             public void handle(
414                     final ClassicHttpRequest request,
415                     final ClassicHttpResponse response,
416                     final HttpContext context) throws HttpException, IOException {
417                 response.setEntity(new StringEntity(entityText));
418                 response.addHeader("Content-Type", "text/plain");
419                 response.addHeader("Content-Type", "text/plain");
420                 final Iterator<HeaderElement> it = MessageSupport.iterate(request, "Accept-Encoding");
421                 while (it.hasNext()) {
422                     final HeaderElement element = it.next();
423                     if ("gzip".equalsIgnoreCase(element.getName())) {
424                         response.addHeader("Content-Encoding", "gzip");
425 
426                         /*
427                          * We have to do a bit more work with gzip versus deflate, since
428                          * Gzip doesn't appear to have an equivalent to DeflaterInputStream in
429                          * the JDK.
430                          *
431                          * UPDATE: DeflaterInputStream is Java 6 anyway, so we have to do a bit
432                          * of work there too!
433                          */
434                         final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
435                         final OutputStream out = new GZIPOutputStream(bytes);
436 
437                         final ByteArrayInputStream uncompressed = new ByteArrayInputStream(
438                                 entityText.getBytes(StandardCharsets.UTF_8));
439 
440                         final byte[] buf = new byte[60];
441 
442                         int n;
443                         while ((n = uncompressed.read(buf)) != -1) {
444                             out.write(buf, 0, n);
445                         }
446 
447                         out.close();
448 
449                         final byte[] arr = bytes.toByteArray();
450                         response.setEntity(new InputStreamEntity(new ByteArrayInputStream(arr),
451                                 arr.length, null));
452 
453                         return;
454                     }
455                 }
456             }
457         };
458     }
459 
460     /**
461      * Sub-ordinate task passed off to a different thread to be executed.
462      *
463      * @author jabley
464      *
465      */
466     class WorkerTask implements Runnable {
467 
468         private final CloseableHttpClient client;
469         private final HttpHost target;
470         private final HttpGet request;
471         private final CountDownLatch startGate;
472         private final CountDownLatch endGate;
473 
474         private boolean failed;
475         private String text;
476 
477         WorkerTask(final CloseableHttpClient client, final HttpHost target, final boolean identity, final CountDownLatch startGate, final CountDownLatch endGate) {
478             this.client = client;
479             this.target = target;
480             this.request = new HttpGet("/some-resource");
481             if (identity) {
482                 request.addHeader("Accept-Encoding", "identity");
483             }
484             this.startGate = startGate;
485             this.endGate = endGate;
486         }
487 
488         /**
489          * Returns the text of the HTTP entity.
490          *
491          * @return a String - may be null.
492          */
493         public String getText() {
494             return this.text;
495         }
496 
497         /**
498          * {@inheritDoc}
499          */
500         @Override
501         public void run() {
502             try {
503                 startGate.await();
504                 try {
505                     text = client.execute(target, request, response ->
506                             EntityUtils.toString(response.getEntity()));
507                 } catch (final Exception e) {
508                     failed = true;
509                 } finally {
510                     endGate.countDown();
511                 }
512             } catch (final InterruptedException ignore) {
513             }
514         }
515 
516         /**
517          * Returns true if this task failed, otherwise false.
518          *
519          * @return a flag
520          */
521         boolean isFailed() {
522             return this.failed;
523         }
524     }
525 }