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         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("snappy") && q > snappyQ) {
315                 snappyQ = q;
316             } else if (encoding.contains("gzip") && q > gzipQ) {
317                 gzipQ = q;
318             } else if (encoding.contains("deflate") && q > deflateQ) {
319                 deflateQ = q;
320             }
321         }
322         if (brQ > 0.0f || zstdQ > 0.0f || snappyQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
323             if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
324                 return "br";
325             } else if (zstdQ != -1.0f && zstdQ >= snappyQ && this.zstdOptions != null) {
326                 return "zstd";
327             } else if (snappyQ != -1.0f && snappyQ >= gzipQ && this.snappyOptions != null) {
328                 return "snappy";
329             } else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
330                 return "gzip";
331             } else if (deflateQ != -1.0f && this.deflateOptions != null) {
332                 return "deflate";
333             }
334         }
335         if (starQ > 0.0f) {
336             if (brQ == -1.0f && this.brotliOptions != null) {
337                 return "br";
338             }
339             if (zstdQ == -1.0f && this.zstdOptions != null) {
340                 return "zstd";
341             }
342             if (snappyQ == -1.0f && this.snappyOptions != null) {
343                 return "snappy";
344             }
345             if (gzipQ == -1.0f && this.gzipOptions != null) {
346                 return "gzip";
347             }
348             if (deflateQ == -1.0f && this.deflateOptions != null) {
349                 return "deflate";
350             }
351         }
352         return null;
353     }
354 
355     @Deprecated
356     @SuppressWarnings("FloatingPointEquality")
357     protected ZlibWrapper determineWrapper(String acceptEncoding) {
358         float starQ = -1.0f;
359         float gzipQ = -1.0f;
360         float deflateQ = -1.0f;
361         for (String encoding : acceptEncoding.split(",")) {
362             float q = 1.0f;
363             int equalsPos = encoding.indexOf('=');
364             if (equalsPos != -1) {
365                 try {
366                     q = Float.parseFloat(encoding.substring(equalsPos + 1));
367                 } catch (NumberFormatException e) {
368                     // Ignore encoding
369                     q = 0.0f;
370                 }
371             }
372             if (encoding.contains("*")) {
373                 starQ = q;
374             } else if (encoding.contains("gzip") && q > gzipQ) {
375                 gzipQ = q;
376             } else if (encoding.contains("deflate") && q > deflateQ) {
377                 deflateQ = q;
378             }
379         }
380         if (gzipQ > 0.0f || deflateQ > 0.0f) {
381             if (gzipQ >= deflateQ) {
382                 return ZlibWrapper.GZIP;
383             } else {
384                 return ZlibWrapper.ZLIB;
385             }
386         }
387         if (starQ > 0.0f) {
388             if (gzipQ == -1.0f) {
389                 return ZlibWrapper.GZIP;
390             }
391             if (deflateQ == -1.0f) {
392                 return ZlibWrapper.ZLIB;
393             }
394         }
395         return null;
396     }
397 
398     /**
399      * Compression Encoder Factory that creates {@link ZlibEncoder}s
400      * used to compress http content for gzip content encoding
401      */
402     private final class GzipEncoderFactory implements CompressionEncoderFactory {
403 
404         @Override
405         public MessageToByteEncoder<ByteBuf> createEncoder() {
406             return ZlibCodecFactory.newZlibEncoder(
407                     ZlibWrapper.GZIP, gzipOptions.compressionLevel(),
408                     gzipOptions.windowBits(), gzipOptions.memLevel());
409         }
410     }
411 
412     /**
413      * Compression Encoder Factory that creates {@link ZlibEncoder}s
414      * used to compress http content for deflate content encoding
415      */
416     private final class DeflateEncoderFactory implements CompressionEncoderFactory {
417 
418         @Override
419         public MessageToByteEncoder<ByteBuf> createEncoder() {
420             return ZlibCodecFactory.newZlibEncoder(
421                     ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
422                     deflateOptions.windowBits(), deflateOptions.memLevel());
423         }
424     }
425 
426     /**
427      * Compression Encoder Factory that creates {@link BrotliEncoder}s
428      * used to compress http content for br content encoding
429      */
430     private final class BrEncoderFactory implements CompressionEncoderFactory {
431 
432         @Override
433         public MessageToByteEncoder<ByteBuf> createEncoder() {
434             return new BrotliEncoder(brotliOptions.parameters());
435         }
436     }
437 
438     /**
439      * Compression Encoder Factory for create {@link ZstdEncoder}
440      * used to compress http content for zstd content encoding
441      */
442     private final class ZstdEncoderFactory implements CompressionEncoderFactory {
443 
444         @Override
445         public MessageToByteEncoder<ByteBuf> createEncoder() {
446             return new ZstdEncoder(zstdOptions.compressionLevel(),
447                     zstdOptions.blockSize(), zstdOptions.maxEncodeSize());
448         }
449     }
450 
451     /**
452      * Compression Encoder Factory for create {@link SnappyFrameEncoder}
453      * used to compress http content for snappy content encoding
454      */
455     private static final class SnappyEncoderFactory implements CompressionEncoderFactory {
456 
457         @Override
458         public MessageToByteEncoder<ByteBuf> createEncoder() {
459             return new SnappyFrameEncoder();
460         }
461     }
462 }