Commit cb6c8f76 authored by Brian Clozel's avatar Brian Clozel

Configure X-Forwarded-* support with Reactor Netty

This commit configures the new X-Forwarded-* / Forwarded HTTP headers
support with Reactor Netty in its 0.8.0 version.

Closes gh-10900
parent f8eefa80
...@@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebSe ...@@ -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;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer; 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.TomcatWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryCustomizer;
...@@ -64,7 +65,8 @@ public class ReactiveManagementChildContextConfiguration { ...@@ -64,7 +65,8 @@ public class ReactiveManagementChildContextConfiguration {
super(beanFactory, ReactiveWebServerFactoryCustomizer.class, super(beanFactory, ReactiveWebServerFactoryCustomizer.class,
TomcatWebServerFactoryCustomizer.class, TomcatWebServerFactoryCustomizer.class,
JettyWebServerFactoryCustomizer.class, JettyWebServerFactoryCustomizer.class,
UndertowWebServerFactoryCustomizer.class); UndertowWebServerFactoryCustomizer.class,
NettyWebServerFactoryCustomizer.class);
} }
} }
......
...@@ -23,6 +23,7 @@ import org.eclipse.jetty.server.Server; ...@@ -23,6 +23,7 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.webapp.WebAppContext;
import org.xnio.SslClientAuthMode; import org.xnio.SslClientAuthMode;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
...@@ -84,4 +85,19 @@ public class EmbeddedWebServerFactoryCustomizerAutoConfiguration { ...@@ -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);
}
}
} }
/*
* 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<NettyReactiveWebServerFactory>, 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();
}
}
/*
* 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);
}
}
...@@ -44,6 +44,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact ...@@ -44,6 +44,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
private Duration lifecycleTimeout; private Duration lifecycleTimeout;
private boolean useForwardHeaders;
public NettyReactiveWebServerFactory() { public NettyReactiveWebServerFactory() {
} }
...@@ -97,6 +99,14 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact ...@@ -97,6 +99,14 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
this.lifecycleTimeout = lifecycleTimeout; 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() { private HttpServer createHttpServer() {
HttpServer server = HttpServer.create().tcpConfiguration( HttpServer server = HttpServer.create().tcpConfiguration(
(tcpServer) -> tcpServer.addressSupplier(() -> getListenAddress())); (tcpServer) -> tcpServer.addressSupplier(() -> getListenAddress()));
...@@ -110,6 +120,7 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact ...@@ -110,6 +120,7 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
getCompression()); getCompression());
server = compressionCustomizer.apply(server); server = compressionCustomizer.apply(server);
} }
server = (this.useForwardHeaders ? server.forwarded() : server.noForwarded());
return applyCustomizers(server); return applyCustomizers(server);
} }
......
...@@ -93,4 +93,11 @@ public class JettyReactiveWebServerFactoryTests ...@@ -93,4 +93,11 @@ public class JettyReactiveWebServerFactoryTests
.isEqualTo(localhost.getHostAddress()); .isEqualTo(localhost.getHostAddress());
} }
@Test
public void useForwardedHeaders() {
JettyReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}
} }
...@@ -74,4 +74,11 @@ public class NettyReactiveWebServerFactoryTests ...@@ -74,4 +74,11 @@ public class NettyReactiveWebServerFactoryTests
} }
} }
@Test
public void useForwardedHeaders() {
NettyReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}
} }
...@@ -23,6 +23,7 @@ import org.apache.catalina.LifecycleEvent; ...@@ -23,6 +23,7 @@ import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener; import org.apache.catalina.LifecycleListener;
import org.apache.catalina.connector.Connector; import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.AprLifecycleListener; import org.apache.catalina.core.AprLifecycleListener;
import org.apache.catalina.valves.RemoteIpValve;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InOrder; import org.mockito.InOrder;
...@@ -133,4 +134,13 @@ public class TomcatReactiveWebServerFactoryTests ...@@ -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);
}
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -76,4 +76,11 @@ public class UndertowReactiveWebServerFactoryTests ...@@ -76,4 +76,11 @@ public class UndertowReactiveWebServerFactoryTests
} }
} }
@Test
public void useForwardedHeaders() {
UndertowReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}
} }
...@@ -47,6 +47,7 @@ import org.springframework.boot.web.server.Compression; ...@@ -47,6 +47,7 @@ import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
...@@ -318,6 +319,14 @@ public abstract class AbstractReactiveWebServerFactoryTests { ...@@ -318,6 +319,14 @@ public abstract class AbstractReactiveWebServerFactoryTests {
assertThat(response.getHeaders().keySet()).doesNotContain("X-Test-Compressed"); 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 { protected static class EchoHandler implements HttpHandler {
public EchoHandler() { public EchoHandler() {
...@@ -374,4 +383,17 @@ public abstract class AbstractReactiveWebServerFactoryTests { ...@@ -374,4 +383,17 @@ public abstract class AbstractReactiveWebServerFactoryTests {
} }
protected static class XForwardedHandler implements HttpHandler {
@Override
public Mono<Void> 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));
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment