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