1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package io.netty5.handler.codec.http2;
16
17 import io.netty5.buffer.api.Buffer;
18 import io.netty5.channel.ChannelHandlerContext;
19 import io.netty5.channel.embedded.EmbeddedChannel;
20 import io.netty5.handler.codec.ByteToMessageDecoder;
21 import io.netty5.handler.codec.compression.Brotli;
22 import io.netty5.handler.codec.compression.BrotliCompressor;
23 import io.netty5.handler.codec.compression.BrotliOptions;
24 import io.netty5.handler.codec.compression.CompressionOptions;
25 import io.netty5.handler.codec.compression.Compressor;
26 import io.netty5.handler.codec.compression.DeflateOptions;
27 import io.netty5.handler.codec.compression.GzipOptions;
28 import io.netty5.handler.codec.compression.StandardCompressionOptions;
29 import io.netty5.handler.codec.compression.ZlibCompressor;
30 import io.netty5.handler.codec.compression.ZlibWrapper;
31 import io.netty5.handler.codec.compression.ZstdCompressor;
32 import io.netty5.handler.codec.compression.ZstdOptions;
33 import io.netty5.util.concurrent.Future;
34 import io.netty5.util.concurrent.Promise;
35 import io.netty5.util.concurrent.PromiseCombiner;
36 import io.netty5.util.internal.ObjectUtil;
37 import io.netty5.util.internal.UnstableApi;
38
39 import static io.netty5.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
40 import static io.netty5.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
41 import static io.netty5.handler.codec.http.HttpHeaderValues.BR;
42 import static io.netty5.handler.codec.http.HttpHeaderValues.DEFLATE;
43 import static io.netty5.handler.codec.http.HttpHeaderValues.GZIP;
44 import static io.netty5.handler.codec.http.HttpHeaderValues.IDENTITY;
45 import static io.netty5.handler.codec.http.HttpHeaderValues.X_DEFLATE;
46 import static io.netty5.handler.codec.http.HttpHeaderValues.X_GZIP;
47 import static io.netty5.handler.codec.http.HttpHeaderValues.ZSTD;
48 import static java.util.Objects.requireNonNull;
49
50
51
52
53
54 @UnstableApi
55 public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionEncoder {
56 private int compressionLevel;
57 private int windowBits;
58 private int memLevel;
59 private final Http2Connection.PropertyKey propertyKey;
60
61 private final boolean supportsCompressionOptions;
62
63 private BrotliOptions brotliOptions;
64 private GzipOptions gzipCompressionOptions;
65 private DeflateOptions deflateOptions;
66 private ZstdOptions zstdOptions;
67
68
69
70
71
72 public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate) {
73 this(delegate, defaultCompressionOptions());
74 }
75
76
77
78
79 @Deprecated
80 public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate, int compressionLevel, int windowBits,
81 int memLevel) {
82 super(delegate);
83 this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
84 this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
85 this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
86
87 propertyKey = connection().newKey();
88 connection().addListener(new Http2ConnectionAdapter() {
89 @Override
90 public void onStreamRemoved(Http2Stream stream) {
91 final Compressor compressor = stream.getProperty(propertyKey);
92 if (compressor != null) {
93 cleanup(stream, compressor);
94 }
95 }
96 });
97
98 supportsCompressionOptions = false;
99 }
100
101
102
103
104
105 public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate,
106 CompressionOptions... compressionOptionsArgs) {
107 super(delegate);
108 requireNonNull(compressionOptionsArgs, "CompressionOptions");
109 ObjectUtil.deepCheckNotNull("CompressionOptions", compressionOptionsArgs);
110
111 for (CompressionOptions compressionOptions : compressionOptionsArgs) {
112
113
114
115
116
117
118 if (Brotli.isAvailable() && compressionOptions instanceof BrotliOptions) {
119 brotliOptions = (BrotliOptions) compressionOptions;
120 } else if (compressionOptions instanceof GzipOptions) {
121 gzipCompressionOptions = (GzipOptions) compressionOptions;
122 } else if (compressionOptions instanceof DeflateOptions) {
123 deflateOptions = (DeflateOptions) compressionOptions;
124 } else if (compressionOptions instanceof ZstdOptions) {
125 zstdOptions = (ZstdOptions) compressionOptions;
126 } else {
127 throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
128 ": " + compressionOptions);
129 }
130 }
131
132 supportsCompressionOptions = true;
133
134 propertyKey = connection().newKey();
135 connection().addListener(new Http2ConnectionAdapter() {
136 @Override
137 public void onStreamRemoved(Http2Stream stream) {
138 final Compressor compressor = stream.getProperty(propertyKey);
139 if (compressor != null) {
140 cleanup(stream, compressor);
141 }
142 }
143 });
144 }
145
146 private static CompressionOptions[] defaultCompressionOptions() {
147 if (Brotli.isAvailable()) {
148 return new CompressionOptions[] {
149 StandardCompressionOptions.brotli(),
150 StandardCompressionOptions.gzip(),
151 StandardCompressionOptions.deflate() };
152 }
153 return new CompressionOptions[] { StandardCompressionOptions.gzip(), StandardCompressionOptions.deflate() };
154 }
155
156 @Override
157 public Future<Void> writeData(final ChannelHandlerContext ctx, final int streamId, Buffer data, int padding,
158 final boolean endOfStream) {
159 final Http2Stream stream = connection().stream(streamId);
160 final Compressor compressor = stream == null ? null : (Compressor) stream.getProperty(propertyKey);
161 if (compressor == null) {
162
163 return super.writeData(ctx, streamId, data, padding, endOfStream);
164 }
165
166 try {
167 Buffer buf = compressor.compress(data, ctx.bufferAllocator());
168 if (buf.readableBytes() == 0) {
169 buf.close();
170 if (endOfStream) {
171 buf = compressor.finish(ctx.bufferAllocator());
172 return super.writeData(ctx, streamId, buf, padding,
173 true);
174 }
175
176 return ctx.newSucceededFuture();
177 }
178
179 Future<Void> future = super.writeData(ctx, streamId, buf, padding, false);
180 if (endOfStream) {
181 Promise<Void> promise = ctx.newPromise();
182 PromiseCombiner combiner = new PromiseCombiner(ctx.executor());
183 combiner.add(future);
184
185 buf = compressor.finish(ctx.bufferAllocator());
186
187
188 future = super.writeData(ctx, streamId, buf, 0, true);
189 combiner.add(future);
190 combiner.finish(promise);
191 return promise.asFuture();
192 }
193 return future;
194
195 } catch (Throwable cause) {
196 return ctx.newFailedFuture(cause);
197 } finally {
198 if (endOfStream) {
199 cleanup(stream, compressor);
200 }
201 }
202 }
203
204 @Override
205 public Future<Void> writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
206 boolean endStream) {
207 try {
208
209 Compressor compressor = newCompressor(ctx, headers, endStream);
210
211
212 Future<Void> future = super.writeHeaders(ctx, streamId, headers, padding, endStream);
213
214
215 bindCompressorToStream(compressor, streamId);
216
217 return future;
218 } catch (Throwable e) {
219 return ctx.newFailedFuture(e);
220 }
221 }
222
223 @Override
224 public Future<Void> writeHeaders(final ChannelHandlerContext ctx, final int streamId, final Http2Headers headers,
225 final int streamDependency, final short weight, final boolean exclusive, final int padding,
226 final boolean endOfStream) {
227 try {
228
229 Compressor compressor = newCompressor(ctx, headers, endOfStream);
230
231
232 Future<Void> future = super.writeHeaders(ctx, streamId, headers, streamDependency, weight, exclusive,
233 padding, endOfStream);
234
235
236 bindCompressorToStream(compressor, streamId);
237
238 return future;
239 } catch (Throwable e) {
240 return ctx.newFailedFuture(e);
241 }
242 }
243
244
245
246
247
248
249
250
251
252
253
254
255 protected Compressor newContentCompressor(ChannelHandlerContext ctx, CharSequence contentEncoding)
256 throws Http2Exception {
257 if (GZIP.contentEqualsIgnoreCase(contentEncoding) || X_GZIP.contentEqualsIgnoreCase(contentEncoding)) {
258 return newCompressionChannel(ctx, ZlibWrapper.GZIP);
259 }
260 if (DEFLATE.contentEqualsIgnoreCase(contentEncoding) || X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) {
261 return newCompressionChannel(ctx, ZlibWrapper.ZLIB);
262 }
263 if (Brotli.isAvailable() && brotliOptions != null && BR.contentEqualsIgnoreCase(contentEncoding)) {
264 return BrotliCompressor.newFactory(brotliOptions.parameters()).get();
265 }
266 if (zstdOptions != null && ZSTD.contentEqualsIgnoreCase(contentEncoding)) {
267 return ZstdCompressor.newFactory(zstdOptions.compressionLevel(),
268 zstdOptions.blockSize(), zstdOptions.maxEncodeSize()).get();
269 }
270
271 return null;
272 }
273
274
275
276
277
278
279
280
281
282 protected CharSequence getTargetContentEncoding(CharSequence contentEncoding) throws Http2Exception {
283 return contentEncoding;
284 }
285
286
287
288
289
290
291 private Compressor newCompressionChannel(final ChannelHandlerContext ctx, ZlibWrapper wrapper) {
292 if (supportsCompressionOptions) {
293 if (wrapper == ZlibWrapper.GZIP && gzipCompressionOptions != null) {
294 return ZlibCompressor.newFactory(wrapper, gzipCompressionOptions.compressionLevel()).get();
295 } else if (wrapper == ZlibWrapper.ZLIB && deflateOptions != null) {
296 return ZlibCompressor.newFactory(wrapper, deflateOptions.compressionLevel()).get();
297 } else {
298 throw new IllegalArgumentException("Unsupported ZlibWrapper: " + wrapper);
299 }
300 }
301 return ZlibCompressor.newFactory(wrapper, compressionLevel).get();
302 }
303
304
305
306
307
308
309
310
311
312
313
314 private Compressor newCompressor(ChannelHandlerContext ctx, Http2Headers headers, boolean endOfStream)
315 throws Http2Exception {
316 if (endOfStream) {
317 return null;
318 }
319
320 CharSequence encoding = headers.get(CONTENT_ENCODING);
321 if (encoding == null) {
322 encoding = IDENTITY;
323 }
324 final Compressor compressor = newContentCompressor(ctx, encoding);
325 if (compressor != null) {
326 CharSequence targetContentEncoding = getTargetContentEncoding(encoding);
327 if (IDENTITY.contentEqualsIgnoreCase(targetContentEncoding)) {
328 headers.remove(CONTENT_ENCODING);
329 } else {
330 headers.set(CONTENT_ENCODING, targetContentEncoding);
331 }
332
333
334
335
336 headers.remove(CONTENT_LENGTH);
337 }
338
339 return compressor;
340 }
341
342
343
344
345
346
347 private void bindCompressorToStream(Compressor compressor, int streamId) {
348 if (compressor != null) {
349 Http2Stream stream = connection().stream(streamId);
350 if (stream != null) {
351 stream.setProperty(propertyKey, compressor);
352 }
353 }
354 }
355
356
357
358
359
360
361
362 void cleanup(Http2Stream stream, Compressor compressor) {
363 compressor.close();
364 stream.removeProperty(propertyKey);
365 }
366 }