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 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.ZlibEncoder;
34 import io.netty.handler.codec.compression.ZlibWrapper;
35 import io.netty.handler.codec.compression.Zstd;
36 import io.netty.handler.codec.compression.ZstdEncoder;
37 import io.netty.handler.codec.compression.ZstdOptions;
38 import io.netty.util.internal.ObjectUtil;
39
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44
45 import static io.netty.util.internal.ObjectUtil.checkInRange;
46
47
48
49
50
51
52
53
54 public class HttpContentCompressor extends HttpContentEncoder {
55
56 private final BrotliOptions brotliOptions;
57 private final GzipOptions gzipOptions;
58 private final DeflateOptions deflateOptions;
59 private final ZstdOptions zstdOptions;
60 private final SnappyOptions snappyOptions;
61
62 private final int contentSizeThreshold;
63 private ChannelHandlerContext ctx;
64 private final Map<String, CompressionEncoderFactory> factories;
65
66
67
68
69
70
71 public HttpContentCompressor() {
72 this(0, (CompressionOptions[]) null);
73 }
74
75
76
77
78
79
80
81
82
83
84 @Deprecated
85 public HttpContentCompressor(int compressionLevel) {
86 this(compressionLevel, 15, 8, 0);
87 }
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108 @Deprecated
109 public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
110 this(compressionLevel, windowBits, memLevel, 0);
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
136 @Deprecated
137 public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
138 this(contentSizeThreshold,
139 defaultCompressionOptions(
140 StandardCompressionOptions.gzip(
141 checkInRange(compressionLevel, 0, 9, "compressionLevel"),
142 checkInRange(windowBits, 9, 15, "windowBits"),
143 checkInRange(memLevel, 1, 9, "memLevel")
144 ),
145 StandardCompressionOptions.deflate(
146 checkInRange(compressionLevel, 0, 9, "compressionLevel"),
147 checkInRange(windowBits, 9, 15, "windowBits"),
148 checkInRange(memLevel, 1, 9, "memLevel")
149 )
150 )
151 );
152 }
153
154
155
156
157
158
159
160
161 public HttpContentCompressor(CompressionOptions... compressionOptions) {
162 this(0, compressionOptions);
163 }
164
165
166
167
168
169
170
171
172
173
174
175
176 public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
177 this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
178 BrotliOptions brotliOptions = null;
179 GzipOptions gzipOptions = null;
180 DeflateOptions deflateOptions = null;
181 ZstdOptions zstdOptions = null;
182 SnappyOptions snappyOptions = null;
183 if (compressionOptions == null || compressionOptions.length == 0) {
184 compressionOptions = defaultCompressionOptions(
185 StandardCompressionOptions.gzip(), StandardCompressionOptions.deflate());
186 }
187
188 ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
189 for (CompressionOptions compressionOption : compressionOptions) {
190
191
192
193
194
195
196 if (Brotli.isAvailable() && compressionOption instanceof BrotliOptions) {
197 brotliOptions = (BrotliOptions) compressionOption;
198 } else if (compressionOption instanceof GzipOptions) {
199 gzipOptions = (GzipOptions) compressionOption;
200 } else if (compressionOption instanceof DeflateOptions) {
201 deflateOptions = (DeflateOptions) compressionOption;
202 } else if (Zstd.isAvailable() && compressionOption instanceof ZstdOptions) {
203 zstdOptions = (ZstdOptions) compressionOption;
204 } else if (compressionOption instanceof SnappyOptions) {
205 snappyOptions = (SnappyOptions) compressionOption;
206 } else {
207 throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
208 ": " + compressionOption);
209 }
210 }
211
212 this.gzipOptions = gzipOptions;
213 this.deflateOptions = deflateOptions;
214 this.brotliOptions = brotliOptions;
215 this.zstdOptions = zstdOptions;
216 this.snappyOptions = snappyOptions;
217
218 this.factories = new HashMap<String, CompressionEncoderFactory>();
219
220 if (this.gzipOptions != null) {
221 this.factories.put("gzip", new GzipEncoderFactory());
222 }
223 if (this.deflateOptions != null) {
224 this.factories.put("deflate", new DeflateEncoderFactory());
225 }
226 if (Brotli.isAvailable() && this.brotliOptions != null) {
227 this.factories.put("br", new BrEncoderFactory());
228 }
229 if (this.zstdOptions != null) {
230 this.factories.put("zstd", new ZstdEncoderFactory());
231 }
232 if (this.snappyOptions != null) {
233 this.factories.put("snappy", new SnappyEncoderFactory());
234 }
235 }
236
237 private static CompressionOptions[] defaultCompressionOptions(
238 GzipOptions gzipOptions, DeflateOptions deflateOptions) {
239 List<CompressionOptions> options = new ArrayList<CompressionOptions>(5);
240 options.add(gzipOptions);
241 options.add(deflateOptions);
242 options.add(StandardCompressionOptions.snappy());
243
244 if (Brotli.isAvailable()) {
245 options.add(StandardCompressionOptions.brotli());
246 }
247 if (Zstd.isAvailable()) {
248 options.add(StandardCompressionOptions.zstd());
249 }
250 return options.toArray(new CompressionOptions[0]);
251 }
252
253 @Override
254 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
255 this.ctx = ctx;
256 }
257
258 @Override
259 protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
260 if (this.contentSizeThreshold > 0) {
261 if (httpResponse instanceof HttpContent &&
262 ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
263 return null;
264 }
265 }
266
267 String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
268 if (contentEncoding != null) {
269
270
271 return null;
272 }
273
274 String targetContentEncoding = determineEncoding(acceptEncoding);
275 if (targetContentEncoding == null) {
276 return null;
277 }
278
279 CompressionEncoderFactory encoderFactory = factories.get(targetContentEncoding);
280
281 if (encoderFactory == null) {
282 throw new IllegalStateException("Couldn't find CompressionEncoderFactory: " + targetContentEncoding);
283 }
284
285 Channel channel = ctx.channel();
286 return new Result(targetContentEncoding,
287 EmbeddedChannel.builder()
288 .channelId(channel.id())
289 .hasDisconnect(channel.metadata().hasDisconnect())
290 .config(channel.config())
291 .handlers(encoderFactory.createEncoder())
292 .build());
293 }
294
295 @SuppressWarnings("FloatingPointEquality")
296 protected String determineEncoding(String acceptEncoding) {
297 float starQ = -1.0f;
298 float brQ = -1.0f;
299 float zstdQ = -1.0f;
300 float snappyQ = -1.0f;
301 float gzipQ = -1.0f;
302 float deflateQ = -1.0f;
303
304 int start = 0;
305 int length = acceptEncoding.length();
306 while (start < length) {
307 int comma = acceptEncoding.indexOf(',', start);
308 if (comma == -1) {
309 comma = length;
310 }
311 String encoding = acceptEncoding.substring(start, comma);
312 float q = 1.0f;
313 int equalsPos = encoding.indexOf('=');
314 if (equalsPos != -1) {
315 try {
316 q = Float.parseFloat(encoding.substring(equalsPos + 1));
317 } catch (NumberFormatException e) {
318
319 q = 0.0f;
320 }
321 }
322 if (encoding.contains("*")) {
323 starQ = q;
324 } else if (encoding.contains("br") && q > brQ) {
325 brQ = q;
326 } else if (encoding.contains("zstd") && q > zstdQ) {
327 zstdQ = q;
328 } else if (encoding.contains("snappy") && q > snappyQ) {
329 snappyQ = q;
330 } else if (encoding.contains("gzip") && q > gzipQ) {
331 gzipQ = q;
332 } else if (encoding.contains("deflate") && q > deflateQ) {
333 deflateQ = q;
334 }
335 start = comma + 1;
336 }
337 if (brQ > 0.0f || zstdQ > 0.0f || snappyQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
338 if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
339 return "br";
340 } else if (zstdQ != -1.0f && zstdQ >= snappyQ && this.zstdOptions != null) {
341 return "zstd";
342 } else if (snappyQ != -1.0f && snappyQ >= gzipQ && this.snappyOptions != null) {
343 return "snappy";
344 } else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
345 return "gzip";
346 } else if (deflateQ != -1.0f && this.deflateOptions != null) {
347 return "deflate";
348 }
349 }
350 if (starQ > 0.0f) {
351 if (brQ == -1.0f && this.brotliOptions != null) {
352 return "br";
353 }
354 if (zstdQ == -1.0f && this.zstdOptions != null) {
355 return "zstd";
356 }
357 if (snappyQ == -1.0f && this.snappyOptions != null) {
358 return "snappy";
359 }
360 if (gzipQ == -1.0f && this.gzipOptions != null) {
361 return "gzip";
362 }
363 if (deflateQ == -1.0f && this.deflateOptions != null) {
364 return "deflate";
365 }
366 }
367 return null;
368 }
369
370 @Deprecated
371 @SuppressWarnings("FloatingPointEquality")
372 protected ZlibWrapper determineWrapper(String acceptEncoding) {
373 float starQ = -1.0f;
374 float gzipQ = -1.0f;
375 float deflateQ = -1.0f;
376 for (String encoding : acceptEncoding.split(",")) {
377 float q = 1.0f;
378 int equalsPos = encoding.indexOf('=');
379 if (equalsPos != -1) {
380 try {
381 q = Float.parseFloat(encoding.substring(equalsPos + 1));
382 } catch (NumberFormatException e) {
383
384 q = 0.0f;
385 }
386 }
387 if (encoding.contains("*")) {
388 starQ = q;
389 } else if (encoding.contains("gzip") && q > gzipQ) {
390 gzipQ = q;
391 } else if (encoding.contains("deflate") && q > deflateQ) {
392 deflateQ = q;
393 }
394 }
395 if (gzipQ > 0.0f || deflateQ > 0.0f) {
396 if (gzipQ >= deflateQ) {
397 return ZlibWrapper.GZIP;
398 } else {
399 return ZlibWrapper.ZLIB;
400 }
401 }
402 if (starQ > 0.0f) {
403 if (gzipQ == -1.0f) {
404 return ZlibWrapper.GZIP;
405 }
406 if (deflateQ == -1.0f) {
407 return ZlibWrapper.ZLIB;
408 }
409 }
410 return null;
411 }
412
413
414
415
416
417 private final class GzipEncoderFactory implements CompressionEncoderFactory {
418
419 @Override
420 public MessageToByteEncoder<ByteBuf> createEncoder() {
421 return ZlibCodecFactory.newZlibEncoder(
422 ZlibWrapper.GZIP, gzipOptions.compressionLevel(),
423 gzipOptions.windowBits(), gzipOptions.memLevel());
424 }
425 }
426
427
428
429
430
431 private final class DeflateEncoderFactory implements CompressionEncoderFactory {
432
433 @Override
434 public MessageToByteEncoder<ByteBuf> createEncoder() {
435 return ZlibCodecFactory.newZlibEncoder(
436 ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
437 deflateOptions.windowBits(), deflateOptions.memLevel());
438 }
439 }
440
441
442
443
444
445 private final class BrEncoderFactory implements CompressionEncoderFactory {
446
447 @Override
448 public MessageToByteEncoder<ByteBuf> createEncoder() {
449 return new BrotliEncoder(brotliOptions.parameters());
450 }
451 }
452
453
454
455
456
457 private final class ZstdEncoderFactory implements CompressionEncoderFactory {
458
459 @Override
460 public MessageToByteEncoder<ByteBuf> createEncoder() {
461 return new ZstdEncoder(zstdOptions.compressionLevel(),
462 zstdOptions.blockSize(), zstdOptions.maxEncodeSize());
463 }
464 }
465
466
467
468
469
470 private static final class SnappyEncoderFactory implements CompressionEncoderFactory {
471
472 @Override
473 public MessageToByteEncoder<ByteBuf> createEncoder() {
474 return new SnappyFrameEncoder();
475 }
476 }
477 }