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.http.impl.classic;
29  
30  import java.io.IOException;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.zip.GZIPInputStream;
35  
36  import org.apache.hc.client5.http.classic.ExecChain;
37  import org.apache.hc.client5.http.classic.ExecChainHandler;
38  import org.apache.hc.client5.http.config.RequestConfig;
39  import org.apache.hc.client5.http.entity.BrotliDecompressingEntity;
40  import org.apache.hc.client5.http.entity.BrotliInputStreamFactory;
41  import org.apache.hc.client5.http.entity.DecompressingEntity;
42  import org.apache.hc.client5.http.entity.DeflateInputStream;
43  import org.apache.hc.client5.http.entity.DeflateInputStreamFactory;
44  import org.apache.hc.client5.http.entity.GZIPInputStreamFactory;
45  import org.apache.hc.client5.http.entity.InputStreamFactory;
46  import org.apache.hc.client5.http.protocol.HttpClientContext;
47  import org.apache.hc.core5.annotation.Contract;
48  import org.apache.hc.core5.annotation.Internal;
49  import org.apache.hc.core5.annotation.ThreadingBehavior;
50  import org.apache.hc.core5.http.ClassicHttpRequest;
51  import org.apache.hc.core5.http.ClassicHttpResponse;
52  import org.apache.hc.core5.http.Header;
53  import org.apache.hc.core5.http.HeaderElement;
54  import org.apache.hc.core5.http.HttpEntity;
55  import org.apache.hc.core5.http.HttpException;
56  import org.apache.hc.core5.http.HttpHeaders;
57  import org.apache.hc.core5.http.ProtocolException;
58  import org.apache.hc.core5.http.config.Lookup;
59  import org.apache.hc.core5.http.config.RegistryBuilder;
60  import org.apache.hc.core5.http.message.BasicHeaderValueParser;
61  import org.apache.hc.core5.http.message.MessageSupport;
62  import org.apache.hc.core5.http.message.ParserCursor;
63  import org.apache.hc.core5.util.Args;
64  import org.brotli.dec.BrotliInputStream;
65  
66  /**
67   * Request execution handler in the classic request execution chain
68   * that is responsible for automatic response content decompression.
69   * <p>
70   * Further responsibilities such as communication with the opposite
71   * endpoint is delegated to the next executor in the request execution
72   * chain.
73   * </p>
74   *
75   * @since 5.0
76   */
77  @Contract(threading = ThreadingBehavior.STATELESS)
78  @Internal
79  public final class ContentCompressionExec implements ExecChainHandler {
80  
81      public static final int MAX_CODEC_LIST_LEN = 5;
82  
83      private final Header acceptEncoding;
84      private final Lookup<InputStreamFactory> decoderRegistry;
85      private final boolean ignoreUnknown;
86      private final int maxCodecListLen;
87  
88      public ContentCompressionExec(
89              final List<String> acceptEncoding,
90              final Lookup<InputStreamFactory> decoderRegistry,
91              final boolean ignoreUnknown,
92              final int maxCodecListLen) {
93  
94          final boolean brotliSupported = decoderRegistry == null && BrotliDecompressingEntity.isAvailable();
95          if (acceptEncoding != null) {
96              this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, acceptEncoding);
97          } else {
98              final List<String> encodings = new ArrayList<>(4);
99              encodings.add("gzip");
100             encodings.add("x-gzip");
101             encodings.add("deflate");
102             if (brotliSupported) {
103                 encodings.add("br");
104             }
105             this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, encodings);
106         }
107         if (decoderRegistry != null) {
108             this.decoderRegistry = decoderRegistry;
109         } else {
110             final RegistryBuilder<InputStreamFactory> builder = RegistryBuilder.<InputStreamFactory>create()
111                 .register("gzip", GZIPInputStreamFactory.getInstance())
112                 .register("x-gzip", GZIPInputStreamFactory.getInstance())
113                 .register("deflate", DeflateInputStreamFactory.getInstance());
114             if (brotliSupported) {
115                 builder.register("br", BrotliInputStreamFactory.getInstance());
116             }
117             this.decoderRegistry = builder.build();
118         }
119         this.ignoreUnknown = ignoreUnknown;
120         this.maxCodecListLen = maxCodecListLen;
121     }
122 
123     public ContentCompressionExec(
124             final List<String> acceptEncoding,
125             final Lookup<InputStreamFactory> decoderRegistry,
126             final boolean ignoreUnknown) {
127         this(acceptEncoding, decoderRegistry, ignoreUnknown, MAX_CODEC_LIST_LEN);
128     }
129 
130     public ContentCompressionExec(final boolean ignoreUnknown) {
131         this(null, null, ignoreUnknown, MAX_CODEC_LIST_LEN);
132     }
133 
134     public ContentCompressionExec(final int maxCodecListLen) {
135         this(null, null, true, maxCodecListLen);
136     }
137 
138     /**
139      * Handles {@code gzip} and {@code deflate} compressed entities by using the following
140      * decoders:
141      * <ul>
142      * <li>gzip - see {@link GZIPInputStream}</li>
143      * <li>deflate - see {@link DeflateInputStream}</li>
144      * <li>brotli - see {@link BrotliInputStream}</li>
145      * </ul>
146      */
147     public ContentCompressionExec() {
148         this(null, null, true, MAX_CODEC_LIST_LEN);
149     }
150 
151 
152     @Override
153     public ClassicHttpResponse execute(
154             final ClassicHttpRequest request,
155             final ExecChain.Scope scope,
156             final ExecChain chain) throws IOException, HttpException {
157         Args.notNull(request, "HTTP request");
158         Args.notNull(scope, "Scope");
159 
160         final HttpClientContext clientContext = scope.clientContext;
161         final RequestConfig requestConfig = clientContext.getRequestConfigOrDefault();
162 
163         /* Signal support for Accept-Encoding transfer encodings. */
164         if (!request.containsHeader(HttpHeaders.ACCEPT_ENCODING) && requestConfig.isContentCompressionEnabled()) {
165             request.addHeader(acceptEncoding);
166         }
167 
168         final ClassicHttpResponse response = chain.proceed(request, scope);
169 
170         final HttpEntity entity = response.getEntity();
171         // entity can be null in case of 304 Not Modified, 204 No Content or similar
172         // check for zero length entity.
173         if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
174             final String contentEncoding = entity.getContentEncoding();
175             if (contentEncoding != null) {
176                 final ParserCursor cursor = new ParserCursor(0, contentEncoding.length());
177                 final HeaderElement[] codecs = BasicHeaderValueParser.INSTANCE.parseElements(contentEncoding, cursor);
178                 if (maxCodecListLen > 0 && codecs.length > maxCodecListLen) {
179                     throw new ProtocolException("Codec list exceeds maximum of " + maxCodecListLen + " elements");
180                 }
181                 for (final HeaderElement codec : codecs) {
182                     final String codecname = codec.getName().toLowerCase(Locale.ROOT);
183                     final InputStreamFactory decoderFactory = decoderRegistry.lookup(codecname);
184                     if (decoderFactory != null) {
185                         response.setEntity(new DecompressingEntity(response.getEntity(), decoderFactory));
186                         response.removeHeaders(HttpHeaders.CONTENT_LENGTH);
187                         response.removeHeaders(HttpHeaders.CONTENT_ENCODING);
188                         response.removeHeaders(HttpHeaders.CONTENT_MD5);
189                     } else {
190                         if (!"identity".equals(codecname) && !ignoreUnknown) {
191                             throw new HttpException("Unsupported Content-Encoding: " + codec.getName());
192                         }
193                     }
194                 }
195             }
196         }
197         return response;
198     }
199 
200 }