diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java index df658c06c5f..3725785ae4e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebSe import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer; +import org.springframework.boot.autoconfigure.web.embedded.NettyWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryCustomizer; @@ -64,7 +65,8 @@ public class ReactiveManagementChildContextConfiguration { super(beanFactory, ReactiveWebServerFactoryCustomizer.class, TomcatWebServerFactoryCustomizer.class, JettyWebServerFactoryCustomizer.class, - UndertowWebServerFactoryCustomizer.class); + UndertowWebServerFactoryCustomizer.class, + NettyWebServerFactoryCustomizer.class); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java index 25a3cc66278..e973058f92e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java @@ -23,6 +23,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.webapp.WebAppContext; import org.xnio.SslClientAuthMode; +import reactor.netty.http.server.HttpServer; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -84,4 +85,19 @@ public class EmbeddedWebServerFactoryCustomizerAutoConfiguration { } + /** + * Nested configuration if Netty is being used. + */ + @Configuration + @ConditionalOnClass(HttpServer.class) + public static class NettyWebServerFactoryCustomizerConfiguration { + + @Bean + public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer( + Environment environment, ServerProperties serverProperties) { + return new NettyWebServerFactoryCustomizer(environment, serverProperties); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java new file mode 100644 index 00000000000..74ff91f6834 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java @@ -0,0 +1,65 @@ +/* + * 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; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; + +/** + * Customization for Netty-specific features. + * + * @author Brian Clozel + * @since 2.1.0 + */ +public class NettyWebServerFactoryCustomizer + implements WebServerFactoryCustomizer, Ordered { + + private final Environment environment; + + private final ServerProperties serverProperties; + + public NettyWebServerFactoryCustomizer(Environment environment, + ServerProperties serverProperties) { + this.environment = environment; + this.serverProperties = serverProperties; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void customize(NettyReactiveWebServerFactory factory) { + factory.setUseForwardHeaders( + getOrDeduceUseForwardHeaders(this.serverProperties, this.environment)); + } + + private 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/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java new file mode 100644 index 00000000000..c2069b34c18 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java @@ -0,0 +1,75 @@ +/* + * 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; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.mock.env.MockEnvironment; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link NettyWebServerFactoryCustomizer}. + * + * @author Brian Clozel + */ +public class NettyWebServerFactoryCustomizerTests { + + private MockEnvironment environment; + + private ServerProperties serverProperties; + + private NettyWebServerFactoryCustomizer customizer; + + @Before + public void setup() { + this.environment = new MockEnvironment(); + this.serverProperties = new ServerProperties(); + ConfigurationPropertySources.attach(this.environment); + this.customizer = new NettyWebServerFactoryCustomizer(this.environment, + this.serverProperties); + } + + @Test + public void deduceUseForwardHeadersUndertow() { + this.environment.setProperty("DYNO", "-"); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory).setUseForwardHeaders(true); + } + + @Test + public void defaultUseForwardHeadersUndertow() { + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory).setUseForwardHeaders(false); + } + + @Test + public void setUseForwardHeadersUndertow() { + this.serverProperties.setUseForwardHeaders(true); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory).setUseForwardHeaders(true); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index e3cd870b74d..4cd40180c2b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -44,6 +44,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact private Duration lifecycleTimeout; + private boolean useForwardHeaders; + public NettyReactiveWebServerFactory() { } @@ -97,6 +99,14 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact this.lifecycleTimeout = lifecycleTimeout; } + /** + * Set if x-forward-* headers should be processed. + * @param useForwardHeaders if x-forward headers should be used + */ + public void setUseForwardHeaders(boolean useForwardHeaders) { + this.useForwardHeaders = useForwardHeaders; + } + private HttpServer createHttpServer() { HttpServer server = HttpServer.create().tcpConfiguration( (tcpServer) -> tcpServer.addressSupplier(() -> getListenAddress())); @@ -110,6 +120,7 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact getCompression()); server = compressionCustomizer.apply(server); } + server = (this.useForwardHeaders ? server.forwarded() : server.noForwarded()); return applyCustomizers(server); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java index 067ea60d450..9c70d43ef42 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java @@ -93,4 +93,11 @@ public class JettyReactiveWebServerFactoryTests .isEqualTo(localhost.getHostAddress()); } + @Test + public void useForwardedHeaders() { + JettyReactiveWebServerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + assertForwardHeaderIsUsed(factory); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java index 43983335165..55c8a446785 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java @@ -74,4 +74,11 @@ public class NettyReactiveWebServerFactoryTests } } + @Test + public void useForwardedHeaders() { + NettyReactiveWebServerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + assertForwardHeaderIsUsed(factory); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java index 3333ae1f187..894669150ed 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java @@ -23,6 +23,7 @@ import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.AprLifecycleListener; +import org.apache.catalina.valves.RemoteIpValve; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; @@ -133,4 +134,13 @@ public class TomcatReactiveWebServerFactoryTests } } + @Test + public void useForwardedHeaders() { + TomcatReactiveWebServerFactory factory = getFactory(); + RemoteIpValve valve = new RemoteIpValve(); + valve.setProtocolHeader("X-Forwarded-Proto"); + factory.addEngineValves(valve); + assertForwardHeaderIsUsed(factory); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java index 873c2d1357c..a57627ec036 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * 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. @@ -76,4 +76,11 @@ public class UndertowReactiveWebServerFactoryTests } } + @Test + public void useForwardedHeaders() { + UndertowReactiveWebServerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + assertForwardHeaderIsUsed(factory); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 50d4f509ac8..40d6f1e8e18 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -47,6 +47,7 @@ import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -318,6 +319,14 @@ public abstract class AbstractReactiveWebServerFactoryTests { assertThat(response.getHeaders().keySet()).doesNotContain("X-Test-Compressed"); } + protected void assertForwardHeaderIsUsed(AbstractReactiveWebServerFactory factory) { + this.webServer = factory.getWebServer(new XForwardedHandler()); + this.webServer.start(); + String body = getWebClient().build().get().header("X-Forwarded-Proto", "https") + .retrieve().bodyToMono(String.class).block(); + assertThat(body).isEqualTo("https"); + } + protected static class EchoHandler implements HttpHandler { public EchoHandler() { @@ -374,4 +383,17 @@ public abstract class AbstractReactiveWebServerFactoryTests { } + protected static class XForwardedHandler implements HttpHandler { + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + String scheme = request.getURI().getScheme(); + DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + DataBuffer buffer = bufferFactory + .wrap(scheme.getBytes(StandardCharsets.UTF_8)); + return response.writeWith(Mono.just(buffer)); + } + + } + }