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.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
46
47
48
49
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
79
80
81
82 public HttpContentCompressor() {
83 this(0, (CompressionOptions[]) null);
84 }
85
86
87
88
89
90
91
92
93
94
95 @Deprecated
96 public HttpContentCompressor(int compressionLevel) {
97 this(compressionLevel, 15, 8, 0);
98 }
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 @Deprecated
120 public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
121 this(compressionLevel, windowBits, memLevel, 0);
122 }
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
167
168
169
170
171
172 public HttpContentCompressor(CompressionOptions... compressionOptions) {
173 this(0, compressionOptions);
174 }
175
176
177
178
179
180
181
182
183
184
185
186
187 public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
188 this(contentSizeThreshold, DEFAULT_MAX_PIPELINE_DEPTH, compressionOptions);
189 }
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205 public HttpContentCompressor(int contentSizeThreshold, int maxPipelineDepth,
206 CompressionOptions... compressionOptions) {
207 super(maxPipelineDepth);
208 this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
209 BrotliOptions brotliOptions = null;
210 GzipOptions gzipOptions = null;
211 DeflateOptions deflateOptions = null;
212 ZstdOptions zstdOptions = null;
213 SnappyOptions snappyOptions = null;
214 if (compressionOptions == null || compressionOptions.length == 0) {
215 compressionOptions = DEFAULT_COMPRESSION_OPTIONS;
216 }
217
218 ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
219 for (CompressionOptions compressionOption : compressionOptions) {
220
221
222
223
224
225
226 if (Brotli.isAvailable() && compressionOption instanceof BrotliOptions) {
227 brotliOptions = (BrotliOptions) compressionOption;
228 } else if (compressionOption instanceof GzipOptions) {
229 gzipOptions = (GzipOptions) compressionOption;
230 } else if (compressionOption instanceof DeflateOptions) {
231 deflateOptions = (DeflateOptions) compressionOption;
232 } else if (Zstd.isAvailable() && compressionOption instanceof ZstdOptions) {
233 zstdOptions = (ZstdOptions) compressionOption;
234 } else if (compressionOption instanceof SnappyOptions) {
235 snappyOptions = (SnappyOptions) compressionOption;
236 } else {
237 throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
238 ": " + compressionOption);
239 }
240 }
241
242 this.gzipOptions = gzipOptions;
243 this.deflateOptions = deflateOptions;
244 this.brotliOptions = brotliOptions;
245 this.zstdOptions = zstdOptions;
246 this.snappyOptions = snappyOptions;
247 }
248
249 @Deprecated
250 private static CompressionOptions[] defaultCompressionOptions(
251 GzipOptions gzipOptions, DeflateOptions deflateOptions) {
252 List<CompressionOptions> options = new ArrayList<>(5);
253 options.add(gzipOptions);
254 options.add(deflateOptions);
255 options.add(StandardCompressionOptions.snappy());
256
257 if (Brotli.isAvailable()) {
258 options.add(StandardCompressionOptions.brotli());
259 }
260 if (Zstd.isAvailable()) {
261 options.add(StandardCompressionOptions.zstd());
262 }
263 return options.toArray(new CompressionOptions[0]);
264 }
265
266 @Override
267 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
268 this.ctx = ctx;
269 }
270
271 @Override
272 protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
273 if (this.contentSizeThreshold > 0) {
274 if (httpResponse instanceof HttpContent &&
275 ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
276 return null;
277 }
278 }
279
280 String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
281 if (contentEncoding != null) {
282
283
284 return null;
285 }
286
287 String targetContentEncoding = determineEncoding(acceptEncoding);
288 if (targetContentEncoding == null) {
289 return null;
290 }
291
292 Channel channel = ctx.channel();
293 return new Result(targetContentEncoding,
294 EmbeddedChannel.builder()
295 .channelId(channel.id())
296 .hasDisconnect(channel.metadata().hasDisconnect())
297 .config(channel.config())
298 .handlers(createEncoderFor(targetContentEncoding))
299 .build());
300 }
301
302 private MessageToByteEncoder<ByteBuf> createEncoderFor(String targetContentEncoding) {
303 switch (targetContentEncoding) {
304 case "br":
305 if (brotliOptions == null || !Brotli.isAvailable()) {
306 throw new IllegalStateException("Brotli not configured");
307 }
308 return new BrotliEncoder(brotliOptions.parameters());
309 case "zstd":
310 if (zstdOptions == null) {
311 throw new IllegalStateException("Zstd not configured");
312 }
313 return new ZstdEncoder(
314 zstdOptions.compressionLevel(),
315 zstdOptions.blockSize(),
316 zstdOptions.maxEncodeSize()
317 );
318 case "snappy":
319 if (snappyOptions == null) {
320 throw new IllegalStateException("Snappy not configured");
321 }
322 return new SnappyFrameEncoder();
323 case "gzip":
324 if (gzipOptions == null) {
325 throw new IllegalStateException("Gzip not configured");
326 }
327 return ZlibCodecFactory.newZlibEncoder(
328 ZlibWrapper.GZIP,
329 gzipOptions.compressionLevel(),
330 gzipOptions.windowBits(),
331 gzipOptions.memLevel()
332 );
333 case "deflate":
334 if (deflateOptions == null) {
335 throw new IllegalStateException("Deflate not configured");
336 }
337 return ZlibCodecFactory.newZlibEncoder(
338 ZlibWrapper.ZLIB,
339 deflateOptions.compressionLevel(),
340 deflateOptions.windowBits(),
341 deflateOptions.memLevel()
342 );
343 default:
344 throw new IllegalStateException("Unknown encoding: " + targetContentEncoding);
345 }
346 }
347
348 @SuppressWarnings("FloatingPointEquality")
349 protected String determineEncoding(String acceptEncoding) {
350 float starQ = -1.0f;
351 float brQ = -1.0f;
352 float zstdQ = -1.0f;
353 float snappyQ = -1.0f;
354 float gzipQ = -1.0f;
355 float deflateQ = -1.0f;
356
357 int start = 0;
358 int length = acceptEncoding.length();
359 while (start < length) {
360 int comma = acceptEncoding.indexOf(',', start);
361 if (comma == -1) {
362 comma = length;
363 }
364 String encoding = acceptEncoding.substring(start, comma);
365 float q = 1.0f;
366 int equalsPos = encoding.indexOf('=');
367 if (equalsPos != -1) {
368 try {
369 q = Float.parseFloat(encoding.substring(equalsPos + 1));
370 } catch (NumberFormatException e) {
371
372 q = 0.0f;
373 }
374 }
375 if (encoding.contains("*")) {
376 starQ = q;
377 } else if (encoding.contains("br") && q > brQ) {
378 brQ = q;
379 } else if (encoding.contains("zstd") && q > zstdQ) {
380 zstdQ = q;
381 } else if (encoding.contains("snappy") && q > snappyQ) {
382 snappyQ = 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 start = comma + 1;
389 }
390 if (brQ > 0.0f || zstdQ > 0.0f || snappyQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
391 if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
392 return "br";
393 } else if (zstdQ != -1.0f && zstdQ >= snappyQ && this.zstdOptions != null) {
394 return "zstd";
395 } else if (snappyQ != -1.0f && snappyQ >= gzipQ && this.snappyOptions != null) {
396 return "snappy";
397 } else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
398 return "gzip";
399 } else if (deflateQ != -1.0f && this.deflateOptions != null) {
400 return "deflate";
401 }
402 }
403 if (starQ > 0.0f) {
404 if (brQ == -1.0f && this.brotliOptions != null) {
405 return "br";
406 }
407 if (zstdQ == -1.0f && this.zstdOptions != null) {
408 return "zstd";
409 }
410 if (snappyQ == -1.0f && this.snappyOptions != null) {
411 return "snappy";
412 }
413 if (gzipQ == -1.0f && this.gzipOptions != null) {
414 return "gzip";
415 }
416 if (deflateQ == -1.0f && this.deflateOptions != null) {
417 return "deflate";
418 }
419 }
420 return null;
421 }
422
423 @Deprecated
424 @SuppressWarnings("FloatingPointEquality")
425 protected ZlibWrapper determineWrapper(String acceptEncoding) {
426 float starQ = -1.0f;
427 float gzipQ = -1.0f;
428 float deflateQ = -1.0f;
429 for (String encoding : acceptEncoding.split(",")) {
430 float q = 1.0f;
431 int equalsPos = encoding.indexOf('=');
432 if (equalsPos != -1) {
433 try {
434 q = Float.parseFloat(encoding.substring(equalsPos + 1));
435 } catch (NumberFormatException e) {
436
437 q = 0.0f;
438 }
439 }
440 if (encoding.contains("*")) {
441 starQ = q;
442 } else if (encoding.contains("gzip") && q > gzipQ) {
443 gzipQ = q;
444 } else if (encoding.contains("deflate") && q > deflateQ) {
445 deflateQ = q;
446 }
447 }
448 if (gzipQ > 0.0f || deflateQ > 0.0f) {
449 if (gzipQ >= deflateQ) {
450 return ZlibWrapper.GZIP;
451 } else {
452 return ZlibWrapper.ZLIB;
453 }
454 }
455 if (starQ > 0.0f) {
456 if (gzipQ == -1.0f) {
457 return ZlibWrapper.GZIP;
458 }
459 if (deflateQ == -1.0f) {
460 return ZlibWrapper.ZLIB;
461 }
462 }
463 return null;
464 }
465
466 }