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
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
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
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
409
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
423
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
437
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
449
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
462
463
464 private static final class SnappyEncoderFactory implements CompressionEncoderFactory {
465
466 @Override
467 public MessageToByteEncoder<ByteBuf> createEncoder() {
468 return new SnappyFrameEncoder();
469 }
470 }
471 }