diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index 4536cd27766..b17e9b697f4 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -521,8 +521,8 @@ If your web server of choice supports the set of HTTP headers you need, setting | xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.jetty[] | Reactor Netty -| `"X-Forwarded-*"` -| +| `"X-Forwarded-*"`, `"Forwarded"` +| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.reactor-netty[] |=== @@ -587,6 +587,24 @@ For more specific options, you can switch off the forwarded configuration (using +[[howto.webserver.use-behind-a-proxy-server.reactor-netty]] +=== Customize Reactor Netty's Proxy Configuration + +If you use Reactor Netty, you can additionally configure the type of the headers used to carry "`forwarded`" information. +By default, the `"X-Forwarded-*"` format is used, but you can opt into the standard RFC variant: + +[configprops,yaml] +---- +server: + netty: + forwarded-headers: + header-format: "standard" +---- + +For more specific options, you can switch off the forwarded configuration (using `server.forward-headers-strategy=NONE`) and directly use `reactor.netty.http.server.HttpServer` forwarded header support to change the HTTP configuration. + + + [[howto.webserver.enable-multiple-connectors]] == Enable Multiple Connectors diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java index 515263e1fcb..f3341f103d2 100644 --- a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactory.java @@ -59,6 +59,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact private boolean useForwardHeaders; + private boolean useRfcForwardHeader; + private @Nullable ReactorResourceFactory resourceFactory; public NettyReactiveWebServerFactory() { @@ -139,6 +141,17 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact this.useForwardHeaders = useForwardHeaders; } + /** + * Set if the RFC "Forwarded" header should be processed. + *
+ * {@link #setUseForwardHeaders(boolean)} will take precedence if enabled. + * @param useRfcForwardHeader if the "Forwarded" header should be used + * @since 4.2.0 + */ + public void setUseRfcForwardHeader(boolean useRfcForwardHeader) { + this.useRfcForwardHeader = useRfcForwardHeader; + } + /** * Set the {@link ReactorResourceFactory} to get the shared resources from. * @param resourceFactory the server resources @@ -157,10 +170,20 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact CompressionCustomizer compressionCustomizer = new CompressionCustomizer(getCompression()); server = compressionCustomizer.apply(server); } - server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders); + server = configureForwardedHeaders(server.protocol(listProtocols())); return applyCustomizers(server); } + private HttpServer configureForwardedHeaders(HttpServer server) { + if (this.useForwardHeaders) { + return server.forwarded(false, false); + } + if (this.useRfcForwardHeader) { + return server.forwarded(true, false); + } + return server.noForwarded(); + } + private HttpServer customizeSslConfiguration(HttpServer httpServer, Ssl ssl) { SslServerCustomizer customizer = new SslServerCustomizer(getHttp2(), ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles()); diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java index f46c6d9a2db..7774aa3c531 100644 --- a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizer.java @@ -59,7 +59,9 @@ public class NettyReactiveWebServerFactoryCustomizer @Override public void customize(NettyReactiveWebServerFactory factory) { - factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); + if (getOrDeduceUseForwardHeaders()) { + configureForwardedSupport(factory); + } PropertyMapper map = PropertyMapper.get(); map.from(this.nettyProperties::getConnectionTimeout) .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); @@ -81,6 +83,13 @@ public class NettyReactiveWebServerFactoryCustomizer return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } + private void configureForwardedSupport(NettyReactiveWebServerFactory factory) { + switch (this.nettyProperties.getForwardedHeaders().getHeaderFormat()) { + case X_FORWARDED -> factory.setUseForwardHeaders(true); + case STANDARD -> factory.setUseRfcForwardHeader(true); + } + } + private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectionTimeout.toMillis())); diff --git a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java index 3273c84ec4c..f37584cc524 100644 --- a/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java +++ b/module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerProperties.java @@ -90,6 +90,11 @@ public class NettyServerProperties { */ private @Nullable Duration idleTimeout; + /** + * Forwarded headers configuration. + */ + private final Forwardedheaders forwardedHeaders = new Forwardedheaders(); + public @Nullable Duration getConnectionTimeout() { return this.connectionTimeout; } @@ -146,4 +151,45 @@ public class NettyServerProperties { this.idleTimeout = idleTimeout; } + public Forwardedheaders getForwardedHeaders() { + return this.forwardedHeaders; + } + + /** + * Forwarded headers. + */ + public static class Forwardedheaders { + + /** + * Format of the forwarded headers to support. + */ + private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED; + + public HeaderFormat getHeaderFormat() { + return this.headerFormat; + } + + public void setHeaderFormat(HeaderFormat headerFormat) { + this.headerFormat = headerFormat; + } + + /** + * Formats of forwarded headers supported by {@link Forwardedheaders}. + */ + public enum HeaderFormat { + + /** + * Use the standard "Forwarded" header, as defined by RFC 7239. + */ + STANDARD, + + /** + * Use the non-standard "X-Forwarded-*" headers. + */ + X_FORWARDED + + } + + } + } diff --git a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java index 35a820ffcbc..d9e8acb5f3f 100644 --- a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java +++ b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/NettyReactiveWebServerFactoryTests.java @@ -146,6 +146,22 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor assertForwardHeaderIsUsed(factory); } + @Test + void useRfcForwardHeader() { + NettyReactiveWebServerFactory factory = getFactory(); + factory.setUseRfcForwardHeader(true); + assertRfcForwardHeaderIsUsed(factory); + } + + @Test + void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() { + NettyReactiveWebServerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + factory.setUseRfcForwardHeader(true); + assertForwardHeaderIsUsed(factory); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory)); + } + @Test @WithPackageResources("test.jks") void whenSslIsConfiguredWithAValidAliasARequestSucceeds() { diff --git a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java index c3ee9666fc5..561fd5cbe0e 100644 --- a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java +++ b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyReactiveWebServerFactoryCustomizerTests.java @@ -39,6 +39,7 @@ import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -85,7 +86,8 @@ class NettyReactiveWebServerFactoryCustomizerTests { void defaultUseForwardHeaders() { NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - then(factory).should().setUseForwardHeaders(false); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); } @Test @@ -102,7 +104,30 @@ class NettyReactiveWebServerFactoryCustomizerTests { this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE); NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - then(factory).should().setUseForwardHeaders(false); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); + } + + @Test + void forwardedHeadersFormatXForwardedConfiguresUseForwardHeaders() { + this.environment.setProperty("DYNO", "-"); + this.nettyProperties.getForwardedHeaders() + .setHeaderFormat(NettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseForwardHeaders(true); + then(factory).should(never()).setUseRfcForwardHeader(anyBoolean()); + } + + @Test + void forwardedHeadersFormatStandardConfiguresUseRfcForwardHeader() { + this.environment.setProperty("DYNO", "-"); + this.nettyProperties.getForwardedHeaders() + .setHeaderFormat(NettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseRfcForwardHeader(true); + then(factory).should(never()).setUseForwardHeaders(anyBoolean()); } @Test diff --git a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java index d1953291e59..d046a9bf5df 100644 --- a/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java +++ b/module/spring-boot-reactor-netty/src/test/java/org/springframework/boot/reactor/netty/autoconfigure/NettyServerPropertiesTests.java @@ -73,6 +73,19 @@ class NettyServerPropertiesTests { .isEqualTo(HttpDecoderSpec.DEFAULT_INITIAL_BUFFER_SIZE); } + @Test + void forwardedHeadersDefaultToXForwarded() { + assertThat(this.properties.getForwardedHeaders().getHeaderFormat()) + .isEqualTo(NettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED); + } + + @Test + void testCustomizeNettyForwardedHeadersFormat() { + bind("server.netty.forwarded-headers.header-format", "standard"); + assertThat(this.properties.getForwardedHeaders().getHeaderFormat()) + .isEqualTo(NettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD); + } + private void bind(String name, String value) { bind(Collections.singletonMap(name, value)); }