From 3eabc54b0b28d145c2478bcc47c6f9619fe0d755 Mon Sep 17 00:00:00 2001 From: baranowb Date: Fri, 9 Feb 2024 11:49:25 +0100 Subject: [PATCH] [UNDERTOW-2340] Remove Content-Length header when request is deflated. --- .../io/undertow/io/AsyncReceiverImpl.java | 38 ++++- .../io/undertow/io/BlockingReceiverImpl.java | 27 ++- .../encoding/RequestEncodingHandler.java | 5 + .../main/java/io/undertow/util/Headers.java | 21 +-- .../RequestContentEncodingTestCase.java | 10 +- .../RequestContentEncodingTestCase2.java | 142 ++++++++++++++++ .../RequestContentEncodingTestCase3.java | 159 +++++++++++++++++ .../RequestContentEncodingTestCase4.java | 160 ++++++++++++++++++ 8 files changed, 540 insertions(+), 22 deletions(-) create mode 100644 core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase2.java create mode 100644 core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase3.java create mode 100644 core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase4.java diff --git a/core/src/main/java/io/undertow/io/AsyncReceiverImpl.java b/core/src/main/java/io/undertow/io/AsyncReceiverImpl.java index df12674db0..0dfc64114c 100644 --- a/core/src/main/java/io/undertow/io/AsyncReceiverImpl.java +++ b/core/src/main/java/io/undertow/io/AsyncReceiverImpl.java @@ -24,6 +24,7 @@ import io.undertow.server.Connectors; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; import io.undertow.util.Headers; import io.undertow.util.StatusCodes; import org.xnio.ChannelListener; @@ -108,6 +109,9 @@ public void receiveFullString(final FullStringCallback callback, final ErrorCall return; } String contentLengthString = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(contentLengthString == null) { + contentLengthString = exchange.getRequestHeaders().getFirst(Headers.X_CONTENT_LENGTH); + } long contentLength; final ByteArrayOutputStream sb; if (contentLengthString != null) { @@ -137,7 +141,12 @@ public void receiveFullString(final FullStringCallback callback, final ErrorCall res = channel.read(buffer); if (res == -1) { done = true; - callback.handle(exchange, sb.toString(charset.name())); + final String message = sb.toString(charset.name()); + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + if(requestHeaders.contains(Headers.X_CONTENT_LENGTH) && !requestHeaders.contains(Headers.CONTENT_LENGTH)) { + requestHeaders.put(Headers.CONTENT_LENGTH, message.length()); + } + callback.handle(exchange, message); return; } else if (res == 0) { channel.getReadSetter().set(new ChannelListener() { @@ -159,7 +168,12 @@ public void handleEvent(StreamSourceChannel channel) { Connectors.executeRootHandler(new HttpHandler() { @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - callback.handle(exchange, sb.toString(charset.name())); + final String message = sb.toString(charset.name()); + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + if(requestHeaders.contains(Headers.X_CONTENT_LENGTH) && !requestHeaders.contains(Headers.CONTENT_LENGTH)) { + requestHeaders.put(Headers.CONTENT_LENGTH, message.length()); + } + callback.handle(exchange, message); } }, exchange); return; @@ -238,6 +252,9 @@ public void receivePartialString(final PartialStringCallback callback, final Err return; } String contentLengthString = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(contentLengthString == null) { + contentLengthString = exchange.getRequestHeaders().getFirst(Headers.X_CONTENT_LENGTH); + } long contentLength; if (contentLengthString != null) { contentLength = Long.parseLong(contentLengthString); @@ -396,7 +413,12 @@ public void receiveFullBytes(final FullBytesCallback callback, final ErrorCallba res = channel.read(buffer); if (res == -1) { done = true; - callback.handle(exchange, sb.toByteArray()); + final byte[] message = sb.toByteArray(); + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + if(requestHeaders.contains(Headers.X_CONTENT_LENGTH) && !requestHeaders.contains(Headers.CONTENT_LENGTH)) { + requestHeaders.put(Headers.CONTENT_LENGTH, message.length); + } + callback.handle(exchange, message); return; } else if (res == 0) { channel.getReadSetter().set(new ChannelListener() { @@ -418,7 +440,12 @@ public void handleEvent(StreamSourceChannel channel) { Connectors.executeRootHandler(new HttpHandler() { @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - callback.handle(exchange, sb.toByteArray()); + final byte[] message = sb.toByteArray(); + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + if(requestHeaders.contains(Headers.X_CONTENT_LENGTH) && !requestHeaders.contains(Headers.CONTENT_LENGTH)) { + requestHeaders.put(Headers.CONTENT_LENGTH, message.length); + } + callback.handle(exchange, message); } }, exchange); return; @@ -495,6 +522,9 @@ public void receivePartialBytes(final PartialBytesCallback callback, final Error return; } String contentLengthString = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(contentLengthString == null) { + contentLengthString = exchange.getRequestHeaders().getFirst(Headers.X_CONTENT_LENGTH); + } long contentLength; if (contentLengthString != null) { contentLength = Long.parseLong(contentLengthString); diff --git a/core/src/main/java/io/undertow/io/BlockingReceiverImpl.java b/core/src/main/java/io/undertow/io/BlockingReceiverImpl.java index 05acf1c2c5..d0c711d318 100644 --- a/core/src/main/java/io/undertow/io/BlockingReceiverImpl.java +++ b/core/src/main/java/io/undertow/io/BlockingReceiverImpl.java @@ -30,6 +30,7 @@ import io.undertow.UndertowMessages; import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; import io.undertow.util.Headers; import io.undertow.util.StatusCodes; @@ -100,6 +101,9 @@ public void receiveFullString(final FullStringCallback callback, final ErrorCall return; } String contentLengthString = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(contentLengthString == null) { + contentLengthString = exchange.getRequestHeaders().getFirst(Headers.X_CONTENT_LENGTH); + } long contentLength; final ByteArrayOutputStream sb; if (contentLengthString != null) { @@ -125,7 +129,12 @@ public void receiveFullString(final FullStringCallback callback, final ErrorCall while ((s = inputStream.read(pooled.getBuffer().array(), pooled.getBuffer().arrayOffset(), pooled.getBuffer().remaining())) > 0) { sb.write(pooled.getBuffer().array(), pooled.getBuffer().arrayOffset(), s); } - callback.handle(exchange, sb.toString(charset.name())); + final String message = sb.toString(charset.name()); + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + if(requestHeaders.contains(Headers.X_CONTENT_LENGTH) && !requestHeaders.contains(Headers.CONTENT_LENGTH)) { + requestHeaders.put(Headers.CONTENT_LENGTH, message.length()); + } + callback.handle(exchange, message); } else { throw UndertowMessages.MESSAGES.failedToAllocateResource(); } @@ -154,6 +163,9 @@ public void receivePartialString(final PartialStringCallback callback, final Err return; } String contentLengthString = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(contentLengthString == null) { + contentLengthString = exchange.getRequestHeaders().getFirst(Headers.X_CONTENT_LENGTH); + } long contentLength; if (contentLengthString != null) { contentLength = Long.parseLong(contentLengthString); @@ -209,6 +221,9 @@ public void receiveFullBytes(final FullBytesCallback callback, final ErrorCallba return; } String contentLengthString = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(contentLengthString == null) { + contentLengthString = exchange.getRequestHeaders().getFirst(Headers.X_CONTENT_LENGTH); + } long contentLength; final ByteArrayOutputStream sb; if (contentLengthString != null) { @@ -234,7 +249,12 @@ public void receiveFullBytes(final FullBytesCallback callback, final ErrorCallba while ((s = inputStream.read(pooled.getBuffer().array(), pooled.getBuffer().arrayOffset(), pooled.getBuffer().remaining())) > 0) { sb.write(pooled.getBuffer().array(), pooled.getBuffer().arrayOffset(), s); } - callback.handle(exchange, sb.toByteArray()); + final byte[] message = sb.toByteArray(); + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + if(requestHeaders.contains(Headers.X_CONTENT_LENGTH) && !requestHeaders.contains(Headers.CONTENT_LENGTH)) { + requestHeaders.put(Headers.CONTENT_LENGTH, message.length); + } + callback.handle(exchange, message); } else { throw UndertowMessages.MESSAGES.failedToAllocateResource(); } @@ -263,6 +283,9 @@ public void receivePartialBytes(final PartialBytesCallback callback, final Error return; } String contentLengthString = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(contentLengthString == null) { + contentLengthString = exchange.getRequestHeaders().getFirst(Headers.X_CONTENT_LENGTH); + } long contentLength; if (contentLengthString != null) { contentLength = Long.parseLong(contentLengthString); diff --git a/core/src/main/java/io/undertow/server/handlers/encoding/RequestEncodingHandler.java b/core/src/main/java/io/undertow/server/handlers/encoding/RequestEncodingHandler.java index 3d1a3c34e4..b92735e3cf 100644 --- a/core/src/main/java/io/undertow/server/handlers/encoding/RequestEncodingHandler.java +++ b/core/src/main/java/io/undertow/server/handlers/encoding/RequestEncodingHandler.java @@ -61,6 +61,11 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { // Nested handlers or even servlet filters may implement logic to decode encoded request data. // Since the data is no longer encoded, we remove the encoding header. exchange.getRequestHeaders().remove(Headers.CONTENT_ENCODING); + final String encodedContentLength = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH); + if(encodedContentLength != null) { + exchange.getRequestHeaders().remove(Headers.CONTENT_LENGTH); + exchange.getRequestHeaders().put(Headers.X_CONTENT_LENGTH, encodedContentLength); + } } next.handleRequest(exchange); } diff --git a/core/src/main/java/io/undertow/util/Headers.java b/core/src/main/java/io/undertow/util/Headers.java index bae0cb64aa..7ae7bd9f9d 100644 --- a/core/src/main/java/io/undertow/util/Headers.java +++ b/core/src/main/java/io/undertow/util/Headers.java @@ -114,6 +114,7 @@ private Headers() { public static final String VIA_STRING = "Via"; public static final String WARNING_STRING = "Warning"; public static final String WWW_AUTHENTICATE_STRING = "WWW-Authenticate"; + public static final String X_CONTENT_LENGTH_STRING = "X-Content-Length"; public static final String X_CONTENT_TYPE_OPTIONS_STRING = "X-Content-Type-Options"; public static final String X_DISABLE_PUSH_STRING = "X-Disable-Push"; public static final String X_FORWARDED_FOR_STRING = "X-Forwarded-For"; @@ -123,7 +124,6 @@ private Headers() { public static final String X_FORWARDED_SERVER_STRING = "X-Forwarded-Server"; public static final String X_FRAME_OPTIONS_STRING = "X-Frame-Options"; public static final String X_XSS_PROTECTION_STRING = "X-Xss-Protection"; - // Header names public static final HttpString ACCEPT = new HttpString(ACCEPT_STRING, 1); @@ -200,15 +200,16 @@ private Headers() { public static final HttpString VIA = new HttpString(VIA_STRING, 72); public static final HttpString WARNING = new HttpString(WARNING_STRING, 73); public static final HttpString WWW_AUTHENTICATE = new HttpString(WWW_AUTHENTICATE_STRING, 74); - public static final HttpString X_CONTENT_TYPE_OPTIONS = new HttpString(X_CONTENT_TYPE_OPTIONS_STRING, 75); - public static final HttpString X_DISABLE_PUSH = new HttpString(X_DISABLE_PUSH_STRING, 76); - public static final HttpString X_FORWARDED_FOR = new HttpString(X_FORWARDED_FOR_STRING, 77); - public static final HttpString X_FORWARDED_HOST = new HttpString(X_FORWARDED_HOST_STRING, 78); - public static final HttpString X_FORWARDED_PORT = new HttpString(X_FORWARDED_PORT_STRING, 79); - public static final HttpString X_FORWARDED_PROTO = new HttpString(X_FORWARDED_PROTO_STRING, 80); - public static final HttpString X_FORWARDED_SERVER = new HttpString(X_FORWARDED_SERVER_STRING, 81); - public static final HttpString X_FRAME_OPTIONS = new HttpString(X_FRAME_OPTIONS_STRING, 82); - public static final HttpString X_XSS_PROTECTION = new HttpString(X_XSS_PROTECTION_STRING, 83); + public static final HttpString X_CONTENT_LENGTH = new HttpString(X_CONTENT_LENGTH_STRING, 75); + public static final HttpString X_CONTENT_TYPE_OPTIONS = new HttpString(X_CONTENT_TYPE_OPTIONS_STRING, 76); + public static final HttpString X_DISABLE_PUSH = new HttpString(X_DISABLE_PUSH_STRING, 77); + public static final HttpString X_FORWARDED_FOR = new HttpString(X_FORWARDED_FOR_STRING, 78); + public static final HttpString X_FORWARDED_HOST = new HttpString(X_FORWARDED_HOST_STRING, 79); + public static final HttpString X_FORWARDED_PORT = new HttpString(X_FORWARDED_PORT_STRING, 80); + public static final HttpString X_FORWARDED_PROTO = new HttpString(X_FORWARDED_PROTO_STRING, 81); + public static final HttpString X_FORWARDED_SERVER = new HttpString(X_FORWARDED_SERVER_STRING, 82); + public static final HttpString X_FRAME_OPTIONS = new HttpString(X_FRAME_OPTIONS_STRING, 83); + public static final HttpString X_XSS_PROTECTION = new HttpString(X_XSS_PROTECTION_STRING, 84); // Content codings public static final HttpString COMPRESS = new HttpString("compress"); 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 4beb3c99f2..4c7bf54a43 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 @@ -67,7 +67,6 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { exchange.getResponseSender().send(message, IoCallback.END_EXCHANGE); } }); - final EncodingHandler wrappedEncode = new EncodingHandler(contentEncodingRepository).setNext(encode); final HttpHandler decode = new RequestEncodingHandler(new HttpHandler() { @Override @@ -75,18 +74,17 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { exchange.getRequestReceiver().receiveFullBytes(new Receiver.FullBytesCallback() { @Override public void handle(HttpServerExchange exchange, byte[] message) { + Assert.assertTrue(exchange.getRequestContentLength()>0); exchange.getResponseSender().send(ByteBuffer.wrap(message)); } }); } }).addEncoding("deflate", InflatingStreamSourceConduit.WRAPPER) .addEncoding("gzip", GzipStreamSourceConduit.WRAPPER); - final HttpHandler wrappedDecode = new RequestEncodingHandler(decode) - .addEncoding("deflate", InflatingStreamSourceConduit.WRAPPER) - .addEncoding("gzip", GzipStreamSourceConduit.WRAPPER); + PathHandler pathHandler = new PathHandler(); - pathHandler.addPrefixPath("/encode", wrappedEncode); - pathHandler.addPrefixPath("/decode", wrappedDecode); + pathHandler.addPrefixPath("/encode", encode); + pathHandler.addPrefixPath("/decode", decode); DefaultServer.setRootHandler(pathHandler); } diff --git a/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase2.java b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase2.java new file mode 100644 index 0000000000..b400c498e2 --- /dev/null +++ b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase2.java @@ -0,0 +1,142 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2024 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.server.handlers.encoding; + +import java.io.IOException; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.undertow.conduits.GzipStreamSourceConduit; +import io.undertow.conduits.InflatingStreamSourceConduit; +import io.undertow.io.IoCallback; +import io.undertow.io.Receiver; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.PathHandler; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpClientUtils; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; + +/** + * This is not part of the HTTP spec + * + * @author Stuart Douglas + */ +@RunWith(DefaultServer.class) +public class RequestContentEncodingTestCase2 { + + private static volatile String message; + + @BeforeClass + public static void setup() { + final ContentEncodingRepository contentEncodingRepository = new ContentEncodingRepository() + .addEncodingHandler("deflate", new DeflateEncodingProvider(), 50) + .addEncodingHandler("gzip", new GzipEncodingProvider(), 60); + final EncodingHandler encode = new EncodingHandler(contentEncodingRepository) + .setNext(new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, message.length() + ""); + exchange.getResponseSender().send(message, IoCallback.END_EXCHANGE); + } + }); + + final HttpHandler decode = new RequestEncodingHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getRequestReceiver().receiveFullString(new Receiver.FullStringCallback() { + @Override + public void handle(HttpServerExchange exchange, String message) { + Assert.assertTrue(exchange.getRequestContentLength()>0); + exchange.getResponseSender().send(message); + } + }); + } + }).addEncoding("deflate", InflatingStreamSourceConduit.WRAPPER) + .addEncoding("gzip", GzipStreamSourceConduit.WRAPPER); + + PathHandler pathHandler = new PathHandler(); + pathHandler.addPrefixPath("/encode", encode); + pathHandler.addPrefixPath("/decode", decode); + + DefaultServer.setRootHandler(pathHandler); + } + + /** + * Tests the use of the deflate contentent encoding + * + * @throws IOException + */ + @Test + public void testDeflateEncoding() throws IOException { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; ++i) { + sb.append("a message"); + } + runTest(sb.toString(), "deflate"); + runTest("Hello World", "deflate"); + + } + + @Test + public void testGzipEncoding() throws IOException { + runTest("Hello World", "gzip"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; ++i) { + sb.append("a message"); + } + runTest(sb.toString(), "gzip"); + } + + + public void runTest(final String theMessage, String encoding) throws IOException { + try (CloseableHttpClient client = HttpClientBuilder.create().disableContentCompression().build()){ + message = theMessage; + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/encode"); + get.setHeader(Headers.ACCEPT_ENCODING_STRING, encoding); + HttpResponse result = client.execute(get); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + Header[] header = result.getHeaders(Headers.CONTENT_ENCODING_STRING); + Assert.assertEquals(encoding, header[0].getValue()); + byte[] body = HttpClientUtils.readRawResponse(result); + + HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/decode"); + post.setEntity(new ByteArrayEntity(body)); + post.addHeader(Headers.CONTENT_ENCODING_STRING, encoding); + + result = client.execute(post); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + String sb = HttpClientUtils.readResponse(result); + Assert.assertEquals(theMessage.length(), sb.length()); + Assert.assertEquals(theMessage, sb); + + } + } +} diff --git a/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase3.java b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase3.java new file mode 100644 index 0000000000..623ba11c6a --- /dev/null +++ b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase3.java @@ -0,0 +1,159 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2024 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.server.handlers.encoding; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import io.undertow.conduits.GzipStreamSourceConduit; +import io.undertow.conduits.InflatingStreamSourceConduit; +import io.undertow.io.IoCallback; +import io.undertow.io.Receiver; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.PathHandler; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpClientUtils; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; + +/** + * This is not part of the HTTP spec + * + * @author Stuart Douglas + */ +@RunWith(DefaultServer.class) +public class RequestContentEncodingTestCase3 { + + private static volatile String message; + private static ExecutorService executor; + + @BeforeClass + public static void setup() { + executor = Executors.newFixedThreadPool(2); + final ContentEncodingRepository contentEncodingRepository = new ContentEncodingRepository() + .addEncodingHandler("deflate", new DeflateEncodingProvider(), 50) + .addEncodingHandler("gzip", new GzipEncodingProvider(), 60); + final EncodingHandler encode = new EncodingHandler(contentEncodingRepository) + .setNext(new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, message.length() + ""); + exchange.getResponseSender().send(message, IoCallback.END_EXCHANGE); + } + }); + + final HttpHandler decode = new RequestEncodingHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.startBlocking(); + Future result = executor.submit(new Runnable() { + @Override + public void run() { + exchange.getRequestReceiver().receiveFullBytes(new Receiver.FullBytesCallback() { + @Override + public void handle(HttpServerExchange exchange, byte[] message) { + Assert.assertTrue(exchange.getRequestContentLength()>0); + exchange.getResponseSender().send(ByteBuffer.wrap(message)); + } + }); + } + }); + result.get(); + } + }).addEncoding("deflate", InflatingStreamSourceConduit.WRAPPER) + .addEncoding("gzip", GzipStreamSourceConduit.WRAPPER); + + PathHandler pathHandler = new PathHandler(); + pathHandler.addPrefixPath("/encode", encode); + pathHandler.addPrefixPath("/decode", decode); + + DefaultServer.setRootHandler(pathHandler); + } + + @AfterClass + public static void boom() { + executor.shutdownNow(); + } + /** + * Tests the use of the deflate contentent encoding + * + * @throws IOException + */ + @Test + public void testDeflateEncoding() throws IOException { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; ++i) { + sb.append("a message"); + } + runTest(sb.toString(), "deflate"); + runTest("Hello World", "deflate"); + + } + + @Test + public void testGzipEncoding() throws IOException { + runTest("Hello World", "gzip"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; ++i) { + sb.append("a message"); + } + runTest(sb.toString(), "gzip"); + } + + + public void runTest(final String theMessage, String encoding) throws IOException { + try (CloseableHttpClient client = HttpClientBuilder.create().disableContentCompression().build()){ + message = theMessage; + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/encode"); + get.setHeader(Headers.ACCEPT_ENCODING_STRING, encoding); + HttpResponse result = client.execute(get); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + Header[] header = result.getHeaders(Headers.CONTENT_ENCODING_STRING); + Assert.assertEquals(encoding, header[0].getValue()); + byte[] body = HttpClientUtils.readRawResponse(result); + + HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/decode"); + post.setEntity(new ByteArrayEntity(body)); + post.addHeader(Headers.CONTENT_ENCODING_STRING, encoding); + + result = client.execute(post); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + String sb = HttpClientUtils.readResponse(result); + Assert.assertEquals(theMessage.length(), sb.length()); + Assert.assertEquals(theMessage, sb); + + } + } +} diff --git a/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase4.java b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase4.java new file mode 100644 index 0000000000..8dafa77913 --- /dev/null +++ b/core/src/test/java/io/undertow/server/handlers/encoding/RequestContentEncodingTestCase4.java @@ -0,0 +1,160 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2024 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.server.handlers.encoding; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.undertow.conduits.GzipStreamSourceConduit; +import io.undertow.conduits.InflatingStreamSourceConduit; +import io.undertow.io.IoCallback; +import io.undertow.io.Receiver; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.PathHandler; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpClientUtils; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; + +/** + * This is not part of the HTTP spec + * + * @author Stuart Douglas + */ +@RunWith(DefaultServer.class) +public class RequestContentEncodingTestCase4 { + + private static volatile String message; + private static ExecutorService executor; + + @BeforeClass + public static void setup() { + executor = Executors.newFixedThreadPool(2); + final ContentEncodingRepository contentEncodingRepository = new ContentEncodingRepository() + .addEncodingHandler("deflate", new DeflateEncodingProvider(), 50) + .addEncodingHandler("gzip", new GzipEncodingProvider(), 60); + final EncodingHandler encode = new EncodingHandler(contentEncodingRepository) + .setNext(new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, message.length() + ""); + exchange.getResponseSender().send(message, IoCallback.END_EXCHANGE); + } + }); + + final HttpHandler decode = new RequestEncodingHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.startBlocking(); + Future result = executor.submit(new Runnable() { + @Override + public void run() { + exchange.getRequestReceiver().receiveFullString(new Receiver.FullStringCallback() { + @Override + public void handle(HttpServerExchange exchange, String message) { + Assert.assertTrue(exchange.getRequestContentLength()>0); + exchange.getResponseSender().send(message); + } + }); + } + }); + result.get(); + } + }).addEncoding("deflate", InflatingStreamSourceConduit.WRAPPER) + .addEncoding("gzip", GzipStreamSourceConduit.WRAPPER); + + PathHandler pathHandler = new PathHandler(); + pathHandler.addPrefixPath("/encode", encode); + pathHandler.addPrefixPath("/decode", decode); + + DefaultServer.setRootHandler(pathHandler); + } + + @AfterClass + public static void boom() { + executor.shutdownNow(); + } + + /** + * Tests the use of the deflate contentent encoding + * + * @throws IOException + */ + @Test + public void testDeflateEncoding() throws IOException { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; ++i) { + sb.append("a message"); + } + runTest(sb.toString(), "deflate"); + runTest("Hello World", "deflate"); + + } + + @Test + public void testGzipEncoding() throws IOException { + runTest("Hello World", "gzip"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; ++i) { + sb.append("a message"); + } + runTest(sb.toString(), "gzip"); + } + + + public void runTest(final String theMessage, String encoding) throws IOException { + try (CloseableHttpClient client = HttpClientBuilder.create().disableContentCompression().build()){ + message = theMessage; + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/encode"); + get.setHeader(Headers.ACCEPT_ENCODING_STRING, encoding); + HttpResponse result = client.execute(get); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + Header[] header = result.getHeaders(Headers.CONTENT_ENCODING_STRING); + Assert.assertEquals(encoding, header[0].getValue()); + byte[] body = HttpClientUtils.readRawResponse(result); + + HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/decode"); + post.setEntity(new ByteArrayEntity(body)); + post.addHeader(Headers.CONTENT_ENCODING_STRING, encoding); + + result = client.execute(post); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + String sb = HttpClientUtils.readResponse(result); + Assert.assertEquals(theMessage.length(), sb.length()); + Assert.assertEquals(theMessage, sb); + + } + } +}