Move web server auto-configure classes into spring-boot-web-server
This commit is contained in:
committed by
Phillip Webb
parent
dee54a8b99
commit
4471c0e644
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.web.ErrorProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.boot.convert.DurationUnit;
|
||||
import org.springframework.boot.web.server.Compression;
|
||||
import org.springframework.boot.web.server.Cookie;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.servlet.Jsp;
|
||||
import org.springframework.boot.web.server.servlet.Session;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties @ConfigurationProperties} for a web server (e.g. port
|
||||
* and path settings).
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @author Ivan Sopov
|
||||
* @author Marcos Barbero
|
||||
* @author Eddú Meléndez
|
||||
* @author Quinten De Swaef
|
||||
* @author Venil Noronha
|
||||
* @author Aurélien Leboulanger
|
||||
* @author Brian Clozel
|
||||
* @author Olivier Lamy
|
||||
* @author Chentao Qu
|
||||
* @author Artsiom Yudovin
|
||||
* @author Andrew McGhie
|
||||
* @author Rafiullah Hamedy
|
||||
* @author Dirk Deyne
|
||||
* @author HaiTao Zhang
|
||||
* @author Victor Mandujano
|
||||
* @author Chris Bono
|
||||
* @author Parviz Rozikov
|
||||
* @author Florian Storz
|
||||
* @author Michael Weidmann
|
||||
* @author Lasse Wulff
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ConfigurationProperties("server")
|
||||
public class ServerProperties {
|
||||
|
||||
/**
|
||||
* Server HTTP port.
|
||||
*/
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* Network address to which the server should bind.
|
||||
*/
|
||||
private InetAddress address;
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final ErrorProperties error = new ErrorProperties();
|
||||
|
||||
/**
|
||||
* Strategy for handling X-Forwarded-* headers.
|
||||
*/
|
||||
private ForwardHeadersStrategy forwardHeadersStrategy;
|
||||
|
||||
/**
|
||||
* Value to use for the Server response header (if empty, no header is sent).
|
||||
*/
|
||||
private String serverHeader;
|
||||
|
||||
/**
|
||||
* Maximum size of the HTTP request header. Refer to the documentation for your chosen
|
||||
* embedded server for details of exactly how this limit is applied. For example,
|
||||
* Netty applies the limit separately to each individual header in the request whereas
|
||||
* Tomcat applies the limit to the combined size of the request line and all of the
|
||||
* header names and values in the request.
|
||||
*/
|
||||
private DataSize maxHttpRequestHeaderSize = DataSize.ofKilobytes(8);
|
||||
|
||||
/**
|
||||
* Type of shutdown that the server will support.
|
||||
*/
|
||||
private Shutdown shutdown = Shutdown.GRACEFUL;
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private Ssl ssl;
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final Compression compression = new Compression();
|
||||
|
||||
/**
|
||||
* Custom MIME mappings in addition to the default MIME mappings.
|
||||
*/
|
||||
private final MimeMappings mimeMappings = new MimeMappings();
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final Http2 http2 = new Http2();
|
||||
|
||||
private final Servlet servlet = new Servlet();
|
||||
|
||||
private final Reactive reactive = new Reactive();
|
||||
|
||||
public Integer getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public void setPort(Integer port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public void setAddress(InetAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getServerHeader() {
|
||||
return this.serverHeader;
|
||||
}
|
||||
|
||||
public void setServerHeader(String serverHeader) {
|
||||
this.serverHeader = serverHeader;
|
||||
}
|
||||
|
||||
public DataSize getMaxHttpRequestHeaderSize() {
|
||||
return this.maxHttpRequestHeaderSize;
|
||||
}
|
||||
|
||||
public void setMaxHttpRequestHeaderSize(DataSize maxHttpRequestHeaderSize) {
|
||||
this.maxHttpRequestHeaderSize = maxHttpRequestHeaderSize;
|
||||
}
|
||||
|
||||
public Shutdown getShutdown() {
|
||||
return this.shutdown;
|
||||
}
|
||||
|
||||
public void setShutdown(Shutdown shutdown) {
|
||||
this.shutdown = shutdown;
|
||||
}
|
||||
|
||||
public ErrorProperties getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
public void setSsl(Ssl ssl) {
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
public Compression getCompression() {
|
||||
return this.compression;
|
||||
}
|
||||
|
||||
public MimeMappings getMimeMappings() {
|
||||
return this.mimeMappings;
|
||||
}
|
||||
|
||||
public void setMimeMappings(Map<String, String> customMappings) {
|
||||
customMappings.forEach(this.mimeMappings::add);
|
||||
}
|
||||
|
||||
public Http2 getHttp2() {
|
||||
return this.http2;
|
||||
}
|
||||
|
||||
public Servlet getServlet() {
|
||||
return this.servlet;
|
||||
}
|
||||
|
||||
public Reactive getReactive() {
|
||||
return this.reactive;
|
||||
}
|
||||
|
||||
public ForwardHeadersStrategy getForwardHeadersStrategy() {
|
||||
return this.forwardHeadersStrategy;
|
||||
}
|
||||
|
||||
public void setForwardHeadersStrategy(ForwardHeadersStrategy forwardHeadersStrategy) {
|
||||
this.forwardHeadersStrategy = forwardHeadersStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Servlet server properties.
|
||||
*/
|
||||
public static class Servlet {
|
||||
|
||||
/**
|
||||
* Servlet context init parameters.
|
||||
*/
|
||||
private final Map<String, String> contextParameters = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Context path of the application.
|
||||
*/
|
||||
private String contextPath;
|
||||
|
||||
/**
|
||||
* Display name of the application.
|
||||
*/
|
||||
private String applicationDisplayName = "application";
|
||||
|
||||
/**
|
||||
* Whether to register the default Servlet with the container.
|
||||
*/
|
||||
private boolean registerDefaultServlet = false;
|
||||
|
||||
private final Encoding encoding = new Encoding();
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final Jsp jsp = new Jsp();
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final Session session = new Session();
|
||||
|
||||
public String getContextPath() {
|
||||
return this.contextPath;
|
||||
}
|
||||
|
||||
public void setContextPath(String contextPath) {
|
||||
this.contextPath = cleanContextPath(contextPath);
|
||||
}
|
||||
|
||||
private String cleanContextPath(String contextPath) {
|
||||
String candidate = null;
|
||||
if (StringUtils.hasLength(contextPath)) {
|
||||
candidate = contextPath.strip();
|
||||
}
|
||||
if (StringUtils.hasText(candidate) && candidate.endsWith("/")) {
|
||||
return candidate.substring(0, candidate.length() - 1);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public String getApplicationDisplayName() {
|
||||
return this.applicationDisplayName;
|
||||
}
|
||||
|
||||
public void setApplicationDisplayName(String displayName) {
|
||||
this.applicationDisplayName = displayName;
|
||||
}
|
||||
|
||||
public boolean isRegisterDefaultServlet() {
|
||||
return this.registerDefaultServlet;
|
||||
}
|
||||
|
||||
public void setRegisterDefaultServlet(boolean registerDefaultServlet) {
|
||||
this.registerDefaultServlet = registerDefaultServlet;
|
||||
}
|
||||
|
||||
public Map<String, String> getContextParameters() {
|
||||
return this.contextParameters;
|
||||
}
|
||||
|
||||
public Encoding getEncoding() {
|
||||
return this.encoding;
|
||||
}
|
||||
|
||||
public Jsp getJsp() {
|
||||
return this.jsp;
|
||||
}
|
||||
|
||||
public Session getSession() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactive server properties.
|
||||
*/
|
||||
public static class Reactive {
|
||||
|
||||
private final Session session = new Session();
|
||||
|
||||
public Session getSession() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
public static class Session {
|
||||
|
||||
/**
|
||||
* Session timeout. If a duration suffix is not specified, seconds will be
|
||||
* used.
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private Duration timeout = Duration.ofMinutes(30);
|
||||
|
||||
/**
|
||||
* Maximum number of sessions that can be stored.
|
||||
*/
|
||||
private int maxSessions = 10000;
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final Cookie cookie = new Cookie();
|
||||
|
||||
public Duration getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public int getMaxSessions() {
|
||||
return this.maxSessions;
|
||||
}
|
||||
|
||||
public void setMaxSessions(int maxSessions) {
|
||||
this.maxSessions = maxSessions;
|
||||
}
|
||||
|
||||
public Cookie getCookie() {
|
||||
return this.cookie;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategies for supporting forward headers.
|
||||
*/
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
public static class Encoding {
|
||||
|
||||
/**
|
||||
* Mapping of locale to charset for response encoding.
|
||||
*/
|
||||
private Map<Locale, Charset> mapping;
|
||||
|
||||
public Map<Locale, Charset> getMapping() {
|
||||
return this.mapping;
|
||||
}
|
||||
|
||||
public void setMapping(Map<Locale, Charset> mapping) {
|
||||
this.mapping = mapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classes related to the auto-configuration of a web server.
|
||||
*/
|
||||
package org.springframework.boot.web.server.autoconfigure;
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.reactive;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
|
||||
/**
|
||||
* {@link Configuration Configuration} for a reactive web server.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(ServerProperties.class)
|
||||
@Import(ReactiveWebServerConfiguration.BeanPostProcessorsRegistrar.class)
|
||||
public class ReactiveWebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
public ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
ObjectProvider<SslBundles> sslBundles) {
|
||||
return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
|
||||
public ForwardedHeaderTransformer forwardedHeaderTransformer() {
|
||||
return new ForwardedHeaderTransformer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
|
||||
* {@link ImportBeanDefinitionRegistrar} for early registration.
|
||||
*/
|
||||
public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
|
||||
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
if (beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory) {
|
||||
this.beanFactory = listableBeanFactory;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
|
||||
BeanDefinitionRegistry registry) {
|
||||
if (this.beanFactory == null) {
|
||||
return;
|
||||
}
|
||||
registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
|
||||
WebServerFactoryCustomizerBeanPostProcessor.class);
|
||||
}
|
||||
|
||||
private <T> void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name,
|
||||
Class<T> beanClass) {
|
||||
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
|
||||
beanDefinition.setSynthetic(true);
|
||||
registry.registerBeanDefinition(name, beanDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.reactive;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
/**
|
||||
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} to reactive
|
||||
* servers.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Yunkun Huang
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class ReactiveWebServerFactoryCustomizer
|
||||
implements WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory>, Ordered {
|
||||
|
||||
private final ServerProperties serverProperties;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveWebServerFactoryCustomizer} instance.
|
||||
* @param serverProperties the server properties
|
||||
*/
|
||||
public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties) {
|
||||
this(serverProperties, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveWebServerFactoryCustomizer} instance.
|
||||
* @param serverProperties the server properties
|
||||
* @param sslBundles the SSL bundles
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties, SslBundles sslBundles) {
|
||||
this.serverProperties = serverProperties;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(ConfigurableReactiveWebServerFactory factory) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.serverProperties::getPort).to(factory::setPort);
|
||||
map.from(this.serverProperties::getAddress).to(factory::setAddress);
|
||||
map.from(this.serverProperties::getSsl).to(factory::setSsl);
|
||||
map.from(this.serverProperties::getCompression).to(factory::setCompression);
|
||||
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
|
||||
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
|
||||
map.from(() -> this.sslBundles).to(factory::setSslBundles);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classes related to the auto-configuration of a reactive web server.
|
||||
*/
|
||||
package org.springframework.boot.web.server.autoconfigure.reactive;
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.servlet;
|
||||
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
/**
|
||||
* Customizer for the auto-configured {@link ForwardedHeaderFilter}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public interface ForwardedHeaderFilterCustomizer {
|
||||
|
||||
/**
|
||||
* Customizes the given {@code filter}.
|
||||
* @param filter the filter to customize
|
||||
*/
|
||||
void customize(ForwardedHeaderFilter filter);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.servlet;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.error.ErrorPageRegistrarBeanPostProcessor;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
/**
|
||||
* {@link Configuration Configuration} for a servlet web server.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Ivan Sopov
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(ServerProperties.class)
|
||||
@Import(ServletWebServerConfiguration.BeanPostProcessorsRegistrar.class)
|
||||
public class ServletWebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
ObjectProvider<WebListenerRegistrar> webListenerRegistrars,
|
||||
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers, ObjectProvider<SslBundles> sslBundles) {
|
||||
return new ServletWebServerFactoryCustomizer(serverProperties, webListenerRegistrars.orderedStream().toList(),
|
||||
cookieSameSiteSuppliers.orderedStream().toList(), sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
|
||||
@ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class)
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter(
|
||||
ObjectProvider<ForwardedHeaderFilterCustomizer> customizerProvider) {
|
||||
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
|
||||
customizerProvider.ifAvailable((customizer) -> customizer.customize(filter));
|
||||
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
|
||||
* {@link ImportBeanDefinitionRegistrar} for early registration.
|
||||
*/
|
||||
static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
|
||||
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
if (beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory) {
|
||||
this.beanFactory = listableBeanFactory;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
|
||||
BeanDefinitionRegistry registry) {
|
||||
if (this.beanFactory == null) {
|
||||
return;
|
||||
}
|
||||
registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
|
||||
WebServerFactoryCustomizerBeanPostProcessor.class);
|
||||
registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor",
|
||||
ErrorPageRegistrarBeanPostProcessor.class);
|
||||
}
|
||||
|
||||
private <T> void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name,
|
||||
Class<T> beanClass) {
|
||||
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
|
||||
beanDefinition.setSynthetic(true);
|
||||
registry.registerBeanDefinition(name, beanDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.servlet;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} and
|
||||
* {@link WebListenerRegistrar WebListenerRegistrars} to servlet web servers.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
* @author Olivier Lamy
|
||||
* @author Yunkun Huang
|
||||
* @author Scott Frederick
|
||||
* @author Lasse Wulff
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class ServletWebServerFactoryCustomizer
|
||||
implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
|
||||
|
||||
private final ServerProperties serverProperties;
|
||||
|
||||
private final List<WebListenerRegistrar> webListenerRegistrars;
|
||||
|
||||
private final List<CookieSameSiteSupplier> cookieSameSiteSuppliers;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
|
||||
this(serverProperties, Collections.emptyList(), Collections.emptyList(), null);
|
||||
}
|
||||
|
||||
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> cookieSameSiteSuppliers,
|
||||
SslBundles sslBundles) {
|
||||
this.serverProperties = serverProperties;
|
||||
this.webListenerRegistrars = webListenerRegistrars;
|
||||
this.cookieSameSiteSuppliers = cookieSameSiteSuppliers;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(ConfigurableServletWebServerFactory factory) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.serverProperties::getPort).to(factory::setPort);
|
||||
map.from(this.serverProperties::getAddress).to(factory::setAddress);
|
||||
map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath);
|
||||
map.from(this.serverProperties.getServlet()::getApplicationDisplayName).to(factory::setDisplayName);
|
||||
map.from(this.serverProperties.getServlet()::isRegisterDefaultServlet).to(factory::setRegisterDefaultServlet);
|
||||
map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
|
||||
map.from(this.serverProperties::getSsl).to(factory::setSsl);
|
||||
map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
|
||||
map.from(this.serverProperties::getCompression).to(factory::setCompression);
|
||||
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
|
||||
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
|
||||
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
|
||||
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
|
||||
map.from(() -> this.sslBundles).to(factory::setSslBundles);
|
||||
map.from(() -> this.cookieSameSiteSuppliers)
|
||||
.whenNot(CollectionUtils::isEmpty)
|
||||
.to(factory::setCookieSameSiteSuppliers);
|
||||
map.from(this.serverProperties::getMimeMappings).to(factory::addMimeMappings);
|
||||
map.from(this.serverProperties.getServlet().getEncoding()::getMapping).to(factory::setLocaleCharsetMappings);
|
||||
this.webListenerRegistrars.forEach((registrar) -> registrar.register(factory));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classes related to the auto-configuration of a servlet web server.
|
||||
*/
|
||||
package org.springframework.boot.web.server.autoconfigure.servlet;
|
||||
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"name": "server.connection-timeout",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"reason": "Each server behaves differently. Use server specific properties instead.",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.error.include-binding-errors",
|
||||
"description": "When to include \"errors\" attribute.",
|
||||
"defaultValue": "never"
|
||||
},
|
||||
{
|
||||
"name": "server.error.include-exception",
|
||||
"description": "Include the \"exception\" attribute.",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "server.error.include-message",
|
||||
"description": "When to include the \"message\" attribute.",
|
||||
"defaultValue": "never"
|
||||
},
|
||||
{
|
||||
"name": "server.error.include-path",
|
||||
"description": "When to include the \"path\" attribute.",
|
||||
"defaultValue": "always"
|
||||
},
|
||||
{
|
||||
"name": "server.error.include-stacktrace",
|
||||
"description": "When to include the \"trace\" attribute.",
|
||||
"defaultValue": "never"
|
||||
},
|
||||
{
|
||||
"name": "server.error.path",
|
||||
"description": "Path of the error controller",
|
||||
"defaultValue": "/error"
|
||||
},
|
||||
{
|
||||
"name": "server.error.whitelabel.enabled",
|
||||
"description": "Whether to enable the default error page displayed in browsers in case of a server error.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "server.max-http-header-size",
|
||||
"deprecation": {
|
||||
"replacement": "server.max-http-request-header-size",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.max-http-post-size",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "Maximum size in bytes of the HTTP post content.",
|
||||
"defaultValue": 0,
|
||||
"deprecation": {
|
||||
"reason": "Use dedicated property for each container.",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.port",
|
||||
"defaultValue": 8080
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.domain",
|
||||
"description": "Domain for the cookie."
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.http-only",
|
||||
"description": "Whether to use \"HttpOnly\" cookies for the cookie."
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.max-age",
|
||||
"description": "Maximum age of the cookie. If a duration suffix is not specified, seconds will be used. A positive value indicates when the cookie expires relative to the current time. A value of 0 means the cookie should expire immediately. A negative value means no \"Max-Age\"."
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.name",
|
||||
"description": "Name for the cookie."
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.partitioned",
|
||||
"description": "Whether the generated cookie carries the Partitioned attribute."
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.path",
|
||||
"description": "Path of the cookie."
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.same-site",
|
||||
"description": "SameSite setting for the cookie."
|
||||
},
|
||||
{
|
||||
"name": "server.reactive.session.cookie.secure",
|
||||
"description": "Whether to always mark the cookie as secure."
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.encoding.charset",
|
||||
"type": "java.nio.charset.Charset",
|
||||
"description": "Charset of HTTP requests and responses. Added to the Content-Type header if not set explicitly.",
|
||||
"deprecation": {
|
||||
"replacement": "spring.servlet.encoding.charset",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.encoding.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable http encoding support.",
|
||||
"defaultValue": true,
|
||||
"deprecation": {
|
||||
"replacement": "spring.servlet.encoding.enabled",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.encoding.force",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to force the encoding to the configured charset on HTTP requests and responses.",
|
||||
"defaultValue": false,
|
||||
"deprecation": {
|
||||
"replacement": "spring.servlet.encoding.force",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.encoding.force-request",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to force the encoding to the configured charset on HTTP requests. Defaults to true when force has not been specified.",
|
||||
"defaultValue": true,
|
||||
"deprecation": {
|
||||
"replacement": "spring.servlet.encoding.force-request",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.encoding.force-response",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to force the encoding to the configured charset on HTTP responses.",
|
||||
"defaultValue": false,
|
||||
"deprecation": {
|
||||
"replacement": "spring.servlet.encoding.force-response",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.jsp.class-name",
|
||||
"description": "Class name of the servlet to use for JSPs. If registered is true and this class\n\t * is on the classpath then it will be registered.",
|
||||
"defaultValue": "org.apache.jasper.servlet.JspServlet"
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.jsp.init-parameters",
|
||||
"description": "Init parameters used to configure the JSP servlet."
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.path",
|
||||
"type": "java.lang.String",
|
||||
"description": "Path of the main dispatcher servlet.",
|
||||
"defaultValue": "/",
|
||||
"deprecation": {
|
||||
"replacement": "spring.mvc.servlet.path",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.servlet.session.cookie.comment",
|
||||
"description": "Comment for the cookie.",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.protocol",
|
||||
"description": "SSL protocol to use.",
|
||||
"defaultValue": "TLS"
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.server-name-bundles",
|
||||
"description": "Mapping of host names to SSL bundles for SNI configuration."
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.trust-certificate",
|
||||
"description": "Path to a PEM-encoded SSL certificate authority file."
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.trust-certificate-private-key",
|
||||
"description": "Path to a PEM-encoded private key file for the SSL certificate authority."
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.trust-store",
|
||||
"description": "Trust store that holds SSL certificates."
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.trust-store-password",
|
||||
"description": "Password used to access the trust store."
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.trust-store-provider",
|
||||
"description": "Provider for the trust store."
|
||||
},
|
||||
{
|
||||
"name": "server.ssl.trust-store-type",
|
||||
"description": "Type of the trust store."
|
||||
},
|
||||
{
|
||||
"name": "server.use-forward-headers",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"reason": "Replaced to support additional strategies.",
|
||||
"replacement": "server.forward-headers-strategy",
|
||||
"level": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
||||
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.MimeMappings.Mapping;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServerProperties}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Eddú Meléndez
|
||||
* @author Quinten De Swaef
|
||||
* @author Venil Noronha
|
||||
* @author Andrew McGhie
|
||||
* @author HaiTao Zhang
|
||||
* @author Rafiullah Hamedy
|
||||
* @author Chris Bono
|
||||
* @author Parviz Rozikov
|
||||
* @author Lasse Wulff
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ServerPropertiesTests {
|
||||
|
||||
private final ServerProperties properties = new ServerProperties();
|
||||
|
||||
@Test
|
||||
void testAddressBinding() throws Exception {
|
||||
bind("server.address", "127.0.0.1");
|
||||
assertThat(this.properties.getAddress()).isEqualTo(InetAddress.getByName("127.0.0.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPortBinding() {
|
||||
bind("server.port", "9000");
|
||||
assertThat(this.properties.getPort().intValue()).isEqualTo(9000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testServerHeaderDefault() {
|
||||
assertThat(this.properties.getServerHeader()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testServerHeader() {
|
||||
bind("server.server-header", "Custom Server");
|
||||
assertThat(this.properties.getServerHeader()).isEqualTo("Custom Server");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTrailingSlashOfContextPathIsRemoved() {
|
||||
bind("server.servlet.context-path", "/foo/");
|
||||
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSlashOfContextPathIsDefaultValue() {
|
||||
bind("server.servlet.context-path", "/");
|
||||
assertThat(this.properties.getServlet().getContextPath()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContextPathWithLeadingWhitespace() {
|
||||
bind("server.servlet.context-path", " /assets");
|
||||
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContextPathWithTrailingWhitespace() {
|
||||
bind("server.servlet.context-path", "/assets/copy/ ");
|
||||
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets/copy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContextPathWithLeadingAndTrailingWhitespace() {
|
||||
bind("server.servlet.context-path", " /assets ");
|
||||
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContextPathWithLeadingAndTrailingWhitespaceAndContextWithSpace() {
|
||||
bind("server.servlet.context-path", " /assets /copy/ ");
|
||||
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets /copy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultMimeMapping() {
|
||||
assertThat(this.properties.getMimeMappings()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizedMimeMapping() {
|
||||
MimeMappings expectedMappings = new MimeMappings();
|
||||
expectedMappings.add("mjs", "text/javascript");
|
||||
bind("server.mime-mappings.mjs", "text/javascript");
|
||||
assertThat(this.properties.getMimeMappings())
|
||||
.containsExactly(expectedMappings.getAll().toArray(new Mapping[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeMaxHttpRequestHeaderSize() {
|
||||
bind("server.max-http-request-header-size", "1MB");
|
||||
assertThat(this.properties.getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofMegabytes(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeMaxHttpRequestHeaderSizeUseBytesByDefault() {
|
||||
bind("server.max-http-request-header-size", "1024");
|
||||
assertThat(this.properties.getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofKilobytes(1));
|
||||
}
|
||||
|
||||
private void bind(String name, String value) {
|
||||
bind(Collections.singletonMap(name, value));
|
||||
}
|
||||
|
||||
private void bind(Map<String, String> map) {
|
||||
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
|
||||
new Binder(source).bind("server", Bindable.ofInstance(this.properties));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.reactive;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.assertArg;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactiveWebServerFactoryCustomizer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Yunkun Huang
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class ReactiveWebServerFactoryCustomizerTests {
|
||||
|
||||
private final ServerProperties properties = new ServerProperties();
|
||||
|
||||
private final SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
|
||||
private ReactiveWebServerFactoryCustomizer customizer;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.customizer = new ReactiveWebServerFactoryCustomizer(this.properties, this.sslBundles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeServerPort() {
|
||||
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
|
||||
this.properties.setPort(9000);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setPort(9000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeServerAddress() {
|
||||
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
|
||||
InetAddress address = InetAddress.getLoopbackAddress();
|
||||
this.properties.setAddress(address);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setAddress(address);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeServerSsl() {
|
||||
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
|
||||
Ssl ssl = mock(Ssl.class);
|
||||
this.properties.setSsl(ssl);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setSsl(ssl);
|
||||
then(factory).should().setSslBundles(this.sslBundles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShutdownPropertyIsSetThenShutdownIsCustomized() {
|
||||
this.properties.setShutdown(Shutdown.GRACEFUL);
|
||||
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setShutdown(assertArg((shutdown) -> assertThat(shutdown).isEqualTo(Shutdown.GRACEFUL)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.servlet;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
||||
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||
import org.springframework.boot.web.server.Cookie;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.Jsp;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.assertArg;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServletWebServerFactoryCustomizer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Yunkun Huang
|
||||
* @author Lasse Wulff
|
||||
*/
|
||||
class ServletWebServerFactoryCustomizerTests {
|
||||
|
||||
private final ServerProperties properties = new ServerProperties();
|
||||
|
||||
private ServletWebServerFactoryCustomizer customizer;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.customizer = new ServletWebServerFactoryCustomizer(this.properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultDisplayName() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setDisplayName("application");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeDisplayName() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.properties.getServlet().setApplicationDisplayName("TestName");
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setDisplayName("TestName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withNoCustomMimeMappingsThenEmptyMimeMappingsIsAdded() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
ArgumentCaptor<MimeMappings> mimeMappingsCaptor = ArgumentCaptor.forClass(MimeMappings.class);
|
||||
then(factory).should().addMimeMappings(mimeMappingsCaptor.capture());
|
||||
MimeMappings mimeMappings = mimeMappingsCaptor.getValue();
|
||||
assertThat(mimeMappings.getAll()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomMimeMappingsThenPopulatedMimeMappingsIsAdded() {
|
||||
this.properties.getMimeMappings().add("a", "alpha");
|
||||
this.properties.getMimeMappings().add("b", "bravo");
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
ArgumentCaptor<MimeMappings> mimeMappingsCaptor = ArgumentCaptor.forClass(MimeMappings.class);
|
||||
then(factory).should().addMimeMappings(mimeMappingsCaptor.capture());
|
||||
MimeMappings mimeMappings = mimeMappingsCaptor.getValue();
|
||||
assertThat(mimeMappings.getAll()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeDefaultServlet() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.properties.getServlet().setRegisterDefaultServlet(false);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setRegisterDefaultServlet(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeSsl() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
Ssl ssl = mock(Ssl.class);
|
||||
this.properties.setSsl(ssl);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setSsl(ssl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeJsp() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setJsp(any(Jsp.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeSessionProperties() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("server.servlet.session.timeout", "123");
|
||||
map.put("server.servlet.session.tracking-modes", "cookie,url");
|
||||
map.put("server.servlet.session.cookie.name", "testname");
|
||||
map.put("server.servlet.session.cookie.domain", "testdomain");
|
||||
map.put("server.servlet.session.cookie.path", "/testpath");
|
||||
map.put("server.servlet.session.cookie.http-only", "true");
|
||||
map.put("server.servlet.session.cookie.secure", "true");
|
||||
map.put("server.servlet.session.cookie.max-age", "60");
|
||||
bindProperties(map);
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setSession(assertArg((session) -> {
|
||||
assertThat(session.getTimeout()).hasSeconds(123);
|
||||
Cookie cookie = session.getCookie();
|
||||
assertThat(cookie.getName()).isEqualTo("testname");
|
||||
assertThat(cookie.getDomain()).isEqualTo("testdomain");
|
||||
assertThat(cookie.getPath()).isEqualTo("/testpath");
|
||||
assertThat(cookie.getHttpOnly()).isTrue();
|
||||
assertThat(cookie.getMaxAge()).hasSeconds(60);
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeTomcatPort() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.properties.setPort(8080);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setPort(8080);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeServletDisplayName() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("server.servlet.application-display-name", "MyBootApp");
|
||||
bindProperties(map);
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setDisplayName("MyBootApp");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionStoreDir() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("server.servlet.session.store-dir", "mydirectory");
|
||||
bindProperties(map);
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should()
|
||||
.setSession(assertArg((session) -> assertThat(session.getStoreDir()).isEqualTo(new File("mydirectory"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShutdownPropertyIsSetThenShutdownIsCustomized() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("server.shutdown", "immediate");
|
||||
bindProperties(map);
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setShutdown(assertArg((shutdown) -> assertThat(shutdown).isEqualTo(Shutdown.IMMEDIATE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noLocaleCharsetMapping() {
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should(never()).setLocaleCharsetMappings(anyMap());
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLocaleCharsetMappings() {
|
||||
Map<String, String> map = Map.of("server.servlet.encoding.mapping.en", "UTF-8",
|
||||
"server.servlet.encoding.mapping.fr_FR", "UTF-8");
|
||||
bindProperties(map);
|
||||
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should()
|
||||
.setLocaleCharsetMappings((assertArg((mappings) -> assertThat(mappings).hasSize(2)
|
||||
.containsEntry(Locale.ENGLISH, StandardCharsets.UTF_8)
|
||||
.containsEntry(Locale.FRANCE, StandardCharsets.UTF_8))));
|
||||
}
|
||||
|
||||
private void bindProperties(Map<String, String> map) {
|
||||
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
|
||||
new Binder(source).bind("server", Bindable.ofInstance(this.properties));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.reactive;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.NoSuchSslBundleException;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Base class for testing sub-classes of {@link ReactiveWebServerConfiguration}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Raheela Aslam
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
// @DirtiesUrlFactories
|
||||
public abstract class AbstractReactiveWebServerAutoConfigurationTests {
|
||||
|
||||
private final ReactiveWebApplicationContextRunner mockServerRunner;
|
||||
|
||||
protected final ReactiveWebApplicationContextRunner serverRunner;
|
||||
|
||||
protected AbstractReactiveWebServerAutoConfigurationTests(Class<?> serverAutoConfiguration) {
|
||||
ReactiveWebApplicationContextRunner common = new ReactiveWebApplicationContextRunner(
|
||||
AnnotationConfigReactiveWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(serverAutoConfiguration));
|
||||
this.serverRunner = common.withPropertyValues("server.port=0")
|
||||
.withUserConfiguration(HttpHandlerConfiguration.class);
|
||||
this.mockServerRunner = common.withUserConfiguration(MockWebServerConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromConfigClass() {
|
||||
this.mockServerRunner.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class)
|
||||
.run((context) -> {
|
||||
assertThat(context.getBeansOfType(ReactiveWebServerFactory.class)).hasSize(1);
|
||||
assertThat(context.getBeansOfType(WebServerFactoryCustomizer.class)).hasSizeGreaterThanOrEqualTo(1);
|
||||
assertThat(context.getBeansOfType(ReactiveWebServerFactoryCustomizer.class)).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingHttpHandler() {
|
||||
this.mockServerRunner.withUserConfiguration(MockWebServerConfiguration.class)
|
||||
.run((context) -> assertThat(context.getStartupFailure()).isInstanceOf(ApplicationContextException.class)
|
||||
.rootCause()
|
||||
.hasMessageContaining("missing HttpHandler bean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleHttpHandler() {
|
||||
this.mockServerRunner
|
||||
.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class,
|
||||
TooManyHttpHandlers.class)
|
||||
.run((context) -> assertThat(context.getStartupFailure()).isInstanceOf(ApplicationContextException.class)
|
||||
.rootCause()
|
||||
.hasMessageContaining("multiple HttpHandler beans : httpHandler,additionalHttpHandler"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeReactiveWebServer() {
|
||||
this.mockServerRunner
|
||||
.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class,
|
||||
ReactiveWebServerCustomization.class)
|
||||
.run((context) -> assertThat(context.getBean(MockReactiveWebServerFactory.class).getPort())
|
||||
.isEqualTo(9000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webServerFailsWithInvalidSslBundle() {
|
||||
this.serverRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withBean(WebServerFactoryCustomizer.class,
|
||||
() -> (WebServerFactoryCustomizer<ConfigurableWebServerFactory>) (factory) -> factory
|
||||
.setSslBundles(new DefaultSslBundleRegistry()))
|
||||
.withPropertyValues("server.ssl.bundle=test-bundle")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure().getCause()).isInstanceOf(NoSuchSslBundleException.class)
|
||||
.withFailMessage("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerShouldBeConfigured() {
|
||||
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerWhenStrategyNotFilterShouldNotBeConfigured() {
|
||||
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=native", "server.port=0")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerWhenAlreadyRegisteredShouldBackOff() {
|
||||
this.mockServerRunner
|
||||
.withUserConfiguration(ForwardedHeaderTransformerConfiguration.class, HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HttpHandlerConfiguration {
|
||||
|
||||
@Bean
|
||||
HttpHandler httpHandler() {
|
||||
return mock(HttpHandler.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TooManyHttpHandlers {
|
||||
|
||||
@Bean
|
||||
HttpHandler additionalHttpHandler() {
|
||||
return mock(HttpHandler.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ReactiveWebServerCustomization {
|
||||
|
||||
@Bean
|
||||
WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory> reactiveWebServerCustomizer() {
|
||||
return (factory) -> factory.setPort(9000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class MockWebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
MockReactiveWebServerFactory mockReactiveWebServerFactory() {
|
||||
return new MockReactiveWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderTransformerConfiguration {
|
||||
|
||||
@Bean
|
||||
ForwardedHeaderTransformer testForwardedHeaderTransformer() {
|
||||
return new ForwardedHeaderTransformer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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
|
||||
*
|
||||
* https://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.web.server.autoconfigure.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.websocket.server.ServerContainer;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.server.context.WebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Base class for testing sub-classes of {@link ServletWebServerConfiguration}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Raheela Aslam
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
public abstract class AbstractServletWebServerAutoConfigurationTests {
|
||||
|
||||
private final WebApplicationContextRunner mockServerRunner;
|
||||
|
||||
protected final WebApplicationContextRunner serverRunner;
|
||||
|
||||
protected AbstractServletWebServerAutoConfigurationTests(Class<?> serverAutoConfiguration) {
|
||||
WebApplicationContextRunner common = new WebApplicationContextRunner(
|
||||
AnnotationConfigServletWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(serverAutoConfiguration));
|
||||
this.serverRunner = common.withPropertyValues("server.port=0");
|
||||
this.mockServerRunner = common.withUserConfiguration(MockWebServerConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromConfigClass() {
|
||||
this.mockServerRunner
|
||||
.run((context) -> assertThat(context).hasSingleBean(ServletWebServerFactoryCustomizer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webServerHasNoServletContext() {
|
||||
this.mockServerRunner.withUserConfiguration(EnsureWebServerHasNoServletContext.class)
|
||||
.run((context) -> assertThat(context).hasNotFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void webServerFactoryCustomizerBeansAreCalledToCustomizeWebServerFactory() {
|
||||
this.mockServerRunner
|
||||
.withBean(WebServerFactoryCustomizer.class,
|
||||
() -> (WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>) (factory) -> factory
|
||||
.setPort(9000))
|
||||
.run((context) -> assertThat(context.getBean(MockServletWebServerFactory.class).getPort()).isEqualTo(9000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void initParametersAreConfiguredOnTheServletContext() {
|
||||
this.mockServerRunner
|
||||
.withPropertyValues("server.servlet.context-parameters.a:alpha",
|
||||
"server.servlet.context-parameters.b:bravo")
|
||||
.run((context) -> {
|
||||
ServletContext servletContext = context.getServletContext();
|
||||
assertThat(servletContext.getInitParameter("a")).isEqualTo("alpha");
|
||||
assertThat(servletContext.getInitParameter("b")).isEqualTo("bravo");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterShouldBeConfigured() {
|
||||
this.mockServerRunner.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);
|
||||
assertThat(filter).extracting("relativeRedirects").isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterWhenStrategyNotFilterShouldNotBeConfigured() {
|
||||
this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=native")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterWhenFilterAlreadyRegisteredShouldBackOff() {
|
||||
this.mockServerRunner.withUserConfiguration(ForwardedHeaderFilterConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework")
|
||||
.run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieSameSiteSuppliersAreApplied() {
|
||||
this.mockServerRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> {
|
||||
ConfigurableServletWebServerFactory webServerFactory = context
|
||||
.getBean(ConfigurableServletWebServerFactory.class);
|
||||
assertThat(webServerFactory.getSettings().getCookieSameSiteSuppliers()).hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void webSocketServerContainerIsAvailableFromServletContext() {
|
||||
this.serverRunner.run((context) -> {
|
||||
Object serverContainer = context.getServletContext()
|
||||
.getAttribute("jakarta.websocket.server.ServerContainer");
|
||||
assertThat(serverContainer).isInstanceOf(ServerContainer.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void webSocketUpgradeDoesNotPreventAFilterFromRejectingTheRequest() {
|
||||
this.serverRunner
|
||||
.withBean("testEndpointRegistrar", ServletContextInitializer.class, () -> TestEndpoint::register)
|
||||
.withUserConfiguration(UnauthorizedFilterConfiguration.class)
|
||||
.run((context) -> {
|
||||
TestEndpoint.register(context.getServletContext());
|
||||
WebServer webServer = ((WebServerApplicationContext) context.getSourceApplicationContext())
|
||||
.getWebServer();
|
||||
int port = webServer.getPort();
|
||||
RestTemplate rest = new RestTemplate();
|
||||
RequestEntity<Void> request = RequestEntity.get("http://localhost:" + port)
|
||||
.header("Upgrade", "websocket")
|
||||
.header("Connection", "upgrade")
|
||||
.header("Sec-WebSocket-Version", "13")
|
||||
.header("Sec-WebSocket-Key", "key")
|
||||
.build();
|
||||
assertThatExceptionOfType(HttpClientErrorException.Unauthorized.class)
|
||||
.isThrownBy(() -> rest.exchange(request, Void.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnExpression("true")
|
||||
static class MockWebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ServletWebServerFactory webServerFactory() {
|
||||
return new MockServletWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
static class EnsureWebServerHasNoServletContext implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) {
|
||||
if (bean instanceof ConfigurableServletWebServerFactory) {
|
||||
MockServletWebServerFactory webServerFactory = (MockServletWebServerFactory) bean;
|
||||
assertThat(webServerFactory.getServletContext()).isNull();
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() {
|
||||
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
|
||||
return new FilterRegistrationBean<>(filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CookieSameSiteSupplierConfiguration {
|
||||
|
||||
@Bean
|
||||
CookieSameSiteSupplier cookieSameSiteSupplier1() {
|
||||
return CookieSameSiteSupplier.ofLax().whenHasName("test1");
|
||||
}
|
||||
|
||||
@Bean
|
||||
CookieSameSiteSupplier cookieSameSiteSupplier2() {
|
||||
return CookieSameSiteSupplier.ofNone().whenHasName("test2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ServerEndpoint("/")
|
||||
public static class TestEndpoint {
|
||||
|
||||
static void register(ServletContext context) {
|
||||
try {
|
||||
ServerContainer serverContainer = (ServerContainer) context
|
||||
.getAttribute("jakarta.websocket.server.ServerContainer");
|
||||
if (serverContainer != null) {
|
||||
serverContainer.addEndpoint(TestEndpoint.class);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class UnauthorizedFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
FilterRegistrationBean<Filter> unauthorizedFilter() {
|
||||
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(new Filter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
((HttpServletResponse) response).sendError(HttpStatus.UNAUTHORIZED.value());
|
||||
}
|
||||
|
||||
});
|
||||
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||
return registration;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ServletRegistrationBean<HttpServlet> basicServlet() {
|
||||
ServletRegistrationBean<HttpServlet> registration = new ServletRegistrationBean<>(new HttpServlet() {
|
||||
});
|
||||
registration.addUrlMappings("/");
|
||||
return registration;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user