Commit 20b29db5 authored by Phillip Webb's avatar Phillip Webb

Unify ServerProperties X-Forwarded-For support

Add a new `server.use-forward-headers` property which can be used to
switch on X-Forwarded-For header support in all supported embedded
servlet containers.

This commit reverts the decision to enable `RemoteIpValve` with Tomcat
by default (gh-3782) and requires that either `user-forward-headers` is
set to true or that `server.tomcat.protocol-header` or
`server.tomcat.remote-ip-header` are set.

See gh-4018
See gh-3782
parent c35105b8
...@@ -47,6 +47,7 @@ import org.springframework.boot.context.embedded.InitParameterConfiguringServlet ...@@ -47,6 +47,7 @@ import org.springframework.boot.context.embedded.InitParameterConfiguringServlet
import org.springframework.boot.context.embedded.JspServlet; import org.springframework.boot.context.embedded.JspServlet;
import org.springframework.boot.context.embedded.ServletContextInitializer; import org.springframework.boot.context.embedded.ServletContextInitializer;
import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.embedded.Ssl;
import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
...@@ -102,6 +103,11 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -102,6 +103,11 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
*/ */
private final Map<String, String> contextParameters = new HashMap<String, String>(); private final Map<String, String> contextParameters = new HashMap<String, String>();
/**
* If X-Forwarded-* headers should be applied to the HttpRequest.
*/
private boolean useForwardHeaders;
private Session session = new Session(); private Session session = new Session();
@NestedConfigurationProperty @NestedConfigurationProperty
...@@ -115,6 +121,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -115,6 +121,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
private final Tomcat tomcat = new Tomcat(); private final Tomcat tomcat = new Tomcat();
private final Jetty jetty = new Jetty();
private final Undertow undertow = new Undertow(); private final Undertow undertow = new Undertow();
@Override @Override
...@@ -150,11 +158,16 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -150,11 +158,16 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
container.setCompression(getCompression()); container.setCompression(getCompression());
} }
if (container instanceof TomcatEmbeddedServletContainerFactory) { if (container instanceof TomcatEmbeddedServletContainerFactory) {
getTomcat() getTomcat().customizeTomcat(this,
.customizeTomcat((TomcatEmbeddedServletContainerFactory) container); (TomcatEmbeddedServletContainerFactory) container);
}
if (container instanceof JettyEmbeddedServletContainerFactory) {
getJetty().customizeJetty(this,
(JettyEmbeddedServletContainerFactory) container);
} }
if (container instanceof UndertowEmbeddedServletContainerFactory) { if (container instanceof UndertowEmbeddedServletContainerFactory) {
getUndertow().customizeUndertow( getUndertow().customizeUndertow(this,
(UndertowEmbeddedServletContainerFactory) container); (UndertowEmbeddedServletContainerFactory) container);
} }
container.addInitializers(new SessionConfiguringInitializer(this.session)); container.addInitializers(new SessionConfiguringInitializer(this.session));
...@@ -267,6 +280,14 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -267,6 +280,14 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
return this.contextParameters; return this.contextParameters;
} }
public boolean isUseForwardHeaders() {
return this.useForwardHeaders;
}
public void setUseForwardHeaders(boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders;
}
/** /**
* Get the session timeout. * Get the session timeout.
* @return the session timeout * @return the session timeout
...@@ -320,6 +341,10 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -320,6 +341,10 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
return this.tomcat; return this.tomcat;
} }
private Jetty getJetty() {
return this.jetty;
}
public Undertow getUndertow() { public Undertow getUndertow() {
return this.undertow; return this.undertow;
} }
...@@ -488,9 +513,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -488,9 +513,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
/** /**
* Header that holds the incoming protocol, usually named "X-Forwarded-Proto". * Header that holds the incoming protocol, usually named "X-Forwarded-Proto".
* Configures a RemoteIpValve only if remoteIpHeader is also set.
*/ */
private String protocolHeader = "x-forwarded-proto"; private String protocolHeader;
/** /**
* Value of the protocol header that indicates that the incoming request uses SSL. * Value of the protocol header that indicates that the incoming request uses SSL.
...@@ -500,13 +524,12 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -500,13 +524,12 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
/** /**
* Name of the HTTP header used to override the original port value. * Name of the HTTP header used to override the original port value.
*/ */
private String portHeader = "x-forwarded-port"; private String portHeader = "X-Forwarded-Port";
/** /**
* Name of the http header from which the remote ip is extracted. Configures a * Name of the http header from which the remote ip is extracted..
* RemoteIpValve only if protocolHeader is also set.
*/ */
private String remoteIpHeader = "x-forwarded-for"; private String remoteIpHeader;
/** /**
* Tomcat base directory. If not specified a temporary directory will be used. * Tomcat base directory. If not specified a temporary directory will be used.
...@@ -659,12 +682,13 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -659,12 +682,13 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
this.uriEncoding = uriEncoding; this.uriEncoding = uriEncoding;
} }
void customizeTomcat(TomcatEmbeddedServletContainerFactory factory) { void customizeTomcat(ServerProperties serverProperties,
TomcatEmbeddedServletContainerFactory factory) {
if (getBasedir() != null) { if (getBasedir() != null) {
factory.setBaseDirectory(getBasedir()); factory.setBaseDirectory(getBasedir());
} }
customizeBackgroundProcessorDelay(factory); customizeBackgroundProcessorDelay(factory);
customizeHeaders(factory); customizeRemoteIpValve(serverProperties, factory);
if (this.maxThreads > 0) { if (this.maxThreads > 0) {
customizeMaxThreads(factory); customizeMaxThreads(factory);
} }
...@@ -691,14 +715,20 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -691,14 +715,20 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
}); });
} }
private void customizeHeaders(TomcatEmbeddedServletContainerFactory factory) { private void customizeRemoteIpValve(ServerProperties properties,
String remoteIpHeader = getRemoteIpHeader(); TomcatEmbeddedServletContainerFactory factory) {
String protocolHeader = getProtocolHeader(); String protocolHeader = getProtocolHeader();
if (StringUtils.hasText(remoteIpHeader) String remoteIpHeader = getRemoteIpHeader();
&& StringUtils.hasText(protocolHeader)) { // For back compatibility the valve is also enabled if protocol-header is set
if (StringUtils.hasText(protocolHeader)
|| StringUtils.hasText(remoteIpHeader)
|| properties.isUseForwardHeaders()) {
RemoteIpValve valve = new RemoteIpValve(); RemoteIpValve valve = new RemoteIpValve();
valve.setRemoteIpHeader(remoteIpHeader); valve.setProtocolHeader(StringUtils.hasLength(protocolHeader) ? protocolHeader
valve.setProtocolHeader(protocolHeader); : "X-Forwarded-Proto");
if (StringUtils.hasLength(remoteIpHeader)) {
valve.setRemoteIpHeader(remoteIpHeader);
}
// The internal proxies default to a white list of "safe" internal IP // The internal proxies default to a white list of "safe" internal IP
// addresses // addresses
valve.setInternalProxies(getInternalProxies()); valve.setInternalProxies(getInternalProxies());
...@@ -822,6 +852,15 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -822,6 +852,15 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
} }
private static class Jetty {
void customizeJetty(ServerProperties serverProperties,
JettyEmbeddedServletContainerFactory factory) {
factory.setUseForwardHeaders(serverProperties.isUseForwardHeaders());
}
}
public static class Undertow { public static class Undertow {
/** /**
...@@ -958,7 +997,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -958,7 +997,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
getAccesslog().setDir(accessLogDir); getAccesslog().setDir(accessLogDir);
} }
void customizeUndertow(UndertowEmbeddedServletContainerFactory factory) { void customizeUndertow(ServerProperties serverProperties,
UndertowEmbeddedServletContainerFactory factory) {
factory.setBufferSize(this.bufferSize); factory.setBufferSize(this.bufferSize);
factory.setBuffersPerRegion(this.buffersPerRegion); factory.setBuffersPerRegion(this.buffersPerRegion);
factory.setIoThreads(this.ioThreads); factory.setIoThreads(this.ioThreads);
...@@ -967,6 +1007,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord ...@@ -967,6 +1007,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord
factory.setAccessLogDirectory(this.accesslog.dir); factory.setAccessLogDirectory(this.accesslog.dir);
factory.setAccessLogPattern(this.accesslog.pattern); factory.setAccessLogPattern(this.accesslog.pattern);
factory.setAccessLogEnabled(this.accesslog.enabled); factory.setAccessLogEnabled(this.accesslog.enabled);
factory.setUseForwardHeaders(serverProperties.isUseForwardHeaders());
} }
public static class Accesslog { public static class Accesslog {
......
...@@ -39,7 +39,9 @@ import org.springframework.beans.MutablePropertyValues; ...@@ -39,7 +39,9 @@ import org.springframework.beans.MutablePropertyValues;
import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.ServletContextInitializer; import org.springframework.boot.context.embedded.ServletContextInitializer;
import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
...@@ -50,6 +52,7 @@ import static org.mockito.BDDMockito.given; ...@@ -50,6 +52,7 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
/** /**
...@@ -252,27 +255,38 @@ public class ServerPropertiesTests { ...@@ -252,27 +255,38 @@ public class ServerPropertiesTests {
map.put("server.tomcat.remote_ip_header", ""); map.put("server.tomcat.remote_ip_header", "");
map.put("server.tomcat.protocol_header", ""); map.put("server.tomcat.protocol_header", "");
bindProperties(map); bindProperties(map);
TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory(); TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory();
this.properties.customize(container); this.properties.customize(container);
assertEquals(0, container.getValves().size()); assertEquals(0, container.getValves().size());
} }
@Test @Test
public void defaultTomcatRemoteIpValve() throws Exception { public void defaultTomcatRemoteIpValve() throws Exception {
// Since 1.3.0 no need to explicitly set header names Map<String, String> map = new HashMap<String, String>();
// Since 1.1.7 you need to specify at least the protocol
map.put("server.tomcat.protocol_header", "X-Forwarded-Proto");
map.put("server.tomcat.remote_ip_header", "X-Forwarded-For");
bindProperties(map);
testRemoteIpValveConfigured();
}
@Test
public void setUseForwardHeadersTomcat() throws Exception {
// Since 1.3.0 no need to explicitly set header names if use-forward-header=true
this.properties.setUseForwardHeaders(true);
testRemoteIpValveConfigured();
}
private void testRemoteIpValveConfigured() {
TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory(); TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory();
this.properties.customize(container); this.properties.customize(container);
assertEquals(1, container.getValves().size()); assertEquals(1, container.getValves().size());
Valve valve = container.getValves().iterator().next(); Valve valve = container.getValves().iterator().next();
assertThat(valve, instanceOf(RemoteIpValve.class)); assertThat(valve, instanceOf(RemoteIpValve.class));
RemoteIpValve remoteIpValve = (RemoteIpValve) valve; RemoteIpValve remoteIpValve = (RemoteIpValve) valve;
assertEquals("x-forwarded-proto", remoteIpValve.getProtocolHeader()); assertEquals("X-Forwarded-Proto", remoteIpValve.getProtocolHeader());
assertEquals("https", remoteIpValve.getProtocolHeaderHttpsValue()); assertEquals("https", remoteIpValve.getProtocolHeaderHttpsValue());
assertEquals("x-forwarded-for", remoteIpValve.getRemoteIpHeader()); assertEquals("X-Forwarded-For", remoteIpValve.getRemoteIpHeader());
String expectedInternalProxies = "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" // 10/8 String expectedInternalProxies = "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" // 10/8
+ "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" // 192.168/16 + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" // 192.168/16
+ "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" // 169.254/16 + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" // 169.254/16
...@@ -280,7 +294,6 @@ public class ServerPropertiesTests { ...@@ -280,7 +294,6 @@ public class ServerPropertiesTests {
+ "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" // 172.16/12 + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" // 172.16/12
+ "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|"
+ "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}"; + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}";
assertEquals(expectedInternalProxies, remoteIpValve.getInternalProxies()); assertEquals(expectedInternalProxies, remoteIpValve.getInternalProxies());
} }
...@@ -308,6 +321,22 @@ public class ServerPropertiesTests { ...@@ -308,6 +321,22 @@ public class ServerPropertiesTests {
assertEquals("192.168.0.1", remoteIpValve.getInternalProxies()); assertEquals("192.168.0.1", remoteIpValve.getInternalProxies());
} }
@Test
public void setUseForwardHeadersUndertow() throws Exception {
this.properties.setUseForwardHeaders(true);
UndertowEmbeddedServletContainerFactory container = spy(new UndertowEmbeddedServletContainerFactory());
this.properties.customize(container);
verify(container).setUseForwardHeaders(true);
}
@Test
public void setUseForwardHeadersJetty() throws Exception {
this.properties.setUseForwardHeaders(true);
JettyEmbeddedServletContainerFactory container = spy(new JettyEmbeddedServletContainerFactory());
this.properties.customize(container);
verify(container).setUseForwardHeaders(true);
}
private void bindProperties(Map<String, String> map) { private void bindProperties(Map<String, String> map) {
new RelaxedDataBinder(this.properties, "server").bind(new MutablePropertyValues( new RelaxedDataBinder(this.properties, "server").bind(new MutablePropertyValues(
map)); map));
......
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