Commit 959e1615 authored by Madhura Bhave's avatar Madhura Bhave

Provide an option to use Spring's forwarded header support

Previously, if the `server.use-forward-headers` property
was set to true, X-Forwarded-* headers support was provided
at the server level. The property has been deprecated in favor
of `server.forward-headers-strategy` which can be also be configured
to use Spring's forwarded header support apart from native server support.

Closes gh-5677
parent 33fecec4
...@@ -30,6 +30,7 @@ import java.util.Map; ...@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.convert.DurationUnit; import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Compression;
...@@ -79,6 +80,11 @@ public class ServerProperties { ...@@ -79,6 +80,11 @@ public class ServerProperties {
*/ */
private Boolean useForwardHeaders; private Boolean useForwardHeaders;
/**
* Strategy for handling X-Forwarded-* headers.
*/
private ForwardHeadersStrategy forwardHeadersStrategy = ForwardHeadersStrategy.NONE;
/** /**
* Value to use for the Server response header (if empty, no header is sent). * Value to use for the Server response header (if empty, no header is sent).
*/ */
...@@ -129,12 +135,18 @@ public class ServerProperties { ...@@ -129,12 +135,18 @@ public class ServerProperties {
this.address = address; this.address = address;
} }
@DeprecatedConfigurationProperty
public Boolean isUseForwardHeaders() { public Boolean isUseForwardHeaders() {
return this.useForwardHeaders; return ForwardHeadersStrategy.NATIVE.equals(this.forwardHeadersStrategy);
} }
public void setUseForwardHeaders(Boolean useForwardHeaders) { public void setUseForwardHeaders(Boolean useForwardHeaders) {
this.useForwardHeaders = useForwardHeaders; if (useForwardHeaders != null && useForwardHeaders) {
this.forwardHeadersStrategy = ForwardHeadersStrategy.NATIVE;
}
else {
this.forwardHeadersStrategy = ForwardHeadersStrategy.NONE;
}
} }
public String getServerHeader() { public String getServerHeader() {
...@@ -197,6 +209,14 @@ public class ServerProperties { ...@@ -197,6 +209,14 @@ public class ServerProperties {
return this.undertow; return this.undertow;
} }
public ForwardHeadersStrategy getForwardHeadersStrategy() {
return this.forwardHeadersStrategy;
}
public void setForwardHeadersStrategy(ForwardHeadersStrategy forwardHeadersStrategy) {
this.forwardHeadersStrategy = forwardHeadersStrategy;
}
/** /**
* Servlet properties. * Servlet properties.
*/ */
...@@ -1208,4 +1228,23 @@ public class ServerProperties { ...@@ -1208,4 +1228,23 @@ public class ServerProperties {
} }
public enum ForwardHeadersStrategy {
/**
* Use the underlying container's native support for forwarded headers.
*/
NATIVE,
/**
* Use Spring's support for handling forwarded headers.
*/
FRAMEWORK,
/**
* Ignore X-Forwarded-* headers.
*/
NONE
}
} }
...@@ -68,8 +68,7 @@ public class JettyWebServerFactoryCustomizer implements ...@@ -68,8 +68,7 @@ public class JettyWebServerFactoryCustomizer implements
public void customize(ConfigurableJettyWebServerFactory factory) { public void customize(ConfigurableJettyWebServerFactory factory) {
ServerProperties properties = this.serverProperties; ServerProperties properties = this.serverProperties;
ServerProperties.Jetty jettyProperties = properties.getJetty(); ServerProperties.Jetty jettyProperties = properties.getJetty();
factory.setUseForwardHeaders( factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders());
getOrDeduceUseForwardHeaders(properties, this.environment));
PropertyMapper propertyMapper = PropertyMapper.get(); PropertyMapper propertyMapper = PropertyMapper.get();
propertyMapper.from(jettyProperties::getAcceptors).whenNonNull() propertyMapper.from(jettyProperties::getAcceptors).whenNonNull()
.to(factory::setAcceptors); .to(factory::setAcceptors);
...@@ -95,13 +94,14 @@ public class JettyWebServerFactoryCustomizer implements ...@@ -95,13 +94,14 @@ public class JettyWebServerFactoryCustomizer implements
return value > 0; return value > 0;
} }
private boolean getOrDeduceUseForwardHeaders(ServerProperties serverProperties, private boolean getOrDeduceUseForwardHeaders() {
Environment environment) { if (this.serverProperties.getForwardHeadersStrategy()
if (serverProperties.isUseForwardHeaders() != null) { .equals(ServerProperties.ForwardHeadersStrategy.NONE)) {
return serverProperties.isUseForwardHeaders(); CloudPlatform platform = CloudPlatform.getActive(this.environment);
return platform != null && platform.isUsingForwardHeaders();
} }
CloudPlatform platform = CloudPlatform.getActive(environment); return this.serverProperties.getForwardHeadersStrategy()
return platform != null && platform.isUsingForwardHeaders(); .equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
} }
private void customizeConnectionTimeout(ConfigurableJettyWebServerFactory factory, private void customizeConnectionTimeout(ConfigurableJettyWebServerFactory factory,
......
...@@ -58,8 +58,7 @@ public class NettyWebServerFactoryCustomizer ...@@ -58,8 +58,7 @@ public class NettyWebServerFactoryCustomizer
@Override @Override
public void customize(NettyReactiveWebServerFactory factory) { public void customize(NettyReactiveWebServerFactory factory) {
factory.setUseForwardHeaders( factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders());
getOrDeduceUseForwardHeaders(this.serverProperties, this.environment));
PropertyMapper propertyMapper = PropertyMapper.get(); PropertyMapper propertyMapper = PropertyMapper.get();
propertyMapper.from(this.serverProperties::getMaxHttpHeaderSize).whenNonNull() propertyMapper.from(this.serverProperties::getMaxHttpHeaderSize).whenNonNull()
.asInt(DataSize::toBytes) .asInt(DataSize::toBytes)
...@@ -70,13 +69,14 @@ public class NettyWebServerFactoryCustomizer ...@@ -70,13 +69,14 @@ public class NettyWebServerFactoryCustomizer
.addServerCustomizers(getConnectionTimeOutCustomizer(duration))); .addServerCustomizers(getConnectionTimeOutCustomizer(duration)));
} }
private boolean getOrDeduceUseForwardHeaders(ServerProperties serverProperties, private boolean getOrDeduceUseForwardHeaders() {
Environment environment) { if (this.serverProperties.getForwardHeadersStrategy()
if (serverProperties.isUseForwardHeaders() != null) { .equals(ServerProperties.ForwardHeadersStrategy.NONE)) {
return serverProperties.isUseForwardHeaders(); CloudPlatform platform = CloudPlatform.getActive(this.environment);
return platform != null && platform.isUsingForwardHeaders();
} }
CloudPlatform platform = CloudPlatform.getActive(environment); return this.serverProperties.getForwardHeadersStrategy()
return platform != null && platform.isUsingForwardHeaders(); .equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
} }
private void customizeMaxHttpHeaderSize(NettyReactiveWebServerFactory factory, private void customizeMaxHttpHeaderSize(NettyReactiveWebServerFactory factory,
......
...@@ -188,11 +188,13 @@ public class TomcatWebServerFactoryCustomizer implements ...@@ -188,11 +188,13 @@ public class TomcatWebServerFactoryCustomizer implements
} }
private boolean getOrDeduceUseForwardHeaders() { private boolean getOrDeduceUseForwardHeaders() {
if (this.serverProperties.isUseForwardHeaders() != null) { if (this.serverProperties.getForwardHeadersStrategy()
return this.serverProperties.isUseForwardHeaders(); .equals(ServerProperties.ForwardHeadersStrategy.NONE)) {
CloudPlatform platform = CloudPlatform.getActive(this.environment);
return platform != null && platform.isUsingForwardHeaders();
} }
CloudPlatform platform = CloudPlatform.getActive(this.environment); return this.serverProperties.getForwardHeadersStrategy()
return platform != null && platform.isUsingForwardHeaders(); .equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
} }
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
......
...@@ -124,11 +124,13 @@ public class UndertowWebServerFactoryCustomizer implements ...@@ -124,11 +124,13 @@ public class UndertowWebServerFactoryCustomizer implements
} }
private boolean getOrDeduceUseForwardHeaders() { private boolean getOrDeduceUseForwardHeaders() {
if (this.serverProperties.isUseForwardHeaders() != null) { if (this.serverProperties.getForwardHeadersStrategy()
return this.serverProperties.isUseForwardHeaders(); .equals(ServerProperties.ForwardHeadersStrategy.NONE)) {
CloudPlatform platform = CloudPlatform.getActive(this.environment);
return platform != null && platform.isUsingForwardHeaders();
} }
CloudPlatform platform = CloudPlatform.getActive(this.environment); return this.serverProperties.getForwardHeadersStrategy()
return platform != null && platform.isUsingForwardHeaders(); .equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
} }
} }
...@@ -25,6 +25,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; ...@@ -25,6 +25,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
...@@ -37,6 +38,7 @@ import org.springframework.core.Ordered; ...@@ -37,6 +38,7 @@ import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for a reactive web server. * {@link EnableAutoConfiguration Auto-configuration} for a reactive web server.
...@@ -62,6 +64,13 @@ public class ReactiveWebServerFactoryAutoConfiguration { ...@@ -62,6 +64,13 @@ public class ReactiveWebServerFactoryAutoConfiguration {
return new ReactiveWebServerFactoryCustomizer(serverProperties); return new ReactiveWebServerFactoryCustomizer(serverProperties);
} }
@Bean
@ConditionalOnProperty(value = "server.forward-headers-strategy",
havingValue = "framework")
public ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
/** /**
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via * Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
* {@link ImportBeanDefinitionRegistrar} for early registration. * {@link ImportBeanDefinitionRegistrar} for early registration.
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure.web.servlet; package org.springframework.boot.autoconfigure.web.servlet;
import javax.servlet.DispatcherType;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
...@@ -27,12 +28,14 @@ import org.springframework.beans.factory.support.RootBeanDefinition; ...@@ -27,12 +28,14 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor; import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor;
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
...@@ -40,6 +43,7 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; ...@@ -40,6 +43,7 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.ForwardedHeaderFilter;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for servlet web servers. * {@link EnableAutoConfiguration Auto-configuration} for servlet web servers.
...@@ -74,6 +78,19 @@ public class ServletWebServerFactoryAutoConfiguration { ...@@ -74,6 +78,19 @@ public class ServletWebServerFactoryAutoConfiguration {
return new TomcatServletWebServerFactoryCustomizer(serverProperties); return new TomcatServletWebServerFactoryCustomizer(serverProperties);
} }
@Bean
@ConditionalOnProperty(value = "server.forward-headers-strategy",
havingValue = "framework")
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(
filter);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC,
DispatcherType.ERROR);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
/** /**
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via * Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
* {@link ImportBeanDefinitionRegistrar} for early registration. * {@link ImportBeanDefinitionRegistrar} for early registration.
......
...@@ -34,6 +34,7 @@ import org.springframework.context.ApplicationContextException; ...@@ -34,6 +34,7 @@ import org.springframework.context.ApplicationContextException;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -42,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -42,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* *
* @author Brian Clozel * @author Brian Clozel
* @author Raheela Aslam * @author Raheela Aslam
* @author Madhura Bhave
*/ */
public class ReactiveWebServerFactoryAutoConfigurationTests { public class ReactiveWebServerFactoryAutoConfigurationTests {
...@@ -147,6 +149,22 @@ public class ReactiveWebServerFactoryAutoConfigurationTests { ...@@ -147,6 +149,22 @@ public class ReactiveWebServerFactoryAutoConfigurationTests {
}); });
} }
@Test
public void forwardedHeaderTransformerShouldBeConfigured() {
this.contextRunner.withUserConfiguration(HttpHandlerConfiguration.class)
.withPropertyValues("server.forward-headers-strategy=framework")
.run((context) -> assertThat(context)
.hasSingleBean(ForwardedHeaderTransformer.class));
}
@Test
public void forwardedHeaderTransformerWhenStrategyNotFilterShouldNotBeConfigured() {
this.contextRunner.withUserConfiguration(HttpHandlerConfiguration.class)
.withPropertyValues("server.forward-headers-strategy=native")
.run((context) -> assertThat(context)
.doesNotHaveBean(ForwardedHeaderTransformer.class));
}
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
protected static class HttpHandlerConfiguration { protected static class HttpHandlerConfiguration {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure.web.servlet; package org.springframework.boot.autoconfigure.web.servlet;
import javax.servlet.Filter;
import javax.servlet.Servlet; import javax.servlet.Servlet;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
...@@ -35,6 +36,7 @@ import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; ...@@ -35,6 +36,7 @@ import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
...@@ -43,6 +45,7 @@ import org.springframework.context.ApplicationContext; ...@@ -43,6 +45,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.ForwardedHeaderFilter;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FrameworkServlet; import org.springframework.web.servlet.FrameworkServlet;
...@@ -56,6 +59,7 @@ import static org.mockito.Mockito.verify; ...@@ -56,6 +59,7 @@ import static org.mockito.Mockito.verify;
* @author Phillip Webb * @author Phillip Webb
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Raheela Aslam * @author Raheela Aslam
* @author Madhura Bhave
*/ */
public class ServletWebServerFactoryAutoConfigurationTests { public class ServletWebServerFactoryAutoConfigurationTests {
...@@ -186,6 +190,24 @@ public class ServletWebServerFactoryAutoConfigurationTests { ...@@ -186,6 +190,24 @@ public class ServletWebServerFactoryAutoConfigurationTests {
}); });
} }
@Test
public void forwardedHeaderFilterShouldBeConfigured() {
this.contextRunner.withPropertyValues("server.forward-headers-strategy=framework")
.run((context) -> {
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
Filter filter = context.getBean(FilterRegistrationBean.class)
.getFilter();
assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class);
});
}
@Test
public void forwardedHeaderFilterWhenStrategyNotFilterShouldNotBeConfigured() {
this.contextRunner.withPropertyValues("server.forward-headers-strategy=native")
.run((context) -> assertThat(context)
.doesNotHaveBean(FilterRegistrationBean.class));
}
private ContextConsumer<AssertableWebApplicationContext> verifyContext() { private ContextConsumer<AssertableWebApplicationContext> verifyContext() {
return this::verifyContext; return this::verifyContext;
} }
......
...@@ -969,11 +969,11 @@ construct links to itself. ...@@ -969,11 +969,11 @@ construct links to itself.
If the proxy adds conventional `X-Forwarded-For` and `X-Forwarded-Proto` headers (most If the proxy adds conventional `X-Forwarded-For` and `X-Forwarded-Proto` headers (most
proxy servers do so), the absolute links should be rendered correctly, provided proxy servers do so), the absolute links should be rendered correctly, provided
`server.use-forward-headers` is set to `true` in your `application.properties`. `server.forward-headers-strategy` is set to `NATIVE` or `FRAMEWORK` in your `application.properties`.
NOTE: If your application runs in Cloud Foundry or Heroku, the NOTE: If your application runs in Cloud Foundry or Heroku, the
`server.use-forward-headers` property defaults to `true`. In all `server.forward-headers-strategy` property defaults to `NATIVE`. In all
other instances, it defaults to `false`. other instances, it defaults to `NONE`.
...@@ -1006,7 +1006,7 @@ NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but ...@@ -1006,7 +1006,7 @@ NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but
so in production). so in production).
You can take complete control of the configuration of Tomcat's `RemoteIpValve` by You can take complete control of the configuration of Tomcat's `RemoteIpValve` by
switching the automatic one off (to do so, set `server.use-forward-headers=false`) and switching the automatic one off (to do so, set `server.forward-headers-strategy=NONE`) and
adding a new valve instance in a `TomcatServletWebServerFactory` bean. adding a new valve instance in a `TomcatServletWebServerFactory` bean.
......
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