Skip to content

Commit

Permalink
Configure X-Forwarded-* support with Reactor Netty
Browse files Browse the repository at this point in the history
This commit configures the new X-Forwarded-* / Forwarded HTTP headers
support with Reactor Netty in its 0.8.0 version.

Closes gh-10900
  • Loading branch information
bclozel committed Jun 13, 2018
1 parent f8eefa8 commit cb6c8f7
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand Down Expand Up @@ -64,7 +65,8 @@ class ReactiveManagementWebServerFactoryCustomizer extends
super(beanFactory, ReactiveWebServerFactoryCustomizer.class,
TomcatWebServerFactoryCustomizer.class,
JettyWebServerFactoryCustomizer.class,
UndertowWebServerFactoryCustomizer.class);
UndertowWebServerFactoryCustomizer.class,
NettyWebServerFactoryCustomizer.class);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand Down Expand Up @@ -84,4 +85,19 @@ public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(

}

/**
* 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);
}

}

}
Original file line number Diff line number Diff line change
@@ -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<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();
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact

private Duration lifecycleTimeout;

private boolean useForwardHeaders;

public NettyReactiveWebServerFactory() {
}

Expand Down Expand Up @@ -97,6 +99,14 @@ public void setLifecycleTimeout(Duration 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() {
HttpServer server = HttpServer.create().tcpConfiguration(
(tcpServer) -> tcpServer.addressSupplier(() -> getListenAddress()));
Expand All @@ -110,6 +120,7 @@ private HttpServer createHttpServer() {
getCompression());
server = compressionCustomizer.apply(server);
}
server = (this.useForwardHeaders ? server.forwarded() : server.noForwarded());
return applyCustomizers(server);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,11 @@ public void specificIPAddressNotReverseResolved() throws Exception {
.isEqualTo(localhost.getHostAddress());
}

@Test
public void useForwardedHeaders() {
JettyReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,11 @@ public void nettyCustomizers() {
}
}

@Test
public void useForwardedHeaders() {
NettyReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand Down Expand Up @@ -133,4 +134,13 @@ public void tomcatConnectorCustomizersShouldBeInvoked() {
}
}

@Test
public void useForwardedHeaders() {
TomcatReactiveWebServerFactory factory = getFactory();
RemoteIpValve valve = new RemoteIpValve();
valve.setProtocolHeader("X-Forwarded-Proto");
factory.addEngineValves(valve);
assertForwardHeaderIsUsed(factory);
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -76,4 +76,11 @@ public void builderCustomizersShouldBeInvoked() {
}
}

@Test
public void useForwardedHeaders() {
UndertowReactiveWebServerFactory factory = getFactory();
factory.setUseForwardHeaders(true);
assertForwardHeaderIsUsed(factory);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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;
Expand Down Expand Up @@ -318,6 +319,14 @@ protected void assertResponseIsNotCompressed(ResponseEntity<Void> response) {
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() {
Expand Down Expand Up @@ -374,4 +383,17 @@ public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response)

}

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));
}

}

}

0 comments on commit cb6c8f7

Please sign in to comment.