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