1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
48
49
50
51
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
67
68
69
70 public HttpContentCompressor() {
71 this(0, (CompressionOptions[]) null);
72 }
73
74
75
76
77
78
79
80
81
82
83 @Deprecated
84 public HttpContentCompressor(int compressionLevel) {
85 this(compressionLevel, 15, 8, 0);
86 }
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 @Deprecated
108 public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
109 this(compressionLevel, windowBits, memLevel, 0);
110 }
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
155
156
157
158
159
160 public HttpContentCompressor(CompressionOptions... compressionOptions) {
161 this(0, compressionOptions);
162 }
163
164
165
166
167
168
169
170
171
172
173
174
175 public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
176 this(contentSizeThreshold, DEFAULT_MAX_PIPELINE_DEPTH, compressionOptions);
177 }
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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
210
211
212
213
214
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
289
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
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
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
420
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
434
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
448
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
460
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
473
474
475 private static final class SnappyEncoderFactory implements CompressionEncoderFactory {
476
477 @Override
478 public MessageToByteEncoder<ByteBuf> createEncoder() {
479 return new SnappyFrameEncoder();
480 }
481 }
482 }