From 50430a20c65939d50722f525c6723c17101da52a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 30 Sep 2015 16:58:55 -0700 Subject: [PATCH 1/6] Add Tomcat X-Forwarded-For header tests Update Abstract & Tomcat EmbeddedServletContainerFactoryTests to check that X-Forwarded-For headers work as expected. See gh-4018 --- ...tEmbeddedServletContainerFactoryTests.java | 34 +++++++++++++------ .../boot/context/embedded/ExampleServlet.java | 3 +- ...tEmbeddedServletContainerFactoryTests.java | 8 +++++ 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index 3df99931e5..842c953669 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -638,8 +638,9 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { return "http://localhost:" + port + resourcePath; } - protected String getResponse(String url) throws IOException, URISyntaxException { - ClientHttpResponse response = getClientResponse(url); + protected String getResponse(String url, String... headers) throws IOException, + URISyntaxException { + ClientHttpResponse response = getClientResponse(url, headers); try { return StreamUtils.copyToString(response.getBody(), Charset.forName("UTF-8")); } @@ -649,9 +650,9 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { } protected String getResponse(String url, - HttpComponentsClientHttpRequestFactory requestFactory) throws IOException, - URISyntaxException { - ClientHttpResponse response = getClientResponse(url, requestFactory); + HttpComponentsClientHttpRequestFactory requestFactory, String... headers) + throws IOException, URISyntaxException { + ClientHttpResponse response = getClientResponse(url, requestFactory, headers); try { return StreamUtils.copyToString(response.getBody(), Charset.forName("UTF-8")); } @@ -660,8 +661,8 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { } } - protected ClientHttpResponse getClientResponse(String url) throws IOException, - URISyntaxException { + protected ClientHttpResponse getClientResponse(String url, String... headers) + throws IOException, URISyntaxException { return getClientResponse(url, new HttpComponentsClientHttpRequestFactory() { @Override @@ -669,19 +670,32 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { return AbstractEmbeddedServletContainerFactoryTests.this.httpClientContext; } - }); + }, headers); } protected ClientHttpResponse getClientResponse(String url, - HttpComponentsClientHttpRequestFactory requestFactory) throws IOException, - URISyntaxException { + HttpComponentsClientHttpRequestFactory requestFactory, String... headers) + throws IOException, URISyntaxException { ClientHttpRequest request = requestFactory.createRequest(new URI(url), HttpMethod.GET); request.getHeaders().add("Cookie", "JSESSIONID=" + "123"); + for (String header : headers) { + String[] parts = header.split(":"); + request.getHeaders().add(parts[0], parts[1]); + } ClientHttpResponse response = request.execute(); return response; } + protected void assertForwardHeaderIsUsed(EmbeddedServletContainerFactory factory) + throws IOException, URISyntaxException { + this.container = factory.getEmbeddedServletContainer(new ServletRegistrationBean( + new ExampleServlet(true), "/hello")); + this.container.start(); + assertThat(getResponse(getLocalUrl("/hello"), "X-Forwarded-For:140.211.11.130"), + containsString("remoteaddr=140.211.11.130")); + } + protected abstract AbstractEmbeddedServletContainerFactory getFactory(); protected abstract Object getJspServlet(); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/ExampleServlet.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/ExampleServlet.java index 6d625aa2a2..a33aa0b976 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/ExampleServlet.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/ExampleServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2015 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. @@ -47,6 +47,7 @@ public class ExampleServlet extends GenericServlet { String content = "Hello World"; if (this.echoRequestInfo) { content += " scheme=" + request.getScheme(); + content += " remoteaddr=" + request.getRemoteAddr(); } response.getWriter().write(content); } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java index ed59f39241..49de3bd665 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java @@ -35,6 +35,7 @@ import org.apache.catalina.Valve; import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.junit.Test; import org.mockito.InOrder; @@ -333,6 +334,13 @@ public class TomcatEmbeddedServletContainerFactoryTests extends assertThat(jspServlet.findInitParameter("a"), is(equalTo("alpha"))); } + @Test + public void useForwardHeaders() throws Exception { + TomcatEmbeddedServletContainerFactory factory = getFactory(); + factory.addContextValves(new RemoteIpValve()); + assertForwardHeaderIsUsed(factory); + } + @Override protected Wrapper getJspServlet() { Container context = ((TomcatEmbeddedServletContainer) this.container).getTomcat() From 7f97681969c61e8d2bf590b168a136bbf7bb1f6e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 1 Oct 2015 15:01:54 -0700 Subject: [PATCH 2/6] Add X-Forwarded-For header support to Jetty Add a `useForwardHeaders` property to allow embedded Jetty containers to respect X-Forwarded-For headers. Fixes gh-3802 --- .../JettyEmbeddedServletContainerFactory.java | 38 +++++++++++++++++++ ...yEmbeddedServletContainerFactoryTests.java | 7 ++++ 2 files changed, 45 insertions(+) diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index 5bcce26fdc..e4dd495247 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -30,7 +30,9 @@ import java.util.Set; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.ForwardedRequestCustomizer; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.SecureRequestCustomizer; @@ -97,6 +99,8 @@ public class JettyEmbeddedServletContainerFactory extends private List configurations = new ArrayList(); + private boolean useForwardHeaders; + private List jettyServerCustomizers = new ArrayList(); private ResourceLoader resourceLoader; @@ -153,6 +157,9 @@ public class JettyEmbeddedServletContainerFactory extends for (JettyServerCustomizer customizer : getServerCustomizers()) { customizer.customize(server); } + if (this.useForwardHeaders) { + new ForwardHeadersCustomizer().customize(server); + } return getJettyEmbeddedServletContainer(server); } @@ -449,6 +456,15 @@ public class JettyEmbeddedServletContainerFactory extends this.resourceLoader = resourceLoader; } + /** + * Set if x-forward-* headers should be processed. + * @param useForwardHeaders if x-forward headers should be used + * @since 1.3.0 + */ + public void setUseForwardHeaders(boolean useForwardHeaders) { + this.useForwardHeaders = useForwardHeaders; + } + /** * Sets {@link JettyServerCustomizer}s that will be applied to the {@link Server} * before it is started. Calling this method will replace any existing configurations. @@ -669,4 +685,26 @@ public class JettyEmbeddedServletContainerFactory extends } + /** + * {@link JettyServerCustomizer} to add {@link ForwardedRequestCustomizer}. Only + * supported with Jetty 9 (hence the inner class) + */ + private static class ForwardHeadersCustomizer implements JettyServerCustomizer { + + @Override + public void customize(Server server) { + ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer(); + for (Connector connector : server.getConnectors()) { + for (ConnectionFactory connectionFactory : connector + .getConnectionFactories()) { + if (connectionFactory instanceof HttpConfiguration.ConnectionFactory) { + ((HttpConfiguration.ConnectionFactory) connectionFactory) + .getHttpConfiguration().addCustomizer(customizer); + } + } + } + } + + } + } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java index 180d1da9be..4429641b1c 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java @@ -183,6 +183,13 @@ public class JettyEmbeddedServletContainerFactoryTests extends assertThat(getJspServlet().getInitParameters(), is(equalTo(initParameters))); } + @Test + public void useForwardHeaders() throws Exception { + JettyEmbeddedServletContainerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + assertForwardHeaderIsUsed(factory); + } + @Override @SuppressWarnings("serial") // Workaround for Jetty issue - https://bugs.eclipse.org/bugs/show_bug.cgi?id=470646 From c35105b868949223c45d701606702aadb86748f2 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 1 Oct 2015 15:01:40 -0700 Subject: [PATCH 3/6] Add X-Forwarded-For header support to Undertow Add a `useForwardHeaders` property to allow embedded Undertow containers to respect X-Forwarded-For headers. Fixes gh-3881 --- .../UndertowEmbeddedServletContainer.java | 15 ++++++++++++++- .../UndertowEmbeddedServletContainerFactory.java | 13 ++++++++++++- ...ertowEmbeddedServletContainerFactoryTests.java | 7 +++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java index 7833c98e79..f6c0471aae 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java @@ -71,6 +71,8 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine private final String contextPath; + private final boolean useForwardHeaders; + private final boolean autoStart; private final Compression compression; @@ -81,9 +83,16 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine public UndertowEmbeddedServletContainer(Builder builder, DeploymentManager manager, String contextPath, int port, boolean autoStart, Compression compression) { + this(builder, manager, contextPath, port, false, autoStart, compression); + } + + public UndertowEmbeddedServletContainer(Builder builder, DeploymentManager manager, + String contextPath, int port, boolean useForwardHeaders, boolean autoStart, + Compression compression) { this.builder = builder; this.manager = manager; this.contextPath = contextPath; + this.useForwardHeaders = useForwardHeaders; this.autoStart = autoStart; this.compression = compression; } @@ -105,7 +114,11 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine private Undertow createUndertowServer() { try { HttpHandler httpHandler = this.manager.start(); - this.builder.setHandler(getContextHandler(httpHandler)); + httpHandler = getContextHandler(httpHandler); + if (this.useForwardHeaders) { + httpHandler = Handlers.proxyPeerAddress(httpHandler); + } + this.builder.setHandler(httpHandler); return this.builder.build(); } catch (ServletException ex) { diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java index e0b181c2cf..899a40d3e3 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java @@ -121,6 +121,8 @@ public class UndertowEmbeddedServletContainerFactory extends private boolean accessLogEnabled = false; + private boolean useForwardHeaders; + /** * Create a new {@link UndertowEmbeddedServletContainerFactory} instance. */ @@ -220,7 +222,7 @@ public class UndertowEmbeddedServletContainerFactory extends int port = getPort(); Builder builder = createBuilder(port); return new UndertowEmbeddedServletContainer(builder, manager, getContextPath(), - port, port >= 0, getCompression()); + port, this.useForwardHeaders, port >= 0, getCompression()); } private Builder createBuilder(int port) { @@ -519,6 +521,15 @@ public class UndertowEmbeddedServletContainerFactory extends return this.accessLogEnabled; } + /** + * Set if x-forward-* headers should be processed. + * @param useForwardHeaders if x-forward headers should be used + * @since 1.3.0 + */ + public void setUseForwardHeaders(boolean useForwardHeaders) { + this.useForwardHeaders = useForwardHeaders; + } + /** * Undertow {@link ResourceManager} for JAR resources. */ diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java index ea2b43919b..140e4f3cea 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java @@ -149,6 +149,13 @@ public class UndertowEmbeddedServletContainerFactoryTests extends assertEquals("/", contextPath.get()); } + @Test + public void useForwardHeaders() throws Exception { + UndertowEmbeddedServletContainerFactory factory = getFactory(); + factory.setUseForwardHeaders(true); + assertForwardHeaderIsUsed(factory); + } + @Override protected Object getJspServlet() { return null; // Undertow does not support JSPs From 20b29db5123544b9c92902530d02a0d59dae670a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 1 Oct 2015 15:02:09 -0700 Subject: [PATCH 4/6] 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 --- .../autoconfigure/web/ServerProperties.java | 77 ++++++++++++++----- .../web/ServerPropertiesTests.java | 45 +++++++++-- 2 files changed, 96 insertions(+), 26 deletions(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index b0c9a813c8..5335a4940c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -47,6 +47,7 @@ import org.springframework.boot.context.embedded.InitParameterConfiguringServlet import org.springframework.boot.context.embedded.JspServlet; import org.springframework.boot.context.embedded.ServletContextInitializer; 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.TomcatContextCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; @@ -102,6 +103,11 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord */ private final Map contextParameters = new HashMap(); + /** + * If X-Forwarded-* headers should be applied to the HttpRequest. + */ + private boolean useForwardHeaders; + private Session session = new Session(); @NestedConfigurationProperty @@ -115,6 +121,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord private final Tomcat tomcat = new Tomcat(); + private final Jetty jetty = new Jetty(); + private final Undertow undertow = new Undertow(); @Override @@ -150,11 +158,16 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord container.setCompression(getCompression()); } if (container instanceof TomcatEmbeddedServletContainerFactory) { - getTomcat() - .customizeTomcat((TomcatEmbeddedServletContainerFactory) container); + getTomcat().customizeTomcat(this, + (TomcatEmbeddedServletContainerFactory) container); + } + if (container instanceof JettyEmbeddedServletContainerFactory) { + getJetty().customizeJetty(this, + (JettyEmbeddedServletContainerFactory) container); } + if (container instanceof UndertowEmbeddedServletContainerFactory) { - getUndertow().customizeUndertow( + getUndertow().customizeUndertow(this, (UndertowEmbeddedServletContainerFactory) container); } container.addInitializers(new SessionConfiguringInitializer(this.session)); @@ -267,6 +280,14 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord return this.contextParameters; } + public boolean isUseForwardHeaders() { + return this.useForwardHeaders; + } + + public void setUseForwardHeaders(boolean useForwardHeaders) { + this.useForwardHeaders = useForwardHeaders; + } + /** * Get the session timeout. * @return the session timeout @@ -320,6 +341,10 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord return this.tomcat; } + private Jetty getJetty() { + return this.jetty; + } + public Undertow getUndertow() { return this.undertow; } @@ -488,9 +513,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord /** * 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. @@ -500,13 +524,12 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord /** * 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 - * RemoteIpValve only if protocolHeader is also set. + * Name of the http header from which the remote ip is extracted.. */ - private String remoteIpHeader = "x-forwarded-for"; + private String remoteIpHeader; /** * Tomcat base directory. If not specified a temporary directory will be used. @@ -659,12 +682,13 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord this.uriEncoding = uriEncoding; } - void customizeTomcat(TomcatEmbeddedServletContainerFactory factory) { + void customizeTomcat(ServerProperties serverProperties, + TomcatEmbeddedServletContainerFactory factory) { if (getBasedir() != null) { factory.setBaseDirectory(getBasedir()); } customizeBackgroundProcessorDelay(factory); - customizeHeaders(factory); + customizeRemoteIpValve(serverProperties, factory); if (this.maxThreads > 0) { customizeMaxThreads(factory); } @@ -691,14 +715,20 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord }); } - private void customizeHeaders(TomcatEmbeddedServletContainerFactory factory) { - String remoteIpHeader = getRemoteIpHeader(); + private void customizeRemoteIpValve(ServerProperties properties, + TomcatEmbeddedServletContainerFactory factory) { String protocolHeader = getProtocolHeader(); - if (StringUtils.hasText(remoteIpHeader) - && StringUtils.hasText(protocolHeader)) { + String remoteIpHeader = getRemoteIpHeader(); + // 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(); - valve.setRemoteIpHeader(remoteIpHeader); - valve.setProtocolHeader(protocolHeader); + valve.setProtocolHeader(StringUtils.hasLength(protocolHeader) ? protocolHeader + : "X-Forwarded-Proto"); + if (StringUtils.hasLength(remoteIpHeader)) { + valve.setRemoteIpHeader(remoteIpHeader); + } // The internal proxies default to a white list of "safe" internal IP // addresses valve.setInternalProxies(getInternalProxies()); @@ -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 { /** @@ -958,7 +997,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord getAccesslog().setDir(accessLogDir); } - void customizeUndertow(UndertowEmbeddedServletContainerFactory factory) { + void customizeUndertow(ServerProperties serverProperties, + UndertowEmbeddedServletContainerFactory factory) { factory.setBufferSize(this.bufferSize); factory.setBuffersPerRegion(this.buffersPerRegion); factory.setIoThreads(this.ioThreads); @@ -967,6 +1007,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord factory.setAccessLogDirectory(this.accesslog.dir); factory.setAccessLogPattern(this.accesslog.pattern); factory.setAccessLogEnabled(this.accesslog.enabled); + factory.setUseForwardHeaders(serverProperties.isUseForwardHeaders()); } public static class Accesslog { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index ec50a64e3e..298e6bdbf1 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -39,7 +39,9 @@ import org.springframework.beans.MutablePropertyValues; import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; 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.undertow.UndertowEmbeddedServletContainerFactory; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -50,6 +52,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; /** @@ -252,27 +255,38 @@ public class ServerPropertiesTests { map.put("server.tomcat.remote_ip_header", ""); map.put("server.tomcat.protocol_header", ""); bindProperties(map); - TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory(); this.properties.customize(container); - assertEquals(0, container.getValves().size()); } @Test public void defaultTomcatRemoteIpValve() throws Exception { - // Since 1.3.0 no need to explicitly set header names + Map map = new HashMap(); + // 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(); this.properties.customize(container); - assertEquals(1, container.getValves().size()); Valve valve = container.getValves().iterator().next(); assertThat(valve, instanceOf(RemoteIpValve.class)); RemoteIpValve remoteIpValve = (RemoteIpValve) valve; - assertEquals("x-forwarded-proto", remoteIpValve.getProtocolHeader()); + assertEquals("X-Forwarded-Proto", remoteIpValve.getProtocolHeader()); 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 + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" // 192.168/16 + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" // 169.254/16 @@ -280,7 +294,6 @@ public class ServerPropertiesTests { + "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\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}"; - assertEquals(expectedInternalProxies, remoteIpValve.getInternalProxies()); } @@ -308,6 +321,22 @@ public class ServerPropertiesTests { 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 map) { new RelaxedDataBinder(this.properties, "server").bind(new MutablePropertyValues( map)); From 33ce160251b1bdf60a6e52ac94a8e29199f21e87 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 1 Oct 2015 22:34:11 -0700 Subject: [PATCH 5/6] Automatically X-Forwarded-For in the cloud Update `ServerProperties` to automatically enable `use-forward-headers` when running on a cloud platform. A new `CloudPlatform` enum has been introduced that detects Heroku and Cloud Foundry. See gh-4018 --- .../autoconfigure/web/ServerProperties.java | 33 ++- .../web/ServerPropertiesTests.java | 40 ++- ...udFoundryVcapEnvironmentPostProcessor.java | 241 ++++++++++++++++++ .../boot/cloud/CloudPlatform.java | 88 +++++++ .../{cloudfoundry => cloud}/package-info.java | 6 +- .../VcapEnvironmentPostProcessor.java | 174 +------------ .../main/resources/META-INF/spring.factories | 4 +- .../boot/cloud/CloudPlatformTests.java | 75 ++++++ ...dryVcapEnvironmentPostProcessorTests.java} | 9 +- 9 files changed, 483 insertions(+), 187 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java rename spring-boot/src/main/java/org/springframework/boot/{cloudfoundry => cloud}/package-info.java (78%) create mode 100644 spring-boot/src/test/java/org/springframework/boot/cloud/CloudPlatformTests.java rename spring-boot/src/test/java/org/springframework/boot/{cloudfoundry/VcapEnvironmentPostProcessorTests.java => cloud/cloudfoundry/CloudFoundryVcapEnvironmentPostProcessorTests.java} (93%) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 5335a4940c..e274303a83 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -38,6 +38,7 @@ import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.boot.autoconfigure.web.ServerProperties.Session.Cookie; +import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; @@ -55,7 +56,9 @@ import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServle import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.EnvironmentAware; import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; import org.springframework.util.StringUtils; /** @@ -70,7 +73,8 @@ import org.springframework.util.StringUtils; * @author Marcos Barbero */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) -public class ServerProperties implements EmbeddedServletContainerCustomizer, Ordered { +public class ServerProperties implements EmbeddedServletContainerCustomizer, + EnvironmentAware, Ordered { /** * Server HTTP port. @@ -106,7 +110,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord /** * If X-Forwarded-* headers should be applied to the HttpRequest. */ - private boolean useForwardHeaders; + private Boolean useForwardHeaders; private Session session = new Session(); @@ -125,11 +129,18 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord private final Undertow undertow = new Undertow(); + private Environment environment; + @Override public int getOrder() { return 0; } + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + @Override public void customize(ConfigurableEmbeddedServletContainer container) { if (getPort() != null) { @@ -280,14 +291,22 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord return this.contextParameters; } - public boolean isUseForwardHeaders() { + public Boolean isUseForwardHeaders() { return this.useForwardHeaders; } - public void setUseForwardHeaders(boolean useForwardHeaders) { + public void setUseForwardHeaders(Boolean useForwardHeaders) { this.useForwardHeaders = useForwardHeaders; } + protected final boolean getOrDeduceUseForwardHeaders() { + if (this.useForwardHeaders != null) { + return this.useForwardHeaders; + } + CloudPlatform platform = CloudPlatform.getActive(this.environment); + return (platform == null ? false : platform.isUsingForwardHeaders()); + } + /** * Get the session timeout. * @return the session timeout @@ -722,7 +741,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord // For back compatibility the valve is also enabled if protocol-header is set if (StringUtils.hasText(protocolHeader) || StringUtils.hasText(remoteIpHeader) - || properties.isUseForwardHeaders()) { + || properties.getOrDeduceUseForwardHeaders()) { RemoteIpValve valve = new RemoteIpValve(); valve.setProtocolHeader(StringUtils.hasLength(protocolHeader) ? protocolHeader : "X-Forwarded-Proto"); @@ -856,7 +875,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord void customizeJetty(ServerProperties serverProperties, JettyEmbeddedServletContainerFactory factory) { - factory.setUseForwardHeaders(serverProperties.isUseForwardHeaders()); + factory.setUseForwardHeaders(serverProperties.getOrDeduceUseForwardHeaders()); } } @@ -1007,7 +1026,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer, Ord factory.setAccessLogDirectory(this.accesslog.dir); factory.setAccessLogPattern(this.accesslog.pattern); factory.setAccessLogEnabled(this.accesslog.enabled); - factory.setUseForwardHeaders(serverProperties.isUseForwardHeaders()); + factory.setUseForwardHeaders(serverProperties.getOrDeduceUseForwardHeaders()); } public static class Accesslog { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 298e6bdbf1..1dcae8c618 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -42,6 +42,7 @@ 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.undertow.UndertowEmbeddedServletContainerFactory; +import org.springframework.mock.env.MockEnvironment; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -61,6 +62,7 @@ import static org.mockito.Mockito.verify; * @author Dave Syer * @author Stephane Nicoll * @author Andy Wilkinson + * @author Phillip Webb */ public class ServerPropertiesTests { @@ -242,10 +244,8 @@ public class ServerPropertiesTests { Map map = new HashMap(); map.put("server.display-name", "MyBootApp"); bindProperties(map); - TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory(); this.properties.customize(container); - assertEquals("MyBootApp", container.getDisplayName()); } @@ -277,6 +277,12 @@ public class ServerPropertiesTests { testRemoteIpValveConfigured(); } + @Test + public void deduceUseForwardHeadersTomcat() throws Exception { + this.properties.setEnvironment(new MockEnvironment().withProperty("DYNO", "-")); + testRemoteIpValveConfigured(); + } + private void testRemoteIpValveConfigured() { TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory(); this.properties.customize(container); @@ -321,6 +327,13 @@ public class ServerPropertiesTests { assertEquals("192.168.0.1", remoteIpValve.getInternalProxies()); } + @Test + public void defaultUseForwardHeadersUndertow() throws Exception { + UndertowEmbeddedServletContainerFactory container = spy(new UndertowEmbeddedServletContainerFactory()); + this.properties.customize(container); + verify(container).setUseForwardHeaders(false); + } + @Test public void setUseForwardHeadersUndertow() throws Exception { this.properties.setUseForwardHeaders(true); @@ -329,6 +342,21 @@ public class ServerPropertiesTests { verify(container).setUseForwardHeaders(true); } + @Test + public void deduceUseForwardHeadersUndertow() throws Exception { + this.properties.setEnvironment(new MockEnvironment().withProperty("DYNO", "-")); + UndertowEmbeddedServletContainerFactory container = spy(new UndertowEmbeddedServletContainerFactory()); + this.properties.customize(container); + verify(container).setUseForwardHeaders(true); + } + + @Test + public void defaultUseForwardHeadersJetty() throws Exception { + JettyEmbeddedServletContainerFactory container = spy(new JettyEmbeddedServletContainerFactory()); + this.properties.customize(container); + verify(container).setUseForwardHeaders(false); + } + @Test public void setUseForwardHeadersJetty() throws Exception { this.properties.setUseForwardHeaders(true); @@ -337,6 +365,14 @@ public class ServerPropertiesTests { verify(container).setUseForwardHeaders(true); } + @Test + public void deduceUseForwardHeadersJetty() throws Exception { + this.properties.setEnvironment(new MockEnvironment().withProperty("DYNO", "-")); + JettyEmbeddedServletContainerFactory container = spy(new JettyEmbeddedServletContainerFactory()); + this.properties.customize(container); + verify(container).setUseForwardHeaders(true); + } + private void bindProperties(Map map) { new RelaxedDataBinder(this.properties, "server").bind(new MutablePropertyValues( map)); diff --git a/spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java new file mode 100644 index 0000000000..5b9bd2153a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java @@ -0,0 +1,241 @@ +/* + * Copyright 2010-2015 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.cloud; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.config.ConfigFileEnvironmentPostProcessor; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.json.JsonParser; +import org.springframework.boot.json.JsonParserFactory; +import org.springframework.core.Ordered; +import org.springframework.core.env.CommandLinePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.util.StringUtils; + +/** + * An {@link EnvironmentPostProcessor} that knows where to find VCAP (a.k.a. Cloud + * Foundry) meta data in the existing environment. It parses out the VCAP_APPLICATION and + * VCAP_SERVICES meta data and dumps it in a form that is easily consumed by + * {@link Environment} users. If the app is running in Cloud Foundry then both meta data + * items are JSON objects encoded in OS environment variables. VCAP_APPLICATION is a + * shallow hash with basic information about the application (name, instance id, instance + * index, etc.), and VCAP_SERVICES is a hash of lists where the keys are service labels + * and the values are lists of hashes of service instance meta data. Examples are: + * + *
+ * VCAP_APPLICATION: {"instance_id":"2ce0ac627a6c8e47e936d829a3a47b5b","instance_index":0,
+ *   "version":"0138c4a6-2a73-416b-aca0-572c09f7ca53","name":"foo",
+ *   "uris":["foo.cfapps.io"], ...}
+ * VCAP_SERVICES: {"rds-mysql-1.0":[{"name":"mysql","label":"rds-mysql-1.0","plan":"10mb",
+ *   "credentials":{"name":"d04fb13d27d964c62b267bbba1cffb9da","hostname":"mysql-service-public.clqg2e2w3ecf.us-east-1.rds.amazonaws.com",
+ *   "host":"mysql-service-public.clqg2e2w3ecf.us-east-1.rds.amazonaws.com","port":3306,"user":"urpRuqTf8Cpe6",
+ *   "username":"urpRuqTf8Cpe6","password":"pxLsGVpsC9A5S"}
+ * }]}
+ * 
+ * + * These objects are flattened into properties. The VCAP_APPLICATION object goes straight + * to {@code vcap.application.*} in a fairly obvious way, and the VCAP_SERVICES object is + * unwrapped so that it is a hash of objects with key equal to the service instance name + * (e.g. "mysql" in the example above), and value equal to that instances properties, and + * then flattened in the same way. E.g. + * + *
+ * vcap.application.instance_id: 2ce0ac627a6c8e47e936d829a3a47b5b
+ * vcap.application.version: 0138c4a6-2a73-416b-aca0-572c09f7ca53
+ * vcap.application.name: foo
+ * vcap.application.uris[0]: foo.cfapps.io
+ *
+ * vcap.services.mysql.name: mysql
+ * vcap.services.mysql.label: rds-mysql-1.0
+ * vcap.services.mysql.credentials.name: d04fb13d27d964c62b267bbba1cffb9da
+ * vcap.services.mysql.credentials.port: 3306
+ * vcap.services.mysql.credentials.host: mysql-service-public.clqg2e2w3ecf.us-east-1.rds.amazonaws.com
+ * vcap.services.mysql.credentials.username: urpRuqTf8Cpe6
+ * vcap.services.mysql.credentials.password: pxLsGVpsC9A5S
+ * ...
+ * 
+ * + * N.B. this initializer is mainly intended for informational use (the application and + * instance ids are particularly useful). For service binding you might find that Spring + * Cloud is more convenient and more robust against potential changes in Cloud Foundry. + * + * @author Dave Syer + * @author Andy Wilkinson + */ +public class CloudFoundryVcapEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + + private static final Log logger = LogFactory + .getLog(CloudFoundryVcapEnvironmentPostProcessor.class); + + private static final String VCAP_APPLICATION = "VCAP_APPLICATION"; + + private static final String VCAP_SERVICES = "VCAP_SERVICES"; + + // Before ConfigFileApplicationListener so values there can use these ones + private int order = ConfigFileEnvironmentPostProcessor.DEFAULT_ORDER - 1; + + private final JsonParser parser = JsonParserFactory.getJsonParser(); + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, + SpringApplication application) { + if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) { + Properties properties = new Properties(); + addWithPrefix(properties, getPropertiesFromApplication(environment), + "vcap.application."); + addWithPrefix(properties, getPropertiesFromServices(environment), + "vcap.services."); + MutablePropertySources propertySources = environment.getPropertySources(); + if (propertySources + .contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) { + propertySources.addAfter( + CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME, + new PropertiesPropertySource("vcap", properties)); + } + else { + propertySources + .addFirst(new PropertiesPropertySource("vcap", properties)); + } + } + } + + private void addWithPrefix(Properties properties, Properties other, String prefix) { + for (String key : other.stringPropertyNames()) { + String prefixed = prefix + key; + properties.setProperty(prefixed, other.getProperty(key)); + } + } + + private Properties getPropertiesFromApplication(Environment environment) { + Properties properties = new Properties(); + try { + String property = environment.getProperty(VCAP_APPLICATION, "{}"); + Map map = this.parser.parseMap(property); + extractPropertiesFromApplication(properties, map); + } + catch (Exception ex) { + logger.error("Could not parse VCAP_APPLICATION", ex); + } + return properties; + } + + private Properties getPropertiesFromServices(Environment environment) { + Properties properties = new Properties(); + try { + String property = environment.getProperty(VCAP_SERVICES, "{}"); + Map map = this.parser.parseMap(property); + extractPropertiesFromServices(properties, map); + } + catch (Exception ex) { + logger.error("Could not parse VCAP_SERVICES", ex); + } + return properties; + } + + private void extractPropertiesFromApplication(Properties properties, + Map map) { + if (map != null) { + flatten(properties, map, ""); + } + } + + private void extractPropertiesFromServices(Properties properties, + Map map) { + if (map != null) { + for (Object services : map.values()) { + @SuppressWarnings("unchecked") + List list = (List) services; + for (Object object : list) { + @SuppressWarnings("unchecked") + Map service = (Map) object; + String key = (String) service.get("name"); + if (key == null) { + key = (String) service.get("label"); + } + flatten(properties, service, key); + } + } + } + } + + @SuppressWarnings("unchecked") + private void flatten(Properties properties, Map input, String path) { + for (Entry entry : input.entrySet()) { + String key = getFullKey(path, entry.getKey()); + Object value = entry.getValue(); + if (value instanceof Map) { + // Need a compound key + flatten(properties, (Map) value, key); + } + else if (value instanceof Collection) { + // Need a compound key + Collection collection = (Collection) value; + properties.put(key, + StringUtils.collectionToCommaDelimitedString(collection)); + int count = 0; + for (Object item : collection) { + String itemKey = "[" + (count++) + "]"; + flatten(properties, Collections.singletonMap(itemKey, item), key); + } + } + else if (value instanceof String) { + properties.put(key, value); + } + else if (value instanceof Number) { + properties.put(key, value.toString()); + } + else if (value instanceof Boolean) { + properties.put(key, value.toString()); + } + else { + properties.put(key, value == null ? "" : value); + } + } + } + + private String getFullKey(String path, String key) { + if (!StringUtils.hasText(path)) { + return key; + } + if (key.startsWith("[")) { + return path + key; + } + return path + "." + key; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java b/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java new file mode 100644 index 0000000000..5f07071d0c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2015 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.cloud; + +import org.springframework.core.env.Environment; + +/** + * Simple detection for well known cloud platforms. For more advanced cloud provider + * integration consider the Spring Clould project. + * + * @author Phillip Webb + * @since 1.3.0 + * @see "http://cloud.spring.io" + */ +public enum CloudPlatform { + + /** + * Cloud Foundry platform. + */ + CLOUD_FOUNDRY { + + @Override + public boolean isActive(Environment environment) { + return environment.containsProperty("VCAP_APPLICATION") + || environment.containsProperty("VCAP_SERVICES"); + } + + }, + + /** + * Heroku platform. + */ + HEROKU { + + @Override + public boolean isActive(Environment environment) { + return environment.containsProperty("DYNO"); + } + + }; + + /** + * Determines if the platform is active (i.e. the application is running in it). + * @param environment the environment + * @return if the platform is active. + */ + public abstract boolean isActive(Environment environment); + + /** + * Returns if the platform is behind a load balancer and uses + * {@literal X-Forwarded-For} headers. + * @return if {@literal X-Forwarded-For} headers are used + */ + public boolean isUsingForwardHeaders() { + return true; + } + + /** + * Returns the active {@link CloudPlatform} or {@code null} if one cannot be deduced. + * @param environment the environment + * @return the {@link CloudPlatform} or {@code null} + */ + public static CloudPlatform getActive(Environment environment) { + if (environment != null) { + for (CloudPlatform cloudPlatform : values()) { + if (cloudPlatform.isActive(environment)) { + return cloudPlatform; + } + } + } + return null; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/package-info.java b/spring-boot/src/main/java/org/springframework/boot/cloud/package-info.java similarity index 78% rename from spring-boot/src/main/java/org/springframework/boot/cloudfoundry/package-info.java rename to spring-boot/src/main/java/org/springframework/boot/cloud/package-info.java index ecb66184c4..1d9c3ad8ab 100644 --- a/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/package-info.java +++ b/spring-boot/src/main/java/org/springframework/boot/cloud/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -15,7 +15,7 @@ */ /** - * Support for Cloud Foundry PAAS based deployment. + * Low level support for Cloud deployments. */ -package org.springframework.boot.cloudfoundry; +package org.springframework.boot.cloud; diff --git a/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapEnvironmentPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapEnvironmentPostProcessor.java index 44e4bc6e5f..93f15b6d49 100644 --- a/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapEnvironmentPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapEnvironmentPostProcessor.java @@ -16,27 +16,9 @@ package org.springframework.boot.cloudfoundry; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.context.config.ConfigFileEnvironmentPostProcessor; +import org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor; import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.boot.json.JsonParser; -import org.springframework.boot.json.JsonParserFactory; -import org.springframework.core.Ordered; -import org.springframework.core.env.CommandLinePropertySource; -import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertiesPropertySource; -import org.springframework.util.StringUtils; /** * An {@link EnvironmentPostProcessor} that knows where to find VCAP (a.k.a. Cloud @@ -87,156 +69,10 @@ import org.springframework.util.StringUtils; * * @author Dave Syer * @author Andy Wilkinson + * @deprecated since 1.3.0 in favor of CloudFoundryVcapEnvironmentPostProcessor */ -public class VcapEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { - - private static final Log logger = LogFactory - .getLog(VcapEnvironmentPostProcessor.class); - - private static final String VCAP_APPLICATION = "VCAP_APPLICATION"; - - private static final String VCAP_SERVICES = "VCAP_SERVICES"; - - // Before ConfigFileApplicationListener so values there can use these ones - private int order = ConfigFileEnvironmentPostProcessor.DEFAULT_ORDER - 1; - - private final JsonParser parser = JsonParserFactory.getJsonParser(); - - public void setOrder(int order) { - this.order = order; - } - - @Override - public int getOrder() { - return this.order; - } - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, - SpringApplication application) { - if (!environment.containsProperty(VCAP_APPLICATION) - && !environment.containsProperty(VCAP_SERVICES)) { - return; - } - Properties properties = new Properties(); - addWithPrefix(properties, getPropertiesFromApplication(environment), - "vcap.application."); - addWithPrefix(properties, getPropertiesFromServices(environment), - "vcap.services."); - MutablePropertySources propertySources = environment.getPropertySources(); - if (propertySources - .contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) { - propertySources.addAfter( - CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME, - new PropertiesPropertySource("vcap", properties)); - } - else { - propertySources.addFirst(new PropertiesPropertySource("vcap", properties)); - } - } - - private void addWithPrefix(Properties properties, Properties other, String prefix) { - for (String key : other.stringPropertyNames()) { - String prefixed = prefix + key; - properties.setProperty(prefixed, other.getProperty(key)); - } - } - - private Properties getPropertiesFromApplication(Environment environment) { - Properties properties = new Properties(); - try { - Map map = this.parser.parseMap(environment.getProperty( - VCAP_APPLICATION, "{}")); - extractPropertiesFromApplication(properties, map); - } - catch (Exception ex) { - logger.error("Could not parse VCAP_APPLICATION", ex); - } - return properties; - } - - private Properties getPropertiesFromServices(Environment environment) { - Properties properties = new Properties(); - try { - Map map = this.parser.parseMap(environment.getProperty( - VCAP_SERVICES, "{}")); - extractPropertiesFromServices(properties, map); - } - catch (Exception ex) { - logger.error("Could not parse VCAP_SERVICES", ex); - } - return properties; - } - - private void extractPropertiesFromApplication(Properties properties, - Map map) { - if (map != null) { - flatten(properties, map, ""); - } - } - - private void extractPropertiesFromServices(Properties properties, - Map map) { - if (map != null) { - for (Object services : map.values()) { - @SuppressWarnings("unchecked") - List list = (List) services; - for (Object object : list) { - @SuppressWarnings("unchecked") - Map service = (Map) object; - String key = (String) service.get("name"); - if (key == null) { - key = (String) service.get("label"); - } - flatten(properties, service, key); - } - } - } - } - - @SuppressWarnings("unchecked") - private void flatten(Properties properties, Map input, String path) { - for (Entry entry : input.entrySet()) { - String key = getFullKey(path, entry.getKey()); - Object value = entry.getValue(); - if (value instanceof Map) { - // Need a compound key - flatten(properties, (Map) value, key); - } - else if (value instanceof Collection) { - // Need a compound key - Collection collection = (Collection) value; - properties.put(key, - StringUtils.collectionToCommaDelimitedString(collection)); - int count = 0; - for (Object item : collection) { - String itemKey = "[" + (count++) + "]"; - flatten(properties, Collections.singletonMap(itemKey, item), key); - } - } - else if (value instanceof String) { - properties.put(key, value); - } - else if (value instanceof Number) { - properties.put(key, value.toString()); - } - else if (value instanceof Boolean) { - properties.put(key, value.toString()); - } - else { - properties.put(key, value == null ? "" : value); - } - } - } - - private String getFullKey(String path, String key) { - if (!StringUtils.hasText(path)) { - return key; - } - if (key.startsWith("[")) { - return path + key; - } - return path + "." + key; - } +@Deprecated +public class VcapEnvironmentPostProcessor extends + CloudFoundryVcapEnvironmentPostProcessor { } diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index 02af0a6206..ee6f17a1ea 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -28,5 +28,5 @@ org.springframework.boot.logging.LoggingApplicationListener # Environment Post Processors org.springframework.boot.env.EnvironmentPostProcessor=\ -org.springframework.boot.cloudfoundry.VcapEnvironmentPostProcessor,\ -org.springframework.boot.context.config.ConfigFileEnvironmentPostProcessor \ No newline at end of file +org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\ +org.springframework.boot.context.config.ConfigFileEnvironmentPostProcessor diff --git a/spring-boot/src/test/java/org/springframework/boot/cloud/CloudPlatformTests.java b/spring-boot/src/test/java/org/springframework/boot/cloud/CloudPlatformTests.java new file mode 100644 index 0000000000..b1d714710f --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/cloud/CloudPlatformTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2015 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.cloud; + +import org.junit.Test; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link CloudPlatform}. + * + * @author Phillip Webb + */ +public class CloudPlatformTests { + + @Test + public void getActiveWhenEnvironmentIsNullShouldReturnNull() throws Exception { + CloudPlatform platform = CloudPlatform.getActive(null); + assertThat(platform, nullValue()); + } + + @Test + public void getActiveWhenNotInCloudShouldReturnNull() throws Exception { + Environment environment = new MockEnvironment(); + CloudPlatform platform = CloudPlatform.getActive(environment); + assertThat(platform, nullValue()); + + } + + @Test + public void getActiveWhenHasVcapApplicationShouldReturnCloudFoundry() + throws Exception { + Environment environment = new MockEnvironment().withProperty("VCAP_APPLICATION", + "---"); + CloudPlatform platform = CloudPlatform.getActive(environment); + assertThat(platform, equalTo(CloudPlatform.CLOUD_FOUNDRY)); + assertThat(platform.isActive(environment), equalTo(true)); + } + + @Test + public void getActiveWhenHasVcapServicesShouldReturnCloudFoundry() throws Exception { + Environment environment = new MockEnvironment().withProperty("VCAP_SERVICES", + "---"); + CloudPlatform platform = CloudPlatform.getActive(environment); + assertThat(platform, equalTo(CloudPlatform.CLOUD_FOUNDRY)); + assertThat(platform.isActive(environment), equalTo(true)); + } + + @Test + public void getActiveWhenHasDynoShouldReturnHeroku() throws Exception { + Environment environment = new MockEnvironment().withProperty("DYNO", "---"); + CloudPlatform platform = CloudPlatform.getActive(environment); + assertThat(platform, equalTo(CloudPlatform.HEROKU)); + assertThat(platform.isActive(environment), equalTo(true)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/cloudfoundry/VcapEnvironmentPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/cloud/cloudfoundry/CloudFoundryVcapEnvironmentPostProcessorTests.java similarity index 93% rename from spring-boot/src/test/java/org/springframework/boot/cloudfoundry/VcapEnvironmentPostProcessorTests.java rename to spring-boot/src/test/java/org/springframework/boot/cloud/cloudfoundry/CloudFoundryVcapEnvironmentPostProcessorTests.java index ead065bf55..89cdb1b0f8 100644 --- a/spring-boot/src/test/java/org/springframework/boot/cloudfoundry/VcapEnvironmentPostProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/cloud/cloudfoundry/CloudFoundryVcapEnvironmentPostProcessorTests.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.cloudfoundry; +package org.springframework.boot.cloud.cloudfoundry; import org.junit.Test; +import org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -25,14 +26,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; /** - * Tests for {@link VcapEnvironmentPostProcessor}. + * Tests for {@link CloudFoundryVcapEnvironmentPostProcessor}. * * @author Dave Syer * @author Andy Wilkinson */ -public class VcapEnvironmentPostProcessorTests { +public class CloudFoundryVcapEnvironmentPostProcessorTests { - private final VcapEnvironmentPostProcessor initializer = new VcapEnvironmentPostProcessor(); + private final CloudFoundryVcapEnvironmentPostProcessor initializer = new CloudFoundryVcapEnvironmentPostProcessor(); private final ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); From d4c2959cee7381931323623afcb3da5f64fc9f9c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 1 Oct 2015 23:57:14 -0700 Subject: [PATCH 6/6] Document X-Forwarded-For support Closes gh-4018 --- .../appendix-application-properties.adoc | 1 + spring-boot-docs/src/main/asciidoc/howto.adoc | 113 ++++++++---------- 2 files changed, 54 insertions(+), 60 deletions(-) diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index edfe18b468..d9b8a2ca19 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -83,6 +83,7 @@ content into your application; rather pick only the properties that you need. server.jsp-servlet.registered=true # Whether or not the JSP servlet is registered server.servlet-path= # the servlet path, defaults to '/' server.display-name= # the display name of the application + server.use-forward-headers= # if X-Forwarded-* headers should be used (default is off unless running in a known cloud) server.session.persistent=false # true if session should be saved across restarts server.session.timeout= # session timeout in seconds server.session.tracking-modes= # tracking modes (one or more of "cookie" ,"url", "ssl") diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 7fd4d6e3bd..43222d9a9a 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -491,6 +491,59 @@ sample project for an example. +[[howto-use-behind-a-proxy-server]] +[[howto-use-tomcat-behind-a-proxy-server]] +=== Use behind a front-end proxy server +Your application might need to send `302` redirects or render content with absolute links +back to itself. When running behind a proxy, the caller wants a link to the proxy, and not +to the physical address of the machine hosting your app. Typically such situations are +handled via a contract with the proxy, which will add headers to tell the back end how to +construct links to itself. + +If the proxy adds conventional `X-Forwarded-For` and `X-Forwarded-Proto` headers (most do +this out of the box) the absolute links should be rendered correctly as long as +`server.use-forward-headers` is set to `true` in your `application.properties`. + +NOTE: If your application is running in Cloud Foundry or Heroku the +`server.use-forward-headers` property will default to `true` if not specified. In all +other instances it defaults to `false`. + + + +[[howto-customize-tomcat-behind-a-proxy-server]] +==== Customize Tomcat's proxy configuration +If you are using Tomcat you can additionally configure the names of the headers used to +carry "`forwarded`" information: + +[indent=0] +---- + server.tomcat.remote-ip-header=x-your-remote-ip-header + server.tomcat.protocol-header=x-your-protocol-header +---- + +Tomcat is also configured with a default regular expression that matches internal +proxies that are to be trusted. By default, IP addresses in `10/8`, `192.168/16`, +`169.254/16` and `127/8` are trusted. You can customize the valve's configuration by +adding an entry to `application.properties`, e.g. + +[indent=0] +---- + server.tomcat.internal-proxies=192\\.168\\.\\d{1,3}\\.\\d{1,3} +---- + +NOTE: The double backslashes are only required when you're using a properties file for +configuration. If you are using YAML, single backslashes are sufficient and a value +that's equivalent to the one shown above would be `192\.168\.\d{1,3}\.\d{1,3}`. + +NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but don't do +this in production). + +You can take complete control of the configuration of Tomcat's `RemoteIpValve` by +switching the automatic one off (i.e. set `server.use-forward-headers=false`) and adding +a new valve instance in a `TomcatEmbeddedServletContainerFactory` bean. + + + [[howto-configure-tomcat]] === Configure Tomcat Generally you can follow the advice from @@ -544,66 +597,6 @@ HTTPS connector: -[[howto-use-tomcat-behind-a-proxy-server]] -=== Use Tomcat behind a front-end proxy server -Your app might need to send 302 redirects, or render UI templates with -absolute links to itself, or hypermedia links back to itself in the -case of a RESTful service. If the app is behind a proxy, the caller -wants a link to the proxy not to the physical address of the app, so -something has to be done in the backend. Typically this is handled via -a contract with the proxy, which will add headers to tell the back end -how to construct links to itself. If the proxy adds conventional -headers (most do this out of the box) the absolute links should be -rendered correctly by default using the Tomcat server. - -Spring Boot using Tomcat automatically adds a `RemoteIpValve`. This -transparently takes the standard `x-forwarded-for` and -`x-forwarded-proto` headers and uses them to change local URLs created -in the `HttpServletRequest`. You can configure the header names in -Spring Boot and the valve is switched on unless one or both of these -properties is empty. These values are the defaults and are the -conventional values used by most proxies, so you don't need to set -them unless you need different values: - -[indent=0] ----- - server.tomcat.remote-ip-header=x-forwarded-for - server.tomcat.protocol-header=x-forwarded-proto ----- - -If your proxy uses different headers you can customize the valve's configuration by adding -some entries to `application.properties`, e.g. - -[indent=0] ----- - server.tomcat.remote-ip-header=x-your-remote-ip-header - server.tomcat.protocol-header=x-your-protocol-header ----- - -The valve is also configured with a default regular expression that matches internal -proxies that are to be trusted. By default, IP addresses in 10/8, 192.168/16, 169.254/16 -and 127/8 are trusted. You can customize the valve's configuration by adding an entry -to `application.properties`, e.g. - -[indent=0] ----- - server.tomcat.internal_proxies=192\\.168\\.\\d{1,3}\\.\\d{1,3} ----- - -NOTE: The double backslashes are only required when you're using a properties file for -configuration. If you are using YAML, single backslashes are sufficient and a value -that's equivalent to the one shown above would be `192\.168\.\d{1,3}\.\d{1,3}`. - -NOTE: You can trust all proxies by setting the `internal_proxies` to empty (but don't do -this in production). - -You can take complete control of the configuration of the -`RemoteIpValve` by switching the automatic one off (i.e. set one of -the headers to empty) and adding a new valve instance in a -`TomcatEmbeddedServletContainerFactory` bean. - - - [[howto-use-jetty-instead-of-tomcat]] === Use Jetty instead of Tomcat The Spring Boot starters (`spring-boot-starter-web` in particular) use Tomcat as an