From 98ac2218db152f9a6f5ed2befb6d41d7b11862a1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 11 Jan 2018 11:15:23 +0100 Subject: [PATCH] Apply server.undertow.* config to reactive servers This commit applies `server.undertow.*` configuration properties to Undertow when configured as a reactive web server. See gh-11500 --- .../embedded/undertow/UndertowCustomizer.java | 110 ++++++++++++ ...ultReactiveWebServerFactoryCustomizer.java | 6 + ...aultServletWebServerFactoryCustomizer.java | 81 +-------- ...activeWebServerFactoryCustomizerTests.java | 49 ++++++ .../src/checkstyle/import-control.xml | 3 + .../ConfigurableUndertowWebServerFactory.java | 114 +++++++++++++ .../UndertowCompressionConfigurer.java | 128 ++++++++++++++ .../UndertowReactiveWebServerFactory.java | 160 +++++++++++++++++- .../undertow/UndertowServletWebServer.java | 89 +--------- .../UndertowServletWebServerFactory.java | 31 ++-- 10 files changed, 581 insertions(+), 190 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/undertow/UndertowCustomizer.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowCompressionConfigurer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/undertow/UndertowCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/undertow/UndertowCustomizer.java new file mode 100644 index 0000000000..8876f37354 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/undertow/UndertowCustomizer.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * 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 org.springframework.boot.autoconfigure.web.embedded.undertow; + +import java.time.Duration; + +import io.undertow.UndertowOptions; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.web.embedded.undertow.ConfigurableUndertowWebServerFactory; +import org.springframework.core.env.Environment; + +/** + * Customization for Undertow-specific features common + * for both Servlet and Reactive servers. + * + * @author Brian Clozel + */ +public final class UndertowCustomizer { + + private UndertowCustomizer() { + } + + public static void customizeUndertow(ServerProperties serverProperties, + Environment environment, ConfigurableUndertowWebServerFactory factory) { + + ServerProperties.Undertow undertowProperties = serverProperties.getUndertow(); + ServerProperties.Undertow.Accesslog accesslogProperties = undertowProperties + .getAccesslog(); + if (undertowProperties.getBufferSize() != null) { + factory.setBufferSize(undertowProperties.getBufferSize()); + } + if (undertowProperties.getIoThreads() != null) { + factory.setIoThreads(undertowProperties.getIoThreads()); + } + if (undertowProperties.getWorkerThreads() != null) { + factory.setWorkerThreads(undertowProperties.getWorkerThreads()); + } + if (undertowProperties.getDirectBuffers() != null) { + factory.setUseDirectBuffers(undertowProperties.getDirectBuffers()); + } + if (undertowProperties.getAccesslog().getEnabled() != null) { + factory.setAccessLogEnabled(accesslogProperties.getEnabled()); + } + factory.setAccessLogDirectory(accesslogProperties.getDir()); + factory.setAccessLogPattern(accesslogProperties.getPattern()); + factory.setAccessLogPrefix(accesslogProperties.getPrefix()); + factory.setAccessLogSuffix(accesslogProperties.getSuffix()); + factory.setAccessLogRotate(accesslogProperties.isRotate()); + factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders(serverProperties, environment)); + if (serverProperties.getMaxHttpHeaderSize() > 0) { + customizeMaxHttpHeaderSize(factory, + serverProperties.getMaxHttpHeaderSize()); + } + if (undertowProperties.getMaxHttpPostSize() > 0) { + customizeMaxHttpPostSize(factory, + undertowProperties.getMaxHttpPostSize()); + } + if (serverProperties.getConnectionTimeout() != null) { + customizeConnectionTimeout(factory, + serverProperties.getConnectionTimeout()); + } + factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo + .setEagerFilterInit(undertowProperties.isEagerFilterInit())); + } + + private static void customizeConnectionTimeout( + ConfigurableUndertowWebServerFactory factory, Duration connectionTimeout) { + factory.addBuilderCustomizers((builder) -> builder.setSocketOption( + UndertowOptions.NO_REQUEST_TIMEOUT, + (int) connectionTimeout.toMillis())); + } + + private static void customizeMaxHttpHeaderSize( + ConfigurableUndertowWebServerFactory factory, int maxHttpHeaderSize) { + factory.addBuilderCustomizers((builder) -> builder + .setServerOption(UndertowOptions.MAX_HEADER_SIZE, maxHttpHeaderSize)); + } + + private static void customizeMaxHttpPostSize( + ConfigurableUndertowWebServerFactory factory, long maxHttpPostSize) { + factory.addBuilderCustomizers((builder) -> builder + .setServerOption(UndertowOptions.MAX_ENTITY_SIZE, maxHttpPostSize)); + } + + private static boolean getOrDeduceUseForwardHeaders(ServerProperties serverProperties, + Environment environment) { + if (serverProperties.isUseForwardHeaders() != null) { + return serverProperties.isUseForwardHeaders(); + } + CloudPlatform platform = CloudPlatform.getActive(environment); + return platform != null && platform.isUsingForwardHeaders(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizer.java index e1aa7a6ecf..430eed31ab 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizer.java @@ -19,8 +19,10 @@ package org.springframework.boot.autoconfigure.web.reactive; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.embedded.jetty.JettyCustomizer; import org.springframework.boot.autoconfigure.web.embedded.tomcat.TomcatCustomizer; +import org.springframework.boot.autoconfigure.web.embedded.undertow.UndertowCustomizer; import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory; +import org.springframework.boot.web.embedded.undertow.UndertowReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.EnvironmentAware; @@ -79,6 +81,10 @@ public class DefaultReactiveWebServerFactoryCustomizer implements JettyCustomizer.customizeJetty(this.serverProperties, this.environment, (JettyReactiveWebServerFactory) factory); } + if (factory instanceof UndertowReactiveWebServerFactory) { + UndertowCustomizer.customizeUndertow(this.serverProperties, this.environment, + (UndertowReactiveWebServerFactory) factory); + } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java index c26b5b4b59..20f27e860a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java @@ -16,7 +16,6 @@ package org.springframework.boot.autoconfigure.web.servlet; -import java.time.Duration; import java.util.LinkedHashSet; import java.util.Set; @@ -24,13 +23,11 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.SessionCookieConfig; -import io.undertow.UndertowOptions; - import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties.Session; import org.springframework.boot.autoconfigure.web.embedded.jetty.JettyCustomizer; import org.springframework.boot.autoconfigure.web.embedded.tomcat.TomcatCustomizer; -import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.autoconfigure.web.embedded.undertow.UndertowCustomizer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -129,15 +126,6 @@ public class DefaultServletWebServerFactoryCustomizer this.serverProperties.getServlet().getContextParameters())); } - private static boolean getOrDeduceUseForwardHeaders(ServerProperties serverProperties, - Environment environment) { - if (serverProperties.isUseForwardHeaders() != null) { - return serverProperties.isUseForwardHeaders(); - } - CloudPlatform platform = CloudPlatform.getActive(environment); - return platform != null && platform.isUsingForwardHeaders(); - } - /** * {@link ServletContextInitializer} to apply appropriate parts of the {@link Session} * configuration. @@ -231,71 +219,4 @@ public class DefaultServletWebServerFactoryCustomizer } } - private static class UndertowCustomizer { - - protected static void customizeUndertow(ServerProperties serverProperties, - Environment environment, UndertowServletWebServerFactory factory) { - - ServerProperties.Undertow undertowProperties = serverProperties.getUndertow(); - ServerProperties.Undertow.Accesslog accesslogProperties = undertowProperties - .getAccesslog(); - if (undertowProperties.getBufferSize() != null) { - factory.setBufferSize(undertowProperties.getBufferSize()); - } - if (undertowProperties.getIoThreads() != null) { - factory.setIoThreads(undertowProperties.getIoThreads()); - } - if (undertowProperties.getWorkerThreads() != null) { - factory.setWorkerThreads(undertowProperties.getWorkerThreads()); - } - if (undertowProperties.getDirectBuffers() != null) { - factory.setDirectBuffers(undertowProperties.getDirectBuffers()); - } - if (undertowProperties.getAccesslog().getEnabled() != null) { - factory.setAccessLogEnabled(accesslogProperties.getEnabled()); - } - factory.setAccessLogDirectory(accesslogProperties.getDir()); - factory.setAccessLogPattern(accesslogProperties.getPattern()); - factory.setAccessLogPrefix(accesslogProperties.getPrefix()); - factory.setAccessLogSuffix(accesslogProperties.getSuffix()); - factory.setAccessLogRotate(accesslogProperties.isRotate()); - factory.setUseForwardHeaders( - getOrDeduceUseForwardHeaders(serverProperties, environment)); - if (serverProperties.getMaxHttpHeaderSize() > 0) { - customizeMaxHttpHeaderSize(factory, - serverProperties.getMaxHttpHeaderSize()); - } - if (undertowProperties.getMaxHttpPostSize() > 0) { - customizeMaxHttpPostSize(factory, - undertowProperties.getMaxHttpPostSize()); - } - if (serverProperties.getConnectionTimeout() != null) { - customizeConnectionTimeout(factory, - serverProperties.getConnectionTimeout()); - } - factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo - .setEagerFilterInit(undertowProperties.isEagerFilterInit())); - } - - private static void customizeConnectionTimeout( - UndertowServletWebServerFactory factory, Duration connectionTimeout) { - factory.addBuilderCustomizers((builder) -> builder.setSocketOption( - UndertowOptions.NO_REQUEST_TIMEOUT, - (int) connectionTimeout.toMillis())); - } - - private static void customizeMaxHttpHeaderSize( - UndertowServletWebServerFactory factory, int maxHttpHeaderSize) { - factory.addBuilderCustomizers((builder) -> builder - .setServerOption(UndertowOptions.MAX_HEADER_SIZE, maxHttpHeaderSize)); - } - - private static void customizeMaxHttpPostSize( - UndertowServletWebServerFactory factory, long maxHttpPostSize) { - factory.addBuilderCustomizers((builder) -> builder - .setServerOption(UndertowOptions.MAX_ENTITY_SIZE, maxHttpPostSize)); - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizerTests.java index fbaaf3e4ab..6fbf4ab842 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/DefaultReactiveWebServerFactoryCustomizerTests.java @@ -44,12 +44,15 @@ import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; +import org.springframework.boot.web.embedded.undertow.UndertowReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -434,6 +437,52 @@ public class DefaultReactiveWebServerFactoryCustomizerTests { } } + @Test + public void customizeUndertowAccessLog() { + Map map = new HashMap<>(); + map.put("server.undertow.accesslog.enabled", "true"); + map.put("server.undertow.accesslog.pattern", "foo"); + map.put("server.undertow.accesslog.prefix", "test_log"); + map.put("server.undertow.accesslog.suffix", "txt"); + map.put("server.undertow.accesslog.dir", "test-logs"); + map.put("server.undertow.accesslog.rotate", "false"); + bindProperties(map); + UndertowReactiveWebServerFactory factory = spy( + new UndertowReactiveWebServerFactory()); + this.customizer.customize(factory); + verify(factory).setAccessLogEnabled(true); + verify(factory).setAccessLogPattern("foo"); + verify(factory).setAccessLogPrefix("test_log"); + verify(factory).setAccessLogSuffix("txt"); + verify(factory).setAccessLogDirectory(new File("test-logs")); + verify(factory).setAccessLogRotate(false); + } + + @Test + public void setUseForwardHeadersUndertow() { + this.properties.setUseForwardHeaders(true); + UndertowReactiveWebServerFactory factory = spy(new UndertowReactiveWebServerFactory()); + this.customizer.customize(factory); + verify(factory).setUseForwardHeaders(true); + } + + @Test + public void deduceUseForwardHeadersUndertow() { + this.customizer.setEnvironment(new MockEnvironment().withProperty("DYNO", "-")); + UndertowReactiveWebServerFactory factory = spy( + new UndertowReactiveWebServerFactory()); + this.customizer.customize(factory); + verify(factory).setUseForwardHeaders(true); + } + + @Test + public void skipNullElementsForUndertow() { + UndertowReactiveWebServerFactory factory = mock( + UndertowReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory, never()).setAccessLogEnabled(anyBoolean()); + } + private NCSARequestLog getNCSARequestLog(JettyWebServer webServer) { RequestLog requestLog = webServer.getServer().getRequestLog(); assertThat(requestLog).isInstanceOf(NCSARequestLog.class); diff --git a/spring-boot-project/spring-boot-parent/src/checkstyle/import-control.xml b/spring-boot-project/spring-boot-parent/src/checkstyle/import-control.xml index 9722ef6576..6b8c0d2092 100644 --- a/spring-boot-project/spring-boot-parent/src/checkstyle/import-control.xml +++ b/spring-boot-project/spring-boot-parent/src/checkstyle/import-control.xml @@ -20,6 +20,9 @@ + + + diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java new file mode 100644 index 0000000000..a39482ce57 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/ConfigurableUndertowWebServerFactory.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * 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 org.springframework.boot.web.embedded.undertow; + +import java.io.File; + +import io.undertow.Undertow; +import io.undertow.servlet.api.DeploymentInfo; + +/** + * Web Server Factory configuration for Undertow-specific features. + * + * @author Brian Clozel + * @since 2.0.0 + * @see UndertowServletWebServerFactory + * @see UndertowReactiveWebServerFactory + */ +public interface ConfigurableUndertowWebServerFactory { + + /** + * Add {@link UndertowBuilderCustomizer}s that should be used to customize the + * Undertow {@link Undertow.Builder}. + * @param customizers the customizers to add + */ + void addBuilderCustomizers(UndertowBuilderCustomizer... customizers); + + /** + * Add {@link UndertowDeploymentInfoCustomizer}s that should be used to customize the + * Undertow {@link DeploymentInfo}. + * @param customizers the customizers to add + */ + void addDeploymentInfoCustomizers(UndertowDeploymentInfoCustomizer... customizers); + + /** + * Set the buffer size. + * @param bufferSize buffer size + */ + void setBufferSize(Integer bufferSize); + + /** + * Set the number of IO Threads. + * @param ioThreads number of IO Threads + */ + void setIoThreads(Integer ioThreads); + + /** + * Set the number of Worker Threads. + * @param workerThreads number of Worker Threads + */ + void setWorkerThreads(Integer workerThreads); + + /** + * Set whether direct buffers should be used. + * @param useForwardHeaders whether direct buffers should be used + */ + void setUseDirectBuffers(Boolean useForwardHeaders); + + /** + * Set the access log directory. + * @param accessLogDirectory access log directory + */ + void setAccessLogDirectory(File accessLogDirectory); + + /** + * Set the access log pattern. + * @param accessLogPattern access log pattern + */ + void setAccessLogPattern(String accessLogPattern); + + /** + * Set the access log prefix. + * @param accessLogPrefix log prefix + */ + void setAccessLogPrefix(String accessLogPrefix); + + /** + * Set the access log suffix. + * @param accessLogSuffix access log suffix + */ + void setAccessLogSuffix(String accessLogSuffix); + + /** + * Set whether access logs are enabled. + * @param accessLogEnabled whether access logs are enabled + */ + void setAccessLogEnabled(boolean accessLogEnabled); + + /** + * Set whether access logs rotation is enabled. + * @param accessLogRotate whether access logs rotation is enabled + */ + void setAccessLogRotate(boolean accessLogRotate); + + /** + * Set if x-forward-* headers should be processed. + * @param useForwardHeaders if x-forward headers should be used + */ + void setUseForwardHeaders(boolean useForwardHeaders); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowCompressionConfigurer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowCompressionConfigurer.java new file mode 100644 index 0000000000..7f59c9821e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowCompressionConfigurer.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * 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 org.springframework.boot.web.embedded.undertow; + +import java.util.ArrayList; +import java.util.List; + +import io.undertow.attribute.RequestHeaderAttribute; +import io.undertow.predicate.Predicate; +import io.undertow.predicate.Predicates; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.encoding.ContentEncodingRepository; +import io.undertow.server.handlers.encoding.EncodingHandler; +import io.undertow.server.handlers.encoding.GzipEncodingProvider; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; + +import org.springframework.boot.web.server.Compression; +import org.springframework.http.HttpHeaders; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Configure the HTTP compression on an Undertow {@link HttpHandler}. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +final class UndertowCompressionConfigurer { + + private UndertowCompressionConfigurer() { + } + + /** + * Optionally wrap the given {@link HttpHandler} for HTTP compression support. + * @param compression the HTTP compression configuration + * @param httpHandler the HTTP handler to wrap + * @return the wrapped HTTP handler if compression is enabled, or the handler itself + */ + public static HttpHandler configureCompression(Compression compression, HttpHandler httpHandler) { + if (compression == null || !compression.getEnabled()) { + return httpHandler; + } + ContentEncodingRepository repository = new ContentEncodingRepository(); + repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50, + Predicates.and(getCompressionPredicates(compression))); + return new EncodingHandler(repository).setNext(httpHandler); + } + + private static Predicate[] getCompressionPredicates(Compression compression) { + List predicates = new ArrayList<>(); + predicates.add(new MaxSizePredicate(compression.getMinResponseSize())); + predicates.add(new CompressibleMimeTypePredicate(compression.getMimeTypes())); + if (compression.getExcludedUserAgents() != null) { + for (String agent : compression.getExcludedUserAgents()) { + RequestHeaderAttribute agentHeader = new RequestHeaderAttribute( + new HttpString(HttpHeaders.USER_AGENT)); + predicates.add(Predicates.not(Predicates.regex(agentHeader, agent))); + } + } + return predicates.toArray(new Predicate[predicates.size()]); + } + + private static class CompressibleMimeTypePredicate implements Predicate { + + private final List mimeTypes; + + CompressibleMimeTypePredicate(String[] mimeTypes) { + this.mimeTypes = new ArrayList<>(mimeTypes.length); + for (String mimeTypeString : mimeTypes) { + this.mimeTypes.add(MimeTypeUtils.parseMimeType(mimeTypeString)); + } + } + + @Override + public boolean resolve(HttpServerExchange value) { + String contentType = value.getResponseHeaders() + .getFirst(HttpHeaders.CONTENT_TYPE); + if (contentType != null) { + for (MimeType mimeType : this.mimeTypes) { + if (mimeType + .isCompatibleWith(MimeTypeUtils.parseMimeType(contentType))) { + return true; + } + } + } + return false; + } + + } + + /** + * Predicate that returns true if the Content-Size of a request is above a given value + * or is missing. + */ + private static class MaxSizePredicate implements Predicate { + + private final Predicate maxContentSize; + + MaxSizePredicate(int size) { + this.maxContentSize = Predicates.maxContentSize(size); + } + + @Override + public boolean resolve(HttpServerExchange value) { + if (value.getResponseHeaders().contains(Headers.CONTENT_LENGTH)) { + return this.maxContentSize.resolve(value); + } + return true; + } + + } +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java index 075bcd7c26..7aa99009bb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java @@ -16,18 +16,29 @@ package org.springframework.boot.web.embedded.undertow; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.UndertowOptions; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.accesslog.AccessLogHandler; +import io.undertow.server.handlers.accesslog.AccessLogReceiver; +import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; +import io.undertow.servlet.api.DeploymentInfo; +import org.xnio.OptionMap; +import org.xnio.Options; +import org.xnio.Xnio; +import org.xnio.XnioWorker; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; -import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; import org.springframework.util.Assert; @@ -37,7 +48,12 @@ import org.springframework.util.Assert; * @author Brian Clozel * @since 2.0.0 */ -public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory { +public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory + implements ConfigurableUndertowWebServerFactory { + + private List builderCustomizers = new ArrayList<>(); + + private List deploymentInfoCustomizers = new ArrayList<>(); private Integer bufferSize; @@ -47,7 +63,20 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF private Boolean directBuffers; - private List builderCustomizers = new ArrayList<>(); + private File accessLogDirectory; + + private String accessLogPattern; + + private String accessLogPrefix; + + private String accessLogSuffix; + + private boolean accessLogEnabled = false; + + private boolean accessLogRotate = true; + + private boolean useForwardHeaders; + /** * Create a new {@link UndertowReactiveWebServerFactory} instance. @@ -65,9 +94,9 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF } @Override - public WebServer getWebServer(HttpHandler httpHandler) { + public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) { Undertow.Builder builder = createBuilder(getPort()); - UndertowHttpHandlerAdapter handler = new UndertowHttpHandlerAdapter(httpHandler); + HttpHandler handler = createUndertowHandler(httpHandler); builder.setHandler(handler); return new UndertowWebServer(builder, getPort() >= 0); } @@ -98,6 +127,50 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF return builder; } + private HttpHandler createUndertowHandler(org.springframework.http.server.reactive.HttpHandler httpHandler) { + HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler); + if (this.useForwardHeaders) { + handler = Handlers.proxyPeerAddress(handler); + } + handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler); + if (isAccessLogEnabled()) { + handler = createAccessLogHandler(handler); + } + return handler; + } + + private AccessLogHandler createAccessLogHandler(io.undertow.server.HttpHandler handler) { + try { + createAccessLogDirectoryIfNecessary(); + String prefix = (this.accessLogPrefix != null ? this.accessLogPrefix + : "access_log."); + AccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver( + createWorker(), this.accessLogDirectory, prefix, this.accessLogSuffix, + this.accessLogRotate); + String formatString = (this.accessLogPattern != null) ? this.accessLogPattern + : "common"; + return new AccessLogHandler(handler, accessLogReceiver, formatString, + Undertow.class.getClassLoader()); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to create AccessLogHandler", ex); + } + } + + private void createAccessLogDirectoryIfNecessary() { + Assert.state(this.accessLogDirectory != null, "Access log directory is not set"); + if (!this.accessLogDirectory.isDirectory() && !this.accessLogDirectory.mkdirs()) { + throw new IllegalStateException("Failed to create access log directory '" + + this.accessLogDirectory + "'"); + } + } + + private XnioWorker createWorker() throws IOException { + Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader()); + return xnio.createWorker( + OptionMap.builder().set(Options.THREAD_DAEMON, true).getMap()); + } + private void customizeSsl(Undertow.Builder builder) { new SslBuilderCustomizer(getPort(), getAddress(), getSsl(), getSslStoreProvider()) .customize(builder); @@ -114,19 +187,94 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF return getAddress().getHostAddress(); } + /** + * Set {@link UndertowDeploymentInfoCustomizer}s that should be applied to the + * Undertow {@link DeploymentInfo}. Calling this method will replace any existing + * customizers. + * @param customizers the customizers to set + */ + public void setDeploymentInfoCustomizers( + Collection customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + this.deploymentInfoCustomizers = new ArrayList<>(customizers); + } + + /** + * Returns a mutable collection of the {@link UndertowDeploymentInfoCustomizer}s that + * will be applied to the Undertow {@link DeploymentInfo}. + * @return the customizers that will be applied + */ + public Collection getDeploymentInfoCustomizers() { + return this.deploymentInfoCustomizers; + } + + @Override + public void addDeploymentInfoCustomizers( + UndertowDeploymentInfoCustomizer... customizers) { + Assert.notNull(customizers, "UndertowDeploymentInfoCustomizers must not be null"); + this.deploymentInfoCustomizers.addAll(Arrays.asList(customizers)); + } + + @Override + public void setAccessLogDirectory(File accessLogDirectory) { + this.accessLogDirectory = accessLogDirectory; + } + + @Override + public void setAccessLogPattern(String accessLogPattern) { + this.accessLogPattern = accessLogPattern; + } + + @Override + public void setAccessLogPrefix(String accessLogPrefix) { + this.accessLogPrefix = accessLogPrefix; + } + + @Override + public void setAccessLogSuffix(String accessLogSuffix) { + this.accessLogSuffix = accessLogSuffix; + } + + public boolean isAccessLogEnabled() { + return this.accessLogEnabled; + } + + @Override + public void setAccessLogEnabled(boolean accessLogEnabled) { + this.accessLogEnabled = accessLogEnabled; + } + + @Override + public void setAccessLogRotate(boolean accessLogRotate) { + this.accessLogRotate = accessLogRotate; + } + + protected final boolean isUseForwardHeaders() { + return this.useForwardHeaders; + } + + @Override + public void setUseForwardHeaders(boolean useForwardHeaders) { + this.useForwardHeaders = useForwardHeaders; + } + + @Override public void setBufferSize(Integer bufferSize) { this.bufferSize = bufferSize; } + @Override public void setIoThreads(Integer ioThreads) { this.ioThreads = ioThreads; } + @Override public void setWorkerThreads(Integer workerThreads) { this.workerThreads = workerThreads; } - public void setDirectBuffers(Boolean directBuffers) { + @Override + public void setUseDirectBuffers(Boolean directBuffers) { this.directBuffers = directBuffers; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java index 1c9bd97991..ed04430d71 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java @@ -28,17 +28,8 @@ import javax.servlet.ServletException; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.Undertow.Builder; -import io.undertow.attribute.RequestHeaderAttribute; -import io.undertow.predicate.Predicate; -import io.undertow.predicate.Predicates; import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.encoding.ContentEncodingRepository; -import io.undertow.server.handlers.encoding.EncodingHandler; -import io.undertow.server.handlers.encoding.GzipEncodingProvider; import io.undertow.servlet.api.DeploymentManager; -import io.undertow.util.Headers; -import io.undertow.util.HttpString; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.xnio.channels.BoundChannel; @@ -47,9 +38,6 @@ import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; -import org.springframework.http.HttpHeaders; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -216,37 +204,14 @@ public class UndertowServletWebServer implements WebServer { } private HttpHandler getContextHandler(HttpHandler httpHandler) { - HttpHandler contextHandler = configurationCompressionIfNecessary(httpHandler); + HttpHandler contextHandler = UndertowCompressionConfigurer + .configureCompression(this.compression, httpHandler); if (StringUtils.isEmpty(this.contextPath)) { return contextHandler; } return Handlers.path().addPrefixPath(this.contextPath, contextHandler); } - private HttpHandler configurationCompressionIfNecessary(HttpHandler httpHandler) { - if (this.compression == null || !this.compression.getEnabled()) { - return httpHandler; - } - ContentEncodingRepository repository = new ContentEncodingRepository(); - repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50, - Predicates.and(getCompressionPredicates(this.compression))); - return new EncodingHandler(repository).setNext(httpHandler); - } - - private Predicate[] getCompressionPredicates(Compression compression) { - List predicates = new ArrayList<>(); - predicates.add(new MaxSizePredicate(compression.getMinResponseSize())); - predicates.add(new CompressibleMimeTypePredicate(compression.getMimeTypes())); - if (compression.getExcludedUserAgents() != null) { - for (String agent : compression.getExcludedUserAgents()) { - RequestHeaderAttribute agentHeader = new RequestHeaderAttribute( - new HttpString(HttpHeaders.USER_AGENT)); - predicates.add(Predicates.not(Predicates.regex(agentHeader, agent))); - } - } - return predicates.toArray(new Predicate[predicates.size()]); - } - private String getPortsDescription() { List ports = getActualPorts(); if (!ports.isEmpty()) { @@ -395,54 +360,4 @@ public class UndertowServletWebServer implements WebServer { } - private static class CompressibleMimeTypePredicate implements Predicate { - - private final List mimeTypes; - - CompressibleMimeTypePredicate(String[] mimeTypes) { - this.mimeTypes = new ArrayList<>(mimeTypes.length); - for (String mimeTypeString : mimeTypes) { - this.mimeTypes.add(MimeTypeUtils.parseMimeType(mimeTypeString)); - } - } - - @Override - public boolean resolve(HttpServerExchange value) { - String contentType = value.getResponseHeaders() - .getFirst(HttpHeaders.CONTENT_TYPE); - if (contentType != null) { - for (MimeType mimeType : this.mimeTypes) { - if (mimeType - .isCompatibleWith(MimeTypeUtils.parseMimeType(contentType))) { - return true; - } - } - } - return false; - } - - } - - /** - * Predicate that returns true if the Content-Size of a request is above a given value - * or is missing. - */ - private static class MaxSizePredicate implements Predicate { - - private final Predicate maxContentSize; - - MaxSizePredicate(int size) { - this.maxContentSize = Predicates.maxContentSize(size); - } - - @Override - public boolean resolve(HttpServerExchange value) { - if (value.getResponseHeaders().contains(Headers.CONTENT_LENGTH)) { - return this.maxContentSize.resolve(value); - } - return true; - } - - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index d5c10f9852..315d771d94 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -85,7 +85,7 @@ import org.springframework.util.Assert; * @see UndertowServletWebServer */ public class UndertowServletWebServerFactory extends AbstractServletWebServerFactory - implements ResourceLoaderAware { + implements ConfigurableUndertowWebServerFactory, ResourceLoaderAware { private static final Set> NO_CLASSES = Collections.emptySet(); @@ -167,11 +167,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac return this.builderCustomizers; } - /** - * Add {@link UndertowBuilderCustomizer}s that should be used to customize the - * Undertow {@link Builder}. - * @param customizers the customizers to add - */ + @Override public void addBuilderCustomizers(UndertowBuilderCustomizer... customizers) { Assert.notNull(customizers, "Customizers must not be null"); this.builderCustomizers.addAll(Arrays.asList(customizers)); @@ -198,11 +194,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac return this.deploymentInfoCustomizers; } - /** - * Add {@link UndertowDeploymentInfoCustomizer}s that should be used to customize the - * Undertow {@link DeploymentInfo}. - * @param customizers the customizers to add - */ + @Override public void addDeploymentInfoCustomizers( UndertowDeploymentInfoCustomizer... customizers) { Assert.notNull(customizers, "UndertowDeploymentInfoCustomizers must not be null"); @@ -449,10 +441,12 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac this.resourceLoader = resourceLoader; } + @Override public void setBufferSize(Integer bufferSize) { this.bufferSize = bufferSize; } + @Override public void setIoThreads(Integer ioThreads) { this.ioThreads = ioThreads; } @@ -461,14 +455,17 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac this.workerThreads = workerThreads; } - public void setDirectBuffers(Boolean directBuffers) { + @Override + public void setUseDirectBuffers(Boolean directBuffers) { this.directBuffers = directBuffers; } + @Override public void setAccessLogDirectory(File accessLogDirectory) { this.accessLogDirectory = accessLogDirectory; } + @Override public void setAccessLogPattern(String accessLogPattern) { this.accessLogPattern = accessLogPattern; } @@ -477,14 +474,17 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac return this.accessLogPrefix; } + @Override public void setAccessLogPrefix(String accessLogPrefix) { this.accessLogPrefix = accessLogPrefix; } + @Override public void setAccessLogSuffix(String accessLogSuffix) { this.accessLogSuffix = accessLogSuffix; } + @Override public void setAccessLogEnabled(boolean accessLogEnabled) { this.accessLogEnabled = accessLogEnabled; } @@ -493,6 +493,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac return this.accessLogEnabled; } + @Override public void setAccessLogRotate(boolean accessLogRotate) { this.accessLogRotate = accessLogRotate; } @@ -501,11 +502,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac return this.useForwardHeaders; } - /** - * Set if x-forward-* headers should be processed. - * @param useForwardHeaders if x-forward headers should be used - * @since 1.3.0 - */ + @Override public void setUseForwardHeaders(boolean useForwardHeaders) { this.useForwardHeaders = useForwardHeaders; }