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