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 = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
189 BrotliOptions brotliOptions = null;
190 GzipOptions gzipOptions = null;
191 DeflateOptions deflateOptions = null;
192 ZstdOptions zstdOptions = null;
193 SnappyOptions snappyOptions = null;
194 if (compressionOptions == null || compressionOptions.length == 0) {
195 compressionOptions = DEFAULT_COMPRESSION_OPTIONS;
196 }
197
198 ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
199 for (CompressionOptions compressionOption : compressionOptions) {
200
201
202
203
204
205
206 if (Brotli.isAvailable() && compressionOption instanceof BrotliOptions) {
207 brotliOptions = (BrotliOptions) compressionOption;
208 } else if (compressionOption instanceof GzipOptions) {
209 gzipOptions = (GzipOptions) compressionOption;
210 } else if (compressionOption instanceof DeflateOptions) {
211 deflateOptions = (DeflateOptions) compressionOption;
212 } else if (Zstd.isAvailable() && compressionOption instanceof ZstdOptions) {
213 zstdOptions = (ZstdOptions) compressionOption;
214 } else if (compressionOption instanceof SnappyOptions) {
215 snappyOptions = (SnappyOptions) compressionOption;
216 } else {
217 throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
218 ": " + compressionOption);
219 }
220 }
221
222 this.gzipOptions = gzipOptions;
223 this.deflateOptions = deflateOptions;
224 this.brotliOptions = brotliOptions;
225 this.zstdOptions = zstdOptions;
226 this.snappyOptions = snappyOptions;
227 }
228
229 @Deprecated
230 private static CompressionOptions[] defaultCompressionOptions(
231 GzipOptions gzipOptions, DeflateOptions deflateOptions) {
232 List<CompressionOptions> options = new ArrayList<>(5);
233 options.add(gzipOptions);
234 options.add(deflateOptions);
235 options.add(StandardCompressionOptions.snappy());
236
237 if (Brotli.isAvailable()) {
238 options.add(StandardCompressionOptions.brotli());
239 }
240 if (Zstd.isAvailable()) {
241 options.add(StandardCompressionOptions.zstd());
242 }
243 return options.toArray(new CompressionOptions[0]);
244 }
245
246 @Override
247 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
248 this.ctx = ctx;
249 }
250
251 @Override
252 protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
253 if (this.contentSizeThreshold > 0) {
254 if (httpResponse instanceof HttpContent &&
255 ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
256 return null;
257 }
258 }
259
260 String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
261 if (contentEncoding != null) {
262
263
264 return null;
265 }
266
267 String targetContentEncoding = determineEncoding(acceptEncoding);
268 if (targetContentEncoding == null) {
269 return null;
270 }
271
272 Channel channel = ctx.channel();
273 return new Result(targetContentEncoding,
274 EmbeddedChannel.builder()
275 .channelId(channel.id())
276 .hasDisconnect(channel.metadata().hasDisconnect())
277 .config(channel.config())
278 .handlers(createEncoderFor(targetContentEncoding))
279 .build());
280 }
281
282 private MessageToByteEncoder<ByteBuf> createEncoderFor(String targetContentEncoding) {
283 switch (targetContentEncoding) {
284 case "br":
285 if (brotliOptions == null || !Brotli.isAvailable()) {
286 throw new IllegalStateException("Brotli not configured");
287 }
288 return new BrotliEncoder(brotliOptions.parameters());
289 case "zstd":
290 if (zstdOptions == null) {
291 throw new IllegalStateException("Zstd not configured");
292 }
293 return new ZstdEncoder(
294 zstdOptions.compressionLevel(),
295 zstdOptions.blockSize(),
296 zstdOptions.maxEncodeSize()
297 );
298 case "snappy":
299 if (snappyOptions == null) {
300 throw new IllegalStateException("Snappy not configured");
301 }
302 return new SnappyFrameEncoder();
303 case "gzip":
304 if (gzipOptions == null) {
305 throw new IllegalStateException("Gzip not configured");
306 }
307 return ZlibCodecFactory.newZlibEncoder(
308 ZlibWrapper.GZIP,
309 gzipOptions.compressionLevel(),
310 gzipOptions.windowBits(),
311 gzipOptions.memLevel()
312 );
313 case "deflate":
314 if (deflateOptions == null) {
315 throw new IllegalStateException("Deflate not configured");
316 }
317 return ZlibCodecFactory.newZlibEncoder(
318 ZlibWrapper.ZLIB,
319 deflateOptions.compressionLevel(),
320 deflateOptions.windowBits(),
321 deflateOptions.memLevel()
322 );
323 default:
324 throw new IllegalStateException("Unknown encoding: " + targetContentEncoding);
325 }
326 }
327
328 @SuppressWarnings("FloatingPointEquality")
329 protected String determineEncoding(String acceptEncoding) {
330 float starQ = -1.0f;
331 float brQ = -1.0f;
332 float zstdQ = -1.0f;
333 float snappyQ = -1.0f;
334 float gzipQ = -1.0f;
335 float deflateQ = -1.0f;
336
337 int start = 0;
338 int length = acceptEncoding.length();
339 while (start < length) {
340 int comma = acceptEncoding.indexOf(',', start);
341 if (comma == -1) {
342 comma = length;
343 }
344 String encoding = acceptEncoding.substring(start, comma);
345 float q = 1.0f;
346 int equalsPos = encoding.indexOf('=');
347 if (equalsPos != -1) {
348 try {
349 q = Float.parseFloat(encoding.substring(equalsPos + 1));
350 } catch (NumberFormatException e) {
351
352 q = 0.0f;
353 }
354 }
355 if (encoding.contains("*")) {
356 starQ = q;
357 } else if (encoding.contains("br") && q > brQ) {
358 brQ = q;
359 } else if (encoding.contains("zstd") && q > zstdQ) {
360 zstdQ = q;
361 } else if (encoding.contains("snappy") && q > snappyQ) {
362 snappyQ = q;
363 } else if (encoding.contains("gzip") && q > gzipQ) {
364 gzipQ = q;
365 } else if (encoding.contains("deflate") && q > deflateQ) {
366 deflateQ = q;
367 }
368 start = comma + 1;
369 }
370 if (brQ > 0.0f || zstdQ > 0.0f || snappyQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
371 if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
372 return "br";
373 } else if (zstdQ != -1.0f && zstdQ >= snappyQ && this.zstdOptions != null) {
374 return "zstd";
375 } else if (snappyQ != -1.0f && snappyQ >= gzipQ && this.snappyOptions != null) {
376 return "snappy";
377 } else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
378 return "gzip";
379 } else if (deflateQ != -1.0f && this.deflateOptions != null) {
380 return "deflate";
381 }
382 }
383 if (starQ > 0.0f) {
384 if (brQ == -1.0f && this.brotliOptions != null) {
385 return "br";
386 }
387 if (zstdQ == -1.0f && this.zstdOptions != null) {
388 return "zstd";
389 }
390 if (snappyQ == -1.0f && this.snappyOptions != null) {
391 return "snappy";
392 }
393 if (gzipQ == -1.0f && this.gzipOptions != null) {
394 return "gzip";
395 }
396 if (deflateQ == -1.0f && this.deflateOptions != null) {
397 return "deflate";
398 }
399 }
400 return null;
401 }
402
403 @Deprecated
404 @SuppressWarnings("FloatingPointEquality")
405 protected ZlibWrapper determineWrapper(String acceptEncoding) {
406 float starQ = -1.0f;
407 float gzipQ = -1.0f;
408 float deflateQ = -1.0f;
409 for (String encoding : acceptEncoding.split(",")) {
410 float q = 1.0f;
411 int equalsPos = encoding.indexOf('=');
412 if (equalsPos != -1) {
413 try {
414 q = Float.parseFloat(encoding.substring(equalsPos + 1));
415 } catch (NumberFormatException e) {
416
417 q = 0.0f;
418 }
419 }
420 if (encoding.contains("*")) {
421 starQ = q;
422 } else if (encoding.contains("gzip") && q > gzipQ) {
423 gzipQ = q;
424 } else if (encoding.contains("deflate") && q > deflateQ) {
425 deflateQ = q;
426 }
427 }
428 if (gzipQ > 0.0f || deflateQ > 0.0f) {
429 if (gzipQ >= deflateQ) {
430 return ZlibWrapper.GZIP;
431 } else {
432 return ZlibWrapper.ZLIB;
433 }
434 }
435 if (starQ > 0.0f) {
436 if (gzipQ == -1.0f) {
437 return ZlibWrapper.GZIP;
438 }
439 if (deflateQ == -1.0f) {
440 return ZlibWrapper.ZLIB;
441 }
442 }
443 return null;
444 }
445
446 }