Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
import io.netty.handler.codec.compression.DecompressionException;
import io.netty.handler.codec.compression.JdkZlibDecoder;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
import io.netty.handler.codec.http2.Http2DataFrame;
import io.netty.handler.codec.http2.Http2HeadersFrame;

import java.util.Locale;
import io.netty.util.AsciiString;

/**
* HTTP/2 content decompressor that transparently decompresses gzip/deflate response bodies.
Expand Down Expand Up @@ -59,19 +60,20 @@ public Http2ContentDecompressor(boolean keepEncodingHeader, long maxDecompressed
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Http2HeadersFrame) {
Http2HeadersFrame headersFrame = (Http2HeadersFrame) msg;
CharSequence contentEncoding = headersFrame.headers().get("content-encoding");
CharSequence contentEncoding = headersFrame.headers().get(HttpHeaderNames.CONTENT_ENCODING);
if (contentEncoding != null) {
// Locale.ROOT: a default-locale toLowerCase() would mangle "GZIP" under the Turkish locale
// ("gzıp", dotless i), so the contains() checks below would miss it and the body would be
// forwarded still-compressed. Matches the toLowerCaseHeaderName idiom on the request path.
String enc = contentEncoding.toString().toLowerCase(Locale.ROOT);
if (enc.contains("gzip") || enc.contains("deflate")) {
ZlibWrapper wrapper = enc.contains("gzip") ? ZlibWrapper.GZIP : ZlibWrapper.ZLIB_OR_NONE;
// Case-insensitive substring match against the AsciiString header-value constants in place,
// instead of allocating contentEncoding.toString().toLowerCase() per response. AsciiString
// case-folding is ASCII-only, hence locale-independent, so it also avoids the Turkish-locale
// hazard where a default-locale toLowerCase() would mangle "GZIP" and miss the match.
boolean gzip = AsciiString.containsIgnoreCase(contentEncoding, HttpHeaderValues.GZIP);
if (gzip || AsciiString.containsIgnoreCase(contentEncoding, HttpHeaderValues.DEFLATE)) {
ZlibWrapper wrapper = gzip ? ZlibWrapper.GZIP : ZlibWrapper.ZLIB_OR_NONE;
decompressor = new EmbeddedChannel(false, new JdkZlibDecoder(wrapper));
if (!keepEncodingHeader) {
headersFrame.headers().remove("content-encoding");
headersFrame.headers().remove(HttpHeaderNames.CONTENT_ENCODING);
}
headersFrame.headers().remove("content-length");
headersFrame.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
}
}
ctx.fireChannelRead(msg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -60,6 +61,50 @@ private static ByteBuf gzip(String s) throws Exception {
return Unpooled.wrappedBuffer(gzipBytes(s));
}

// Returns true iff the decompressor installed a decoder for the given content-encoding, detected by
// content-length being stripped (the code removes it only when it activates). Guards that the switch to
// AsciiString.containsIgnoreCase preserves the old toLowerCase().contains() activation set exactly.
private static boolean activatesDecompression(String contentEncoding) {
DefaultHttp2Headers headers = new DefaultHttp2Headers();
headers.status("200");
if (contentEncoding != null) {
headers.set("content-encoding", contentEncoding);
}
headers.set("content-length", "123");
EmbeddedChannel ch = new EmbeddedChannel(new Http2ContentDecompressor(false));
try {
ch.writeInbound(new DefaultHttp2HeadersFrame(headers, false));
Http2HeadersFrame out = ch.readInbound();
boolean clRemoved = !out.headers().contains("content-length");
if (clRemoved) {
assertNull(out.headers().get("content-encoding"),
"content-encoding must be stripped when decompression activates and keepEncodingHeader=false");
}
return clRemoved;
} finally {
ch.finishAndReleaseAll();
}
}

@Test
public void activatesForGzipDeflateCaseInsensitivelyAndSubstrings() {
// Case-insensitive substring match, identical to the old contains() logic.
assertTrue(activatesDecompression("gzip"));
assertTrue(activatesDecompression("GZIP"));
assertTrue(activatesDecompression("x-gzip"));
assertTrue(activatesDecompression("deflate"));
assertTrue(activatesDecompression("DEFLATE"));
assertTrue(activatesDecompression("gzip, deflate"));
}

@Test
public void doesNotActivateForOtherOrAbsentEncodings() {
assertFalse(activatesDecompression(null), "no content-encoding -> no decompression");
assertFalse(activatesDecompression("br"), "brotli is not handled by this decompressor");
assertFalse(activatesDecompression("zstd"), "zstd is not handled by this decompressor");
assertFalse(activatesDecompression("identity"));
}

@Test
public void decompressesValidGzipBodyAndReleasesFrames() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new Http2ContentDecompressor(false));
Expand Down