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.HashMap;
19 import java.util.Map;
20
21 import io.netty.buffer.ByteBuf;
22 import io.netty.channel.ChannelHandlerContext;
23 import io.netty.channel.embedded.EmbeddedChannel;
24 import io.netty.handler.codec.MessageToByteEncoder;
25 import io.netty.handler.codec.compression.Brotli;
26 import io.netty.handler.codec.compression.BrotliEncoder;
27 import io.netty.handler.codec.compression.BrotliOptions;
28 import io.netty.handler.codec.compression.CompressionOptions;
29 import io.netty.handler.codec.compression.DeflateOptions;
30 import io.netty.handler.codec.compression.GzipOptions;
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
41
42
43
44
45
46
47 public class HttpContentCompressor extends HttpContentEncoder {
48
49 private final boolean supportsCompressionOptions;
50 private final BrotliOptions brotliOptions;
51 private final GzipOptions gzipOptions;
52 private final DeflateOptions deflateOptions;
53 private final ZstdOptions zstdOptions;
54
55 private final int compressionLevel;
56 private final int windowBits;
57 private final int memLevel;
58 private final int contentSizeThreshold;
59 private ChannelHandlerContext ctx;
60 private final Map<String, CompressionEncoderFactory> factories;
61
62
63
64
65
66 public HttpContentCompressor() {
67 this(6);
68 }
69
70
71
72
73
74
75
76
77
78
79 @Deprecated
80 public HttpContentCompressor(int compressionLevel) {
81 this(compressionLevel, 15, 8, 0);
82 }
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103 @Deprecated
104 public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
105 this(compressionLevel, windowBits, memLevel, 0);
106 }
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 @Deprecated
132 public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
133 this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
134 this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
135 this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
136 this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
137 this.brotliOptions = null;
138 this.gzipOptions = null;
139 this.deflateOptions = null;
140 this.zstdOptions = null;
141 this.factories = null;
142 this.supportsCompressionOptions = false;
143 }
144
145
146
147
148
149
150
151
152 public HttpContentCompressor(CompressionOptions... compressionOptions) {
153 this(0, compressionOptions);
154 }
155
156
157
158
159
160
161
162
163
164
165
166
167 public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
168 this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
169 BrotliOptions brotliOptions = null;
170 GzipOptions gzipOptions = null;
171 DeflateOptions deflateOptions = null;
172 ZstdOptions zstdOptions = null;
173 if (compressionOptions == null || compressionOptions.length == 0) {
174 brotliOptions = Brotli.isAvailable() ? StandardCompressionOptions.brotli() : null;
175 gzipOptions = StandardCompressionOptions.gzip();
176 deflateOptions = StandardCompressionOptions.deflate();
177 zstdOptions = Zstd.isAvailable() ? StandardCompressionOptions.zstd() : null;
178 } else {
179 ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
180 for (CompressionOptions compressionOption : compressionOptions) {
181
182
183
184
185
186
187 if (Brotli.isAvailable() && compressionOption instanceof BrotliOptions) {
188 brotliOptions = (BrotliOptions) compressionOption;
189 } else if (compressionOption instanceof GzipOptions) {
190 gzipOptions = (GzipOptions) compressionOption;
191 } else if (compressionOption instanceof DeflateOptions) {
192 deflateOptions = (DeflateOptions) compressionOption;
193 } else if (compressionOption instanceof ZstdOptions) {
194 zstdOptions = (ZstdOptions) compressionOption;
195 } else {
196 throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
197 ": " + compressionOption);
198 }
199 }
200 }
201
202 this.gzipOptions = gzipOptions;
203 this.deflateOptions = deflateOptions;
204 this.brotliOptions = brotliOptions;
205 this.zstdOptions = zstdOptions;
206
207 this.factories = new HashMap<String, CompressionEncoderFactory>();
208
209 if (this.gzipOptions != null) {
210 this.factories.put("gzip", new GzipEncoderFactory());
211 }
212 if (this.deflateOptions != null) {
213 this.factories.put("deflate", new DeflateEncoderFactory());
214 }
215 if (Brotli.isAvailable() && this.brotliOptions != null) {
216 this.factories.put("br", new BrEncoderFactory());
217 }
218 if (this.zstdOptions != null) {
219 this.factories.put("zstd", new ZstdEncoderFactory());
220 }
221
222 this.compressionLevel = -1;
223 this.windowBits = -1;
224 this.memLevel = -1;
225 supportsCompressionOptions = true;
226 }
227
228 @Override
229 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
230 this.ctx = ctx;
231 }
232
233 @Override
234 protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
235 if (this.contentSizeThreshold > 0) {
236 if (httpResponse instanceof HttpContent &&
237 ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
238 return null;
239 }
240 }
241
242 String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
243 if (contentEncoding != null) {
244
245
246 return null;
247 }
248
249 if (supportsCompressionOptions) {
250 String targetContentEncoding = determineEncoding(acceptEncoding);
251 if (targetContentEncoding == null) {
252 return null;
253 }
254
255 CompressionEncoderFactory encoderFactory = factories.get(targetContentEncoding);
256
257 if (encoderFactory == null) {
258 throw new Error();
259 }
260
261 return new Result(targetContentEncoding,
262 new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
263 ctx.channel().config(), encoderFactory.createEncoder()));
264 } else {
265 ZlibWrapper wrapper = determineWrapper(acceptEncoding);
266 if (wrapper == null) {
267 return null;
268 }
269
270 String targetContentEncoding;
271 switch (wrapper) {
272 case GZIP:
273 targetContentEncoding = "gzip";
274 break;
275 case ZLIB:
276 targetContentEncoding = "deflate";
277 break;
278 default:
279 throw new Error();
280 }
281
282 return new Result(
283 targetContentEncoding,
284 new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
285 ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
286 wrapper, compressionLevel, windowBits, memLevel)));
287 }
288 }
289
290 @SuppressWarnings("FloatingPointEquality")
291 protected String determineEncoding(String acceptEncoding) {
292 float starQ = -1.0f;
293 float brQ = -1.0f;
294 float zstdQ = -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("gzip") && q > gzipQ) {
315 gzipQ = q;
316 } else if (encoding.contains("deflate") && q > deflateQ) {
317 deflateQ = q;
318 }
319 }
320 if (brQ > 0.0f || zstdQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
321 if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
322 return "br";
323 } else if (zstdQ != -1.0f && zstdQ >= gzipQ && this.zstdOptions != null) {
324 return "zstd";
325 } else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
326 return "gzip";
327 } else if (deflateQ != -1.0f && this.deflateOptions != null) {
328 return "deflate";
329 }
330 }
331 if (starQ > 0.0f) {
332 if (brQ == -1.0f && this.brotliOptions != null) {
333 return "br";
334 }
335 if (zstdQ == -1.0f && this.zstdOptions != null) {
336 return "zstd";
337 }
338 if (gzipQ == -1.0f && this.gzipOptions != null) {
339 return "gzip";
340 }
341 if (deflateQ == -1.0f && this.deflateOptions != null) {
342 return "deflate";
343 }
344 }
345 return null;
346 }
347
348 @Deprecated
349 @SuppressWarnings("FloatingPointEquality")
350 protected ZlibWrapper determineWrapper(String acceptEncoding) {
351 float starQ = -1.0f;
352 float gzipQ = -1.0f;
353 float deflateQ = -1.0f;
354 for (String encoding : acceptEncoding.split(",")) {
355 float q = 1.0f;
356 int equalsPos = encoding.indexOf('=');
357 if (equalsPos != -1) {
358 try {
359 q = Float.parseFloat(encoding.substring(equalsPos + 1));
360 } catch (NumberFormatException e) {
361
362 q = 0.0f;
363 }
364 }
365 if (encoding.contains("*")) {
366 starQ = q;
367 } else if (encoding.contains("gzip") && q > gzipQ) {
368 gzipQ = q;
369 } else if (encoding.contains("deflate") && q > deflateQ) {
370 deflateQ = q;
371 }
372 }
373 if (gzipQ > 0.0f || deflateQ > 0.0f) {
374 if (gzipQ >= deflateQ) {
375 return ZlibWrapper.GZIP;
376 } else {
377 return ZlibWrapper.ZLIB;
378 }
379 }
380 if (starQ > 0.0f) {
381 if (gzipQ == -1.0f) {
382 return ZlibWrapper.GZIP;
383 }
384 if (deflateQ == -1.0f) {
385 return ZlibWrapper.ZLIB;
386 }
387 }
388 return null;
389 }
390
391
392
393
394
395 private final class GzipEncoderFactory implements CompressionEncoderFactory {
396
397 @Override
398 public MessageToByteEncoder<ByteBuf> createEncoder() {
399 return ZlibCodecFactory.newZlibEncoder(
400 ZlibWrapper.GZIP, gzipOptions.compressionLevel(),
401 gzipOptions.windowBits(), gzipOptions.memLevel());
402 }
403 }
404
405
406
407
408
409 private final class DeflateEncoderFactory implements CompressionEncoderFactory {
410
411 @Override
412 public MessageToByteEncoder<ByteBuf> createEncoder() {
413 return ZlibCodecFactory.newZlibEncoder(
414 ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
415 deflateOptions.windowBits(), deflateOptions.memLevel());
416 }
417 }
418
419
420
421
422
423 private final class BrEncoderFactory implements CompressionEncoderFactory {
424
425 @Override
426 public MessageToByteEncoder<ByteBuf> createEncoder() {
427 return new BrotliEncoder(brotliOptions.parameters());
428 }
429 }
430
431
432
433
434
435 private final class ZstdEncoderFactory implements CompressionEncoderFactory {
436
437 @Override
438 public MessageToByteEncoder<ByteBuf> createEncoder() {
439 return new ZstdEncoder(zstdOptions.compressionLevel(),
440 zstdOptions.blockSize(), zstdOptions.maxEncodeSize());
441 }
442 }
443 }