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 = 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
190
191
192
193
194
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
269
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
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
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
400
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
414
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
428
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
440
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
453
454
455 private static final class SnappyEncoderFactory implements CompressionEncoderFactory {
456
457 @Override
458 public MessageToByteEncoder<ByteBuf> createEncoder() {
459 return new SnappyFrameEncoder();
460 }
461 }
462 }