From 41bfa5989f22795ff4b4c896cc584866464217ad Mon Sep 17 00:00:00 2001 From: baranowb Date: Wed, 10 Apr 2024 10:48:12 +0200 Subject: [PATCH] [UNDERTOW-2361] handle inflater wrapping properly in deflate encoding --- .../java/io/undertow/UndertowMessages.java | 5 ++ .../conduits/GzipStreamSourceConduit.java | 6 ++ .../InflatingStreamSourceConduit.java | 82 +++++++++++++++++-- .../RequestContentEncodingTestCase.java | 19 +++++ 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/io/undertow/UndertowMessages.java b/core/src/main/java/io/undertow/UndertowMessages.java index 553863570e..2a251b10a5 100644 --- a/core/src/main/java/io/undertow/UndertowMessages.java +++ b/core/src/main/java/io/undertow/UndertowMessages.java @@ -19,6 +19,7 @@ package io.undertow; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.file.Path; @@ -26,6 +27,7 @@ import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; +import io.undertow.server.HttpServerExchange; import io.undertow.server.RequestTooBigException; import io.undertow.server.handlers.form.MultiPartParserDefinition; import io.undertow.util.UrlDecodeException; @@ -650,4 +652,7 @@ public interface UndertowMessages { @Message(id = 209, value = "Protocol string was too large for the buffer. Either provide a smaller message or a bigger buffer. Protocol: %s") IllegalStateException protocolTooLargeForBuffer(String protocolString); + @Message(id = 210, value = "Buffer content underflow for exchange '%s', buffer '%s'") + IOException bufferUnderflow(final HttpServerExchange exchange,final ByteBuffer buf); + } diff --git a/core/src/main/java/io/undertow/conduits/GzipStreamSourceConduit.java b/core/src/main/java/io/undertow/conduits/GzipStreamSourceConduit.java index 12b15fe684..d428d66eb0 100644 --- a/core/src/main/java/io/undertow/conduits/GzipStreamSourceConduit.java +++ b/core/src/main/java/io/undertow/conduits/GzipStreamSourceConduit.java @@ -119,4 +119,10 @@ protected void dataDeflated(byte[] data, int off, int len) { totalOut += len; } + @Override + protected boolean isZlibHeaderPresent(ByteBuffer buf) { + //this will default to no wrapping object pool + return false; + } + } diff --git a/core/src/main/java/io/undertow/conduits/InflatingStreamSourceConduit.java b/core/src/main/java/io/undertow/conduits/InflatingStreamSourceConduit.java index f228e12a73..3ad7cb8033 100644 --- a/core/src/main/java/io/undertow/conduits/InflatingStreamSourceConduit.java +++ b/core/src/main/java/io/undertow/conduits/InflatingStreamSourceConduit.java @@ -32,6 +32,7 @@ import org.xnio.conduits.ConduitReadableByteChannel; import org.xnio.conduits.StreamSourceConduit; import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; import io.undertow.connector.PooledByteBuffer; import io.undertow.server.ConduitWrapper; import io.undertow.server.HttpServerExchange; @@ -54,8 +55,10 @@ public StreamSourceConduit wrap(ConduitFactory factory, Htt }; private volatile Inflater inflater; + private volatile PooledObject activePooledObject; - private final PooledObject pooledObject; + private final ObjectPool objectPoolNonWrapping; + private final ObjectPool objectPoolWrapping; private final HttpServerExchange exchange; private PooledByteBuffer compressed; private PooledByteBuffer uncompressed; @@ -63,7 +66,7 @@ public StreamSourceConduit wrap(ConduitFactory factory, Htt private boolean headerDone = false; public InflatingStreamSourceConduit(HttpServerExchange exchange, StreamSourceConduit next) { - this(exchange, next, newInstanceInflaterPool()); + this(exchange, next, newInstanceInflaterPool(), newInstanceWrappingInflaterPool()); } public InflatingStreamSourceConduit( @@ -72,18 +75,52 @@ public InflatingStreamSourceConduit( ObjectPool inflaterPool) { super(next); this.exchange = exchange; - this.pooledObject = inflaterPool.allocate(); - this.inflater = pooledObject.getObject(); + this.objectPoolNonWrapping = inflaterPool; + this.objectPoolWrapping = null; } + public InflatingStreamSourceConduit( + HttpServerExchange exchange, + StreamSourceConduit next, + ObjectPool inflaterPool, + ObjectPool inflaterWrappingPool) { + super(next); + this.exchange = exchange; + this.objectPoolNonWrapping = inflaterPool; + this.objectPoolWrapping = inflaterWrappingPool; + } + /** + * Create non-wrapping(gzip/zlib without headers) inflater pool + * @return + */ public static ObjectPool newInstanceInflaterPool() { return new NewInstanceObjectPool<>(() -> new Inflater(true), Inflater::end); } + /** + * Create non-wrapping(gzip/zlib without headers) inflater pool + * @return + */ public static ObjectPool simpleInflaterPool(int poolSize) { return new SimpleObjectPool<>(poolSize, () -> new Inflater(true), Inflater::reset, Inflater::end); } + /** + * Create wrapping inflater pool, one that expects headers. + * @return + */ + public static ObjectPool newInstanceWrappingInflaterPool(){ + return new NewInstanceObjectPool<>(() -> new Inflater(false), Inflater::end); + } + + /** + * Create wrapping inflater pool, one that expects headers. + * @return + */ + public static ObjectPool simpleWrappingInflaterPool(int poolSize) { + return new SimpleObjectPool<>(poolSize, () -> new Inflater(false), Inflater::reset, Inflater::end); + } + @Override public int read(ByteBuffer dst) throws IOException { if (isReadShutdown()) { @@ -115,6 +152,8 @@ public int read(ByteBuffer dst) throws IOException { if (!headerDone) { headerDone = readHeader(buf); } + + initializeInflater(buf); inflater.setInput(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); } } @@ -171,6 +210,38 @@ public int read(ByteBuffer dst) throws IOException { } } + protected void initializeInflater(ByteBuffer buf) throws IOException { + if(isZlibHeaderPresent(buf)) { + this.activePooledObject = this.objectPoolWrapping.allocate(); + } else { + this.activePooledObject = this.objectPoolNonWrapping.allocate(); + } + this.inflater = this.activePooledObject.getObject(); + } + + protected boolean isZlibHeaderPresent(final ByteBuffer buf) throws IOException { + if(buf.remaining()<2) { + throw UndertowMessages.MESSAGES.bufferUnderflow(this.exchange, buf); + } + // https://www.ietf.org/rfc/rfc1950.txt - 2.2. - Data format, two bytes. Below is sort of a cheat, we have so much power + //to quickly compress to best cap. + // FLEVEL: 0 1 2 3 + // CINFO: + // 0 08 1D 08 5B 08 99 08 D7 + // 1 18 19 18 57 18 95 18 D3 + // 2 28 15 28 53 28 91 28 CF + // 3 38 11 38 4F 38 8D 38 CB + // 4 48 0D 48 4B 48 89 48 C7 + // 5 58 09 58 47 58 85 58 C3 + // 6 68 05 68 43 68 81 68 DE + // 7 78 01 78 5E 78 9C 78 DA + buf.mark(); + final char cmf = (char)(buf.get() & 0xFF); + final char flg = (char)(buf.get() & 0xFF); + buf.reset(); + return (cmf == 0x78 && (flg == 0x01 || flg == 0x5E || flg == 0x9c || flg == 0xDA)); + } + protected void readFooter(ByteBuffer buf) throws IOException { } @@ -191,7 +262,8 @@ private void done() { uncompressed.close(); } if (inflater != null) { - pooledObject.close(); + activePooledObject.close(); + activePooledObject = null; inflater = null; } } diff --git a/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase.java b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase.java index d986dc09c7..4beb3c99f2 100644 --- a/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase.java +++ b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase.java @@ -117,6 +117,25 @@ public void testGzipEncoding() throws IOException { runTest(sb.toString(), "gzip"); } + private static final String MESSAGE = "COMPRESSED I'AM"; + private static final byte[] COMPRESSED_MESSAGE = { 0x78, (byte) (0x9C & 0xFF), 0x73, (byte) (0xF6 & 0xFF), + (byte) (0xF7 & 0xFF), 0x0D, 0x08, 0x72, 0x0D, 0x0E, 0x76, 0x75, 0x51, (byte) (0xF0 & 0xFF), 0x54, 0x77, + (byte) (0xF4 & 0xFF), 0x05, 0x00, 0x22, 0x35, 0x04, 0x14 }; + + @Test + public void testDeflateWithNoWrapping() throws IOException { + HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/decode"); + post.setEntity(new ByteArrayEntity(COMPRESSED_MESSAGE)); + post.addHeader(Headers.CONTENT_ENCODING_STRING, "deflate"); + + try (CloseableHttpClient client = HttpClientBuilder.create().disableContentCompression().build()) { + HttpResponse result = client.execute(post); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + String sb = HttpClientUtils.readResponse(result); + Assert.assertEquals(MESSAGE.length(), sb.length()); + Assert.assertEquals(MESSAGE, sb); + } + } public void runTest(final String theMessage, String encoding) throws IOException { try (CloseableHttpClient client = HttpClientBuilder.create().disableContentCompression().build()){