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