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