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