diff --git a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java index b118c708ad3..9a122e75125 100644 --- a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java +++ b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java @@ -113,6 +113,12 @@ public class WebServerDTO extends ComponentDTO { @XmlAttribute public Integer maxResponseHeaderSize; + @XmlAttribute + public Boolean compressionEnabled; + + @XmlAttribute + public Integer compressionLevel; + public String getPath() { return path; } diff --git a/artemis-web/pom.xml b/artemis-web/pom.xml index 7459b950466..2f887519b97 100644 --- a/artemis-web/pom.xml +++ b/artemis-web/pom.xml @@ -102,6 +102,14 @@ org.eclipse.jetty jetty-alpn-java-server + + org.eclipse.jetty.compression + jetty-compression-server + + + org.eclipse.jetty.compression + jetty-compression-gzip + org.apache.artemis artemis-server diff --git a/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java b/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java index 316b0e31205..c5af6026bd8 100644 --- a/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java +++ b/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java @@ -57,6 +57,11 @@ import org.apache.activemq.artemis.utils.PemConfigUtil; import org.apache.activemq.artemis.utils.sm.SecurityManagerShim; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.compression.Compression; +import org.eclipse.jetty.compression.EncoderConfig; +import org.eclipse.jetty.compression.gzip.GzipCompression; +import org.eclipse.jetty.compression.gzip.GzipEncoderConfig; +import org.eclipse.jetty.compression.server.CompressionHandler; import org.eclipse.jetty.ee9.security.DefaultAuthenticatorFactory; import org.eclipse.jetty.ee9.servlet.FilterHolder; import org.eclipse.jetty.ee9.webapp.WebAppContext; @@ -149,6 +154,21 @@ public synchronized void start() throws Exception { Scheduler scheduler = new ScheduledExecutorScheduler("activemq-web-scheduled", false); server = new Server(threadPool, scheduler, null); handlers = new Handler.Sequence(); + if (webServerConfig.compressionEnabled != null && webServerConfig.compressionEnabled) { + int compressionLevel = Objects.requireNonNullElse(webServerConfig.compressionLevel, 6); + logger.debug("embedded web server is using GZIP compression level {}", compressionLevel); + EncoderConfig encoderConfig = new GzipEncoderConfig(); + encoderConfig.setCompressionLevel(compressionLevel); + Compression compression = new GzipCompression(); + compression.setDefaultEncoderConfig(encoderConfig); + CompressionHandler compressionHandler = new CompressionHandler(); + compressionHandler.putCompression(compression); + compressionHandler.setHandler(handlers); + server.setHandler(compressionHandler); + } else { + server.setHandler(handlers); + } + HttpConfiguration httpConfiguration = new HttpConfiguration(); @@ -253,8 +273,6 @@ public void requestDestroyed(ServletRequestEvent sre) { handlers.addHandler(defaultHandler); // this should be last - server.setHandler(handlers); - server.start(); printStatus(bindings); diff --git a/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java b/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java index 1f56dad6bdb..a6134483cee 100644 --- a/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java +++ b/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java @@ -65,6 +65,7 @@ import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; @@ -202,6 +203,62 @@ private void internalSimpleServer(boolean useCustomizer) throws Exception { assertFalse(webServerComponent.isStarted()); } + @Test + public void testCompressionEnabled() throws Exception { + testCompression(true); + } + + @Test + public void testCompressionDisabled() throws Exception { + testCompression(false); + } + + private void testCompression(boolean compressionEnabled) throws Exception { + final String encoding = "gzip"; + + BindingDTO bindingDTO = new BindingDTO(); + bindingDTO.uri = "http://localhost:0"; + WebServerDTO webServerDTO = new WebServerDTO(); + webServerDTO.setBindings(Collections.singletonList(bindingDTO)); + webServerDTO.path = "webapps"; + webServerDTO.webContentEnabled = true; + webServerDTO.compressionEnabled = compressionEnabled; + WebServerComponent webServerComponent = new WebServerComponent(); + assertFalse(webServerComponent.isStarted()); + webServerComponent.configure(webServerDTO, "src/test/resources/", "src/test/resources/"); + testedComponents.add(webServerComponent); + webServerComponent.start(); + final int port = webServerComponent.getPort(); + // Make the connection attempt. + CountDownLatch latch = new CountDownLatch(1); + final ClientHandler clientHandler = new ClientHandler(latch); + Channel ch = getChannel(port, clientHandler); + + // this file is different from the other tests because it has to be above a certain size in order to be compressed + URI uri = new URI("http://localhost/WebServerComponentCompressionTest.txt"); + // Prepare the HTTP request. + HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath()); + request.headers().set(HttpHeaderNames.HOST, "localhost"); + request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, encoding); + + // Send the HTTP request. + ch.writeAndFlush(request); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + String contentEncoding = clientHandler.headers.get(HttpHeaderNames.CONTENT_ENCODING); + if (compressionEnabled) { + assertEquals(encoding, contentEncoding); + } else { + assertNull(contentEncoding); + } + + // Wait for the server to close the connection. + ch.close(); + ch.eventLoop().shutdownNow(); + assertTrue(webServerComponent.isStarted()); + webServerComponent.stop(true); + assertFalse(webServerComponent.isStarted()); + } + @Test public void testThreadPool() throws Exception { BindingDTO bindingDTO = new BindingDTO(); @@ -1133,6 +1190,7 @@ class ClientHandler extends SimpleChannelInboundHandler { private CountDownLatch latch; private StringBuilder body = new StringBuilder(); private String serverHeader; + private HttpHeaders headers; ClientHandler(CountDownLatch latch) { this.latch = latch; @@ -1141,6 +1199,7 @@ class ClientHandler extends SimpleChannelInboundHandler { @Override public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { if (msg instanceof HttpResponse response) { + headers = response.headers(); serverHeader = response.headers().get("Server"); } else if (msg instanceof HttpContent content) { body.append(content.content().toString(CharsetUtil.UTF_8)); diff --git a/artemis-web/src/test/resources/webapps/WebServerComponentCompressionTest.txt b/artemis-web/src/test/resources/webapps/WebServerComponentCompressionTest.txt new file mode 100644 index 00000000000..fd4f971ab3b --- /dev/null +++ b/artemis-web/src/test/resources/webapps/WebServerComponentCompressionTest.txt @@ -0,0 +1 @@ +0123456789012345678901234567890123456789 \ No newline at end of file diff --git a/docs/user-manual/web-server.adoc b/docs/user-manual/web-server.adoc index 9f648e8fa2e..b71aba496ee 100644 --- a/docs/user-manual/web-server.adoc +++ b/docs/user-manual/web-server.adoc @@ -34,6 +34,15 @@ The location to redirect the requests with the root target. webContentEnabled:: Whether or not the content included in the web folder of the home and the instance directories is accessible. Default is `false`. +compressionEnabled:: +Whether to compress HTTP responses. +Uses `gzip` encoding for maximum compatibility. +This will impact any client communicating with the embedded web server including the web console and consumers of xref:metrics.adoc#metrics[metrics] (e.g. Prometheus) assuming they set the `Accept-Encoding` header to `gzip` in their HTTP requests. +Default is `false`. +compressionLevel:: +The level of compression for HTTP responses. +Only valid if `compressionEnabled` is `true`. +Default is `6`. Must be between `0` and `9` inclusive. maxThreads:: The maximum number of threads the embedded web server can create to service HTTP requests. Default is `200`.