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 = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
177         BrotliOptions brotliOptions = null;
178         GzipOptions gzipOptions = null;
179         DeflateOptions deflateOptions = null;
180         ZstdOptions zstdOptions = null;
181         SnappyOptions snappyOptions = null;
182         if (compressionOptions == null || compressionOptions.length == 0) {
183             compressionOptions = defaultCompressionOptions(
184                     StandardCompressionOptions.gzip(), StandardCompressionOptions.deflate());
185         }
186 
187         ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
188         for (CompressionOptions compressionOption : compressionOptions) {
189             // BrotliOptions' class initialization depends on Brotli classes being on the classpath.
190             // The Brotli.isAvailable check ensures that BrotliOptions will only get instantiated if Brotli is
191             // on the classpath.
192             // This results in the static analysis of native-image identifying the instanceof BrotliOptions check
193             // and thus BrotliOptions itself as unreachable, enabling native-image to link all classes
194             // at build time and not complain about the missing Brotli classes.
195             if (Brotli.isAvailable() && compressionOption instanceof BrotliOptions) {
196                 brotliOptions = (BrotliOptions) compressionOption;
197             } else if (compressionOption instanceof GzipOptions) {
198                 gzipOptions = (GzipOptions) compressionOption;
199             } else if (compressionOption instanceof DeflateOptions) {
200                 deflateOptions = (DeflateOptions) compressionOption;
201             } else if (Zstd.isAvailable() && compressionOption instanceof ZstdOptions) {
202                 zstdOptions = (ZstdOptions) compressionOption;
203             } else if (compressionOption instanceof SnappyOptions) {
204                 snappyOptions = (SnappyOptions) compressionOption;
205             } else {
206                 throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
207                         ": " + compressionOption);
208             }
209         }
210 
211         this.gzipOptions = gzipOptions;
212         this.deflateOptions = deflateOptions;
213         this.brotliOptions = brotliOptions;
214         this.zstdOptions = zstdOptions;
215         this.snappyOptions = snappyOptions;
216 
217         this.factories = new HashMap<String, CompressionEncoderFactory>();
218 
219         if (this.gzipOptions != null) {
220             this.factories.put("gzip", new GzipEncoderFactory());
221         }
222         if (this.deflateOptions != null) {
223             this.factories.put("deflate", new DeflateEncoderFactory());
224         }
225         if (Brotli.isAvailable() && this.brotliOptions != null) {
226             this.factories.put("br", new BrEncoderFactory());
227         }
228         if (this.zstdOptions != null) {
229             this.factories.put("zstd", new ZstdEncoderFactory());
230         }
231         if (this.snappyOptions != null) {
232             this.factories.put("snappy", new SnappyEncoderFactory());
233         }
234     }
235 
236     private static CompressionOptions[] defaultCompressionOptions(
237             GzipOptions gzipOptions, DeflateOptions deflateOptions) {
238         List<CompressionOptions> options = new ArrayList<CompressionOptions>(5);
239         options.add(gzipOptions);
240         options.add(deflateOptions);
241         options.add(StandardCompressionOptions.snappy());
242 
243         if (Brotli.isAvailable()) {
244             options.add(StandardCompressionOptions.brotli());
245         }
246         if (Zstd.isAvailable()) {
247             options.add(StandardCompressionOptions.zstd());
248         }
249         return options.toArray(new CompressionOptions[0]);
250     }
251 
252     @Override
253     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
254         this.ctx = ctx;
255     }
256 
257     @Override
258     protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
259         if (this.contentSizeThreshold > 0) {
260             if (httpResponse instanceof HttpContent &&
261                     ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
262                 return null;
263             }
264         }
265 
266         String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
267         if (contentEncoding != null) {
268             // Content-Encoding was set, either as something specific or as the IDENTITY encoding
269             // Therefore, we should NOT encode here
270             return null;
271         }
272 
273         String targetContentEncoding = determineEncoding(acceptEncoding);
274         if (targetContentEncoding == null) {
275             return null;
276         }
277 
278         CompressionEncoderFactory encoderFactory = factories.get(targetContentEncoding);
279 
280         if (encoderFactory == null) {
281             throw new IllegalStateException("Couldn't find CompressionEncoderFactory: " + targetContentEncoding);
282         }
283 
284         return new Result(targetContentEncoding,
285                 new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
286                         ctx.channel().config(), encoderFactory.createEncoder()));
287     }
288 
289     @SuppressWarnings("FloatingPointEquality")
290     protected String determineEncoding(String acceptEncoding) {
291         float starQ = -1.0f;
292         float brQ = -1.0f;
293         float zstdQ = -1.0f;
294         float snappyQ = -1.0f;
295         float gzipQ = -1.0f;
296         float deflateQ = -1.0f;
297 
298         int start = 0;
299         int length = acceptEncoding.length();
300         while (start < length) {
301             int comma = acceptEncoding.indexOf(',', start);
302             if (comma == -1) {
303                 comma = length;
304             }
305             String encoding = acceptEncoding.substring(start, comma);
306             float q = 1.0f;
307             int equalsPos = encoding.indexOf('=');
308             if (equalsPos != -1) {
309                 try {
310                     q = Float.parseFloat(encoding.substring(equalsPos + 1));
311                 } catch (NumberFormatException e) {
312                     // Ignore encoding
313                     q = 0.0f;
314                 }
315             }
316             if (encoding.contains("*")) {
317                 starQ = q;
318             } else if (encoding.contains("br") && q > brQ) {
319                 brQ = q;
320             } else if (encoding.contains("zstd") && q > zstdQ) {
321                 zstdQ = q;
322             } else if (encoding.contains("snappy") && q > snappyQ) {
323                 snappyQ = q;
324             } else if (encoding.contains("gzip") && q > gzipQ) {
325                 gzipQ = q;
326             } else if (encoding.contains("deflate") && q > deflateQ) {
327                 deflateQ = q;
328             }
329             start = comma + 1;
330         }
331         if (brQ > 0.0f || zstdQ > 0.0f || snappyQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
332             if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
333                 return "br";
334             } else if (zstdQ != -1.0f && zstdQ >= snappyQ && this.zstdOptions != null) {
335                 return "zstd";
336             } else if (snappyQ != -1.0f && snappyQ >= gzipQ && this.snappyOptions != null) {
337                 return "snappy";
338             } else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
339                 return "gzip";
340             } else if (deflateQ != -1.0f && this.deflateOptions != null) {
341                 return "deflate";
342             }
343         }
344         if (starQ > 0.0f) {
345             if (brQ == -1.0f && this.brotliOptions != null) {
346                 return "br";
347             }
348             if (zstdQ == -1.0f && this.zstdOptions != null) {
349                 return "zstd";
350             }
351             if (snappyQ == -1.0f && this.snappyOptions != null) {
352                 return "snappy";
353             }
354             if (gzipQ == -1.0f && this.gzipOptions != null) {
355                 return "gzip";
356             }
357             if (deflateQ == -1.0f && this.deflateOptions != null) {
358                 return "deflate";
359             }
360         }
361         return null;
362     }
363 
364     @Deprecated
365     @SuppressWarnings("FloatingPointEquality")
366     protected ZlibWrapper determineWrapper(String acceptEncoding) {
367         float starQ = -1.0f;
368         float gzipQ = -1.0f;
369         float deflateQ = -1.0f;
370         for (String encoding : acceptEncoding.split(",")) {
371             float q = 1.0f;
372             int equalsPos = encoding.indexOf('=');
373             if (equalsPos != -1) {
374                 try {
375                     q = Float.parseFloat(encoding.substring(equalsPos + 1));
376                 } catch (NumberFormatException e) {
377                     // Ignore encoding
378                     q = 0.0f;
379                 }
380             }
381             if (encoding.contains("*")) {
382                 starQ = q;
383             } else if (encoding.contains("gzip") && q > gzipQ) {
384                 gzipQ = q;
385             } else if (encoding.contains("deflate") && q > deflateQ) {
386                 deflateQ = q;
387             }
388         }
389         if (gzipQ > 0.0f || deflateQ > 0.0f) {
390             if (gzipQ >= deflateQ) {
391                 return ZlibWrapper.GZIP;
392             } else {
393                 return ZlibWrapper.ZLIB;
394             }
395         }
396         if (starQ > 0.0f) {
397             if (gzipQ == -1.0f) {
398                 return ZlibWrapper.GZIP;
399             }
400             if (deflateQ == -1.0f) {
401                 return ZlibWrapper.ZLIB;
402             }
403         }
404         return null;
405     }
406 
407     /**
408      * Compression Encoder Factory that creates {@link ZlibEncoder}s
409      * used to compress http content for gzip content encoding
410      */
411     private final class GzipEncoderFactory implements CompressionEncoderFactory {
412 
413         @Override
414         public MessageToByteEncoder<ByteBuf> createEncoder() {
415             return ZlibCodecFactory.newZlibEncoder(
416                     ZlibWrapper.GZIP, gzipOptions.compressionLevel(),
417                     gzipOptions.windowBits(), gzipOptions.memLevel());
418         }
419     }
420 
421     /**
422      * Compression Encoder Factory that creates {@link ZlibEncoder}s
423      * used to compress http content for deflate content encoding
424      */
425     private final class DeflateEncoderFactory implements CompressionEncoderFactory {
426 
427         @Override
428         public MessageToByteEncoder<ByteBuf> createEncoder() {
429             return ZlibCodecFactory.newZlibEncoder(
430                     ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
431                     deflateOptions.windowBits(), deflateOptions.memLevel());
432         }
433     }
434 
435     /**
436      * Compression Encoder Factory that creates {@link BrotliEncoder}s
437      * used to compress http content for br content encoding
438      */
439     private final class BrEncoderFactory implements CompressionEncoderFactory {
440 
441         @Override
442         public MessageToByteEncoder<ByteBuf> createEncoder() {
443             return new BrotliEncoder(brotliOptions.parameters());
444         }
445     }
446 
447     /**
448      * Compression Encoder Factory for create {@link ZstdEncoder}
449      * used to compress http content for zstd content encoding
450      */
451     private final class ZstdEncoderFactory implements CompressionEncoderFactory {
452 
453         @Override
454         public MessageToByteEncoder<ByteBuf> createEncoder() {
455             return new ZstdEncoder(zstdOptions.compressionLevel(),
456                     zstdOptions.blockSize(), zstdOptions.maxEncodeSize());
457         }
458     }
459 
460     /**
461      * Compression Encoder Factory for create {@link SnappyFrameEncoder}
462      * used to compress http content for snappy content encoding
463      */
464     private static final class SnappyEncoderFactory implements CompressionEncoderFactory {
465 
466         @Override
467         public MessageToByteEncoder<ByteBuf> createEncoder() {
468             return new SnappyFrameEncoder();
469         }
470     }
471 }