View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http;
17  
18  import java.util.HashMap;
19  import java.util.Map;
20  
21  import io.netty.buffer.ByteBuf;
22  import io.netty.channel.ChannelHandlerContext;
23  import io.netty.channel.embedded.EmbeddedChannel;
24  import io.netty.handler.codec.MessageToByteEncoder;
25  import io.netty.handler.codec.compression.Brotli;
26  import io.netty.handler.codec.compression.BrotliEncoder;
27  import io.netty.handler.codec.compression.BrotliOptions;
28  import io.netty.handler.codec.compression.CompressionOptions;
29  import io.netty.handler.codec.compression.DeflateOptions;
30  import io.netty.handler.codec.compression.GzipOptions;
31  import io.netty.handler.codec.compression.StandardCompressionOptions;
32  import io.netty.handler.codec.compression.ZlibCodecFactory;
33  import io.netty.handler.codec.compression.ZlibEncoder;
34  import io.netty.handler.codec.compression.ZlibWrapper;
35  import io.netty.handler.codec.compression.Zstd;
36  import io.netty.handler.codec.compression.ZstdEncoder;
37  import io.netty.handler.codec.compression.ZstdOptions;
38  import io.netty.util.internal.ObjectUtil;
39  
40  /**
41   * Compresses an {@link HttpMessage} and an {@link HttpContent} in {@code gzip} or
42   * {@code deflate} encoding while respecting the {@code "Accept-Encoding"} header.
43   * If there is no matching encoding, no compression is done.  For more
44   * information on how this handler modifies the message, please refer to
45   * {@link HttpContentEncoder}.
46   */
47  public class HttpContentCompressor extends HttpContentEncoder {
48  
49      private final boolean supportsCompressionOptions;
50      private final BrotliOptions brotliOptions;
51      private final GzipOptions gzipOptions;
52      private final DeflateOptions deflateOptions;
53      private final ZstdOptions zstdOptions;
54  
55      private final int compressionLevel;
56      private final int windowBits;
57      private final int memLevel;
58      private final int contentSizeThreshold;
59      private ChannelHandlerContext ctx;
60      private final Map<String, CompressionEncoderFactory> factories;
61  
62      /**
63       * Creates a new handler with the default compression level (<tt>6</tt>),
64       * default window size (<tt>15</tt>) and default memory level (<tt>8</tt>).
65       */
66      public HttpContentCompressor() {
67          this(6);
68      }
69  
70      /**
71       * Creates a new handler with the specified compression level, default
72       * window size (<tt>15</tt>) and default memory level (<tt>8</tt>).
73       *
74       * @param compressionLevel
75       *        {@code 1} yields the fastest compression and {@code 9} yields the
76       *        best compression.  {@code 0} means no compression.  The default
77       *        compression level is {@code 6}.
78       */
79      @Deprecated
80      public HttpContentCompressor(int compressionLevel) {
81          this(compressionLevel, 15, 8, 0);
82      }
83  
84      /**
85       * Creates a new handler with the specified compression level, window size,
86       * and memory level..
87       *
88       * @param compressionLevel
89       *        {@code 1} yields the fastest compression and {@code 9} yields the
90       *        best compression.  {@code 0} means no compression.  The default
91       *        compression level is {@code 6}.
92       * @param windowBits
93       *        The base two logarithm of the size of the history buffer.  The
94       *        value should be in the range {@code 9} to {@code 15} inclusive.
95       *        Larger values result in better compression at the expense of
96       *        memory usage.  The default value is {@code 15}.
97       * @param memLevel
98       *        How much memory should be allocated for the internal compression
99       *        state.  {@code 1} uses minimum memory and {@code 9} uses maximum
100      *        memory.  Larger values result in better and faster compression
101      *        at the expense of memory usage.  The default value is {@code 8}
102      */
103     @Deprecated
104     public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
105         this(compressionLevel, windowBits, memLevel, 0);
106     }
107 
108     /**
109      * Creates a new handler with the specified compression level, window size,
110      * and memory level..
111      *
112      * @param compressionLevel
113      *        {@code 1} yields the fastest compression and {@code 9} yields the
114      *        best compression.  {@code 0} means no compression.  The default
115      *        compression level is {@code 6}.
116      * @param windowBits
117      *        The base two logarithm of the size of the history buffer.  The
118      *        value should be in the range {@code 9} to {@code 15} inclusive.
119      *        Larger values result in better compression at the expense of
120      *        memory usage.  The default value is {@code 15}.
121      * @param memLevel
122      *        How much memory should be allocated for the internal compression
123      *        state.  {@code 1} uses minimum memory and {@code 9} uses maximum
124      *        memory.  Larger values result in better and faster compression
125      *        at the expense of memory usage.  The default value is {@code 8}
126      * @param contentSizeThreshold
127      *        The response body is compressed when the size of the response
128      *        body exceeds the threshold. The value should be a non negative
129      *        number. {@code 0} will enable compression for all responses.
130      */
131     @Deprecated
132     public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
133         this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
134         this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
135         this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
136         this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
137         this.brotliOptions = null;
138         this.gzipOptions = null;
139         this.deflateOptions = null;
140         this.zstdOptions = null;
141         this.factories = null;
142         this.supportsCompressionOptions = false;
143     }
144 
145     /**
146      * Create a new {@link HttpContentCompressor} Instance with specified
147      * {@link CompressionOptions}s and contentSizeThreshold set to {@code 0}
148      *
149      * @param compressionOptions {@link CompressionOptions} or {@code null} if the default
150      *        should be used.
151      */
152     public HttpContentCompressor(CompressionOptions... compressionOptions) {
153         this(0, compressionOptions);
154     }
155 
156     /**
157      * Create a new {@link HttpContentCompressor} instance with specified
158      * {@link CompressionOptions}s
159      *
160      * @param contentSizeThreshold
161      *        The response body is compressed when the size of the response
162      *        body exceeds the threshold. The value should be a non negative
163      *        number. {@code 0} will enable compression for all responses.
164      * @param compressionOptions {@link CompressionOptions} or {@code null}
165      *        if the default should be used.
166      */
167     public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
168         this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
169         BrotliOptions brotliOptions = null;
170         GzipOptions gzipOptions = null;
171         DeflateOptions deflateOptions = null;
172         ZstdOptions zstdOptions = null;
173         if (compressionOptions == null || compressionOptions.length == 0) {
174             brotliOptions = Brotli.isAvailable() ? StandardCompressionOptions.brotli() : null;
175             gzipOptions = StandardCompressionOptions.gzip();
176             deflateOptions = StandardCompressionOptions.deflate();
177             zstdOptions = Zstd.isAvailable() ? StandardCompressionOptions.zstd() : null;
178         } else {
179             ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
180             for (CompressionOptions compressionOption : compressionOptions) {
181                 // BrotliOptions' class initialization depends on Brotli classes being on the classpath.
182                 // The Brotli.isAvailable check ensures that BrotliOptions will only get instantiated if Brotli is
183                 // on the classpath.
184                 // This results in the static analysis of native-image identifying the instanceof BrotliOptions check
185                 // and thus BrotliOptions itself as unreachable, enabling native-image to link all classes
186                 // at build time and not complain about the missing Brotli classes.
187                 if (Brotli.isAvailable() && compressionOption instanceof BrotliOptions) {
188                     brotliOptions = (BrotliOptions) compressionOption;
189                 } else if (compressionOption instanceof GzipOptions) {
190                     gzipOptions = (GzipOptions) compressionOption;
191                 } else if (compressionOption instanceof DeflateOptions) {
192                     deflateOptions = (DeflateOptions) compressionOption;
193                 } else if (compressionOption instanceof ZstdOptions) {
194                     zstdOptions = (ZstdOptions) compressionOption;
195                 } else {
196                     throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
197                             ": " + compressionOption);
198                 }
199             }
200         }
201 
202         this.gzipOptions = gzipOptions;
203         this.deflateOptions = deflateOptions;
204         this.brotliOptions = brotliOptions;
205         this.zstdOptions = zstdOptions;
206 
207         this.factories = new HashMap<String, CompressionEncoderFactory>();
208 
209         if (this.gzipOptions != null) {
210             this.factories.put("gzip", new GzipEncoderFactory());
211         }
212         if (this.deflateOptions != null) {
213             this.factories.put("deflate", new DeflateEncoderFactory());
214         }
215         if (Brotli.isAvailable() && this.brotliOptions != null) {
216             this.factories.put("br", new BrEncoderFactory());
217         }
218         if (this.zstdOptions != null) {
219             this.factories.put("zstd", new ZstdEncoderFactory());
220         }
221 
222         this.compressionLevel = -1;
223         this.windowBits = -1;
224         this.memLevel = -1;
225         supportsCompressionOptions = true;
226     }
227 
228     @Override
229     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
230         this.ctx = ctx;
231     }
232 
233     @Override
234     protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
235         if (this.contentSizeThreshold > 0) {
236             if (httpResponse instanceof HttpContent &&
237                     ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
238                 return null;
239             }
240         }
241 
242         String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
243         if (contentEncoding != null) {
244             // Content-Encoding was set, either as something specific or as the IDENTITY encoding
245             // Therefore, we should NOT encode here
246             return null;
247         }
248 
249         if (supportsCompressionOptions) {
250             String targetContentEncoding = determineEncoding(acceptEncoding);
251             if (targetContentEncoding == null) {
252                 return null;
253             }
254 
255             CompressionEncoderFactory encoderFactory = factories.get(targetContentEncoding);
256 
257             if (encoderFactory == null) {
258                 throw new Error();
259             }
260 
261             return new Result(targetContentEncoding,
262                     new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
263                             ctx.channel().config(), encoderFactory.createEncoder()));
264         } else {
265             ZlibWrapper wrapper = determineWrapper(acceptEncoding);
266             if (wrapper == null) {
267                 return null;
268             }
269 
270             String targetContentEncoding;
271             switch (wrapper) {
272                 case GZIP:
273                     targetContentEncoding = "gzip";
274                     break;
275                 case ZLIB:
276                     targetContentEncoding = "deflate";
277                     break;
278                 default:
279                     throw new Error();
280             }
281 
282             return new Result(
283                     targetContentEncoding,
284                     new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
285                             ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
286                             wrapper, compressionLevel, windowBits, memLevel)));
287         }
288     }
289 
290     @SuppressWarnings("FloatingPointEquality")
291     protected String determineEncoding(String acceptEncoding) {
292         float starQ = -1.0f;
293         float brQ = -1.0f;
294         float zstdQ = -1.0f;
295         float gzipQ = -1.0f;
296         float deflateQ = -1.0f;
297         for (String encoding : acceptEncoding.split(",")) {
298             float q = 1.0f;
299             int equalsPos = encoding.indexOf('=');
300             if (equalsPos != -1) {
301                 try {
302                     q = Float.parseFloat(encoding.substring(equalsPos + 1));
303                 } catch (NumberFormatException e) {
304                     // Ignore encoding
305                     q = 0.0f;
306                 }
307             }
308             if (encoding.contains("*")) {
309                 starQ = q;
310             } else if (encoding.contains("br") && q > brQ) {
311                 brQ = q;
312             } else if (encoding.contains("zstd") && q > zstdQ) {
313                 zstdQ = q;
314             } else if (encoding.contains("gzip") && q > gzipQ) {
315                 gzipQ = q;
316             } else if (encoding.contains("deflate") && q > deflateQ) {
317                 deflateQ = q;
318             }
319         }
320         if (brQ > 0.0f || zstdQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
321             if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
322                 return "br";
323             } else if (zstdQ != -1.0f && zstdQ >= gzipQ && this.zstdOptions != null) {
324                 return "zstd";
325             } else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
326                 return "gzip";
327             } else if (deflateQ != -1.0f && this.deflateOptions != null) {
328                 return "deflate";
329             }
330         }
331         if (starQ > 0.0f) {
332             if (brQ == -1.0f && this.brotliOptions != null) {
333                 return "br";
334             }
335             if (zstdQ == -1.0f && this.zstdOptions != null) {
336                 return "zstd";
337             }
338             if (gzipQ == -1.0f && this.gzipOptions != null) {
339                 return "gzip";
340             }
341             if (deflateQ == -1.0f && this.deflateOptions != null) {
342                 return "deflate";
343             }
344         }
345         return null;
346     }
347 
348     @Deprecated
349     @SuppressWarnings("FloatingPointEquality")
350     protected ZlibWrapper determineWrapper(String acceptEncoding) {
351         float starQ = -1.0f;
352         float gzipQ = -1.0f;
353         float deflateQ = -1.0f;
354         for (String encoding : acceptEncoding.split(",")) {
355             float q = 1.0f;
356             int equalsPos = encoding.indexOf('=');
357             if (equalsPos != -1) {
358                 try {
359                     q = Float.parseFloat(encoding.substring(equalsPos + 1));
360                 } catch (NumberFormatException e) {
361                     // Ignore encoding
362                     q = 0.0f;
363                 }
364             }
365             if (encoding.contains("*")) {
366                 starQ = q;
367             } else if (encoding.contains("gzip") && q > gzipQ) {
368                 gzipQ = q;
369             } else if (encoding.contains("deflate") && q > deflateQ) {
370                 deflateQ = q;
371             }
372         }
373         if (gzipQ > 0.0f || deflateQ > 0.0f) {
374             if (gzipQ >= deflateQ) {
375                 return ZlibWrapper.GZIP;
376             } else {
377                 return ZlibWrapper.ZLIB;
378             }
379         }
380         if (starQ > 0.0f) {
381             if (gzipQ == -1.0f) {
382                 return ZlibWrapper.GZIP;
383             }
384             if (deflateQ == -1.0f) {
385                 return ZlibWrapper.ZLIB;
386             }
387         }
388         return null;
389     }
390 
391     /**
392      * Compression Encoder Factory that creates {@link ZlibEncoder}s
393      * used to compress http content for gzip content encoding
394      */
395     private final class GzipEncoderFactory implements CompressionEncoderFactory {
396 
397         @Override
398         public MessageToByteEncoder<ByteBuf> createEncoder() {
399             return ZlibCodecFactory.newZlibEncoder(
400                     ZlibWrapper.GZIP, gzipOptions.compressionLevel(),
401                     gzipOptions.windowBits(), gzipOptions.memLevel());
402         }
403     }
404 
405     /**
406      * Compression Encoder Factory that creates {@link ZlibEncoder}s
407      * used to compress http content for deflate content encoding
408      */
409     private final class DeflateEncoderFactory implements CompressionEncoderFactory {
410 
411         @Override
412         public MessageToByteEncoder<ByteBuf> createEncoder() {
413             return ZlibCodecFactory.newZlibEncoder(
414                     ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
415                     deflateOptions.windowBits(), deflateOptions.memLevel());
416         }
417     }
418 
419     /**
420      * Compression Encoder Factory that creates {@link BrotliEncoder}s
421      * used to compress http content for br content encoding
422      */
423     private final class BrEncoderFactory implements CompressionEncoderFactory {
424 
425         @Override
426         public MessageToByteEncoder<ByteBuf> createEncoder() {
427             return new BrotliEncoder(brotliOptions.parameters());
428         }
429     }
430 
431     /**
432      * Compression Encoder Factory for create {@link ZstdEncoder}
433      * used to compress http content for zstd content encoding
434      */
435     private final class ZstdEncoderFactory implements CompressionEncoderFactory {
436 
437         @Override
438         public MessageToByteEncoder<ByteBuf> createEncoder() {
439             return new ZstdEncoder(zstdOptions.compressionLevel(),
440                     zstdOptions.blockSize(), zstdOptions.maxEncodeSize());
441         }
442     }
443 }