Browse Source

Merge pull request #42448 from qingbozhang

* pr/42448:
  Polish 'Add support for 'server.jetty.max-form-key' property'
  Add support for 'server.jetty.max-form-key' property

Closes gh-42448
pull/42503/head
Phillip Webb 1 year ago
parent
commit
f29d49211f
  1. 13
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java
  2. 154
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java
  3. 9
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java
  4. 31
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java

13
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java

@ -1148,6 +1148,11 @@ public class ServerProperties {
*/ */
private DataSize maxHttpFormPostSize = DataSize.ofBytes(200000); private DataSize maxHttpFormPostSize = DataSize.ofBytes(200000);
/**
* Maximum number of form keys.
*/
private int maxFormKeys = 1000;
/** /**
* Time that the connection can be idle before it is closed. * Time that the connection can be idle before it is closed.
*/ */
@ -1180,6 +1185,14 @@ public class ServerProperties {
this.maxHttpFormPostSize = maxHttpFormPostSize; this.maxHttpFormPostSize = maxHttpFormPostSize;
} }
public int getMaxFormKeys() {
return this.maxFormKeys;
}
public void setMaxFormKeys(int maxFormKeys) {
this.maxFormKeys = maxFormKeys;
}
public Duration getConnectionIdleTimeout() { public Duration getConnectionIdleTimeout() {
return this.connectionIdleTimeout; return this.connectionIdleTimeout;
} }

154
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,21 +19,23 @@ package org.springframework.boot.autoconfigure.web.embedded;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.RequestLogWriter; import org.eclipse.jetty.server.RequestLogWriter;
import org.eclipse.jetty.server.Server;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory; import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory;
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
@ -83,18 +85,21 @@ public class JettyWebServerFactoryCustomizer
map.from(this.serverProperties::getMaxHttpRequestHeaderSize) map.from(this.serverProperties::getMaxHttpRequestHeaderSize)
.asInt(DataSize::toBytes) .asInt(DataSize::toBytes)
.when(this::isPositive) .when(this::isPositive)
.to((maxHttpRequestHeaderSize) -> factory .to(customizeHttpConfigurations(factory, HttpConfiguration::setRequestHeaderSize));
.addServerCustomizers(new MaxHttpRequestHeaderSizeCustomizer(maxHttpRequestHeaderSize)));
map.from(properties::getMaxHttpResponseHeaderSize) map.from(properties::getMaxHttpResponseHeaderSize)
.asInt(DataSize::toBytes) .asInt(DataSize::toBytes)
.when(this::isPositive) .when(this::isPositive)
.to((maxHttpResponseHeaderSize) -> factory .to(customizeHttpConfigurations(factory, HttpConfiguration::setResponseHeaderSize));
.addServerCustomizers(new MaxHttpResponseHeaderSizeCustomizer(maxHttpResponseHeaderSize)));
map.from(properties::getMaxHttpFormPostSize) map.from(properties::getMaxHttpFormPostSize)
.asInt(DataSize::toBytes) .asInt(DataSize::toBytes)
.when(this::isPositive) .when(this::isPositive)
.to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize)); .to(customizeServletContextHandler(factory, ServletContextHandler::setMaxFormContentSize));
map.from(properties::getConnectionIdleTimeout).to((idleTimeout) -> customizeIdleTimeout(factory, idleTimeout)); map.from(properties::getMaxFormKeys)
.when(this::isPositive)
.to(customizeServletContextHandler(factory, ServletContextHandler::setMaxFormKeys));
map.from(properties::getConnectionIdleTimeout)
.as(Duration::toMillis)
.to(customizeAbstractConnectors(factory, AbstractConnector::setIdleTimeout));
map.from(properties::getAccesslog) map.from(properties::getAccesslog)
.when(ServerProperties.Jetty.Accesslog::isEnabled) .when(ServerProperties.Jetty.Accesslog::isEnabled)
.to((accesslog) -> customizeAccessLog(factory, accesslog)); .to((accesslog) -> customizeAccessLog(factory, accesslog));
@ -112,43 +117,63 @@ public class JettyWebServerFactoryCustomizer
return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
} }
private void customizeIdleTimeout(ConfigurableJettyWebServerFactory factory, Duration connectionTimeout) { private <T> Consumer<T> customizeHttpConfigurations(ConfigurableJettyWebServerFactory factory,
factory.addServerCustomizers((server) -> { BiConsumer<HttpConfiguration, T> action) {
for (org.eclipse.jetty.server.Connector connector : server.getConnectors()) { return customizeConnectionFactories(factory, HttpConfiguration.ConnectionFactory.class,
if (connector instanceof AbstractConnector abstractConnector) { (connectionFactory, value) -> action.accept(connectionFactory.getHttpConfiguration(), value));
abstractConnector.setIdleTimeout(connectionTimeout.toMillis()); }
}
} private <V, F> Consumer<V> customizeConnectionFactories(ConfigurableJettyWebServerFactory factory,
Class<F> connectionFactoryType, BiConsumer<F, V> action) {
return customizeConnectors(factory, Connector.class, (connector, value) -> {
Stream<ConnectionFactory> connectionFactories = connector.getConnectionFactories().stream();
forEach(connectionFactories, connectionFactoryType, action, value);
}); });
} }
private void customizeMaxHttpFormPostSize(ConfigurableJettyWebServerFactory factory, int maxHttpFormPostSize) { private <V> Consumer<V> customizeAbstractConnectors(ConfigurableJettyWebServerFactory factory,
factory.addServerCustomizers(new JettyServerCustomizer() { BiConsumer<AbstractConnector, V> action) {
return customizeConnectors(factory, AbstractConnector.class, action);
}
@Override private <V, C> Consumer<V> customizeConnectors(ConfigurableJettyWebServerFactory factory, Class<C> connectorType,
public void customize(Server server) { BiConsumer<C, V> action) {
setHandlerMaxHttpFormPostSize(server.getHandlers()); return (value) -> factory.addServerCustomizers((server) -> {
} Stream<Connector> connectors = Arrays.stream(server.getConnectors());
forEach(connectors, connectorType, action, value);
});
}
private void setHandlerMaxHttpFormPostSize(List<Handler> handlers) { private <V> Consumer<V> customizeServletContextHandler(ConfigurableJettyWebServerFactory factory,
for (Handler handler : handlers) { BiConsumer<ServletContextHandler, V> action) {
setHandlerMaxHttpFormPostSize(handler); return customizeHandlers(factory, ServletContextHandler.class, action);
} }
}
private void setHandlerMaxHttpFormPostSize(Handler handler) { private <V, H> Consumer<V> customizeHandlers(ConfigurableJettyWebServerFactory factory, Class<H> handlerType,
if (handler instanceof ServletContextHandler contextHandler) { BiConsumer<H, V> action) {
contextHandler.setMaxFormContentSize(maxHttpFormPostSize); return (value) -> factory.addServerCustomizers((server) -> {
} List<Handler> handlers = server.getHandlers();
else if (handler instanceof Handler.Wrapper wrapper) { forEachHandler(handlers, handlerType, action, value);
setHandlerMaxHttpFormPostSize(wrapper.getHandler()); });
} }
else if (handler instanceof Handler.Collection collection) {
setHandlerMaxHttpFormPostSize(collection.getHandlers()); @SuppressWarnings("unchecked")
} private <V, H> void forEachHandler(List<Handler> handlers, Class<H> handlerType, BiConsumer<H, V> action, V value) {
for (Handler handler : handlers) {
if (handlerType.isInstance(handler)) {
action.accept((H) handler, value);
}
if (handler instanceof Handler.Wrapper wrapper) {
forEachHandler(wrapper.getHandlers(), handlerType, action, value);
} }
if (handler instanceof Handler.Collection collection) {
forEachHandler(collection.getHandlers(), handlerType, action, value);
}
}
}
}); private <T, V> void forEach(Stream<?> elements, Class<T> type, BiConsumer<T, V> action, V value) {
elements.filter(type::isInstance).map(type::cast).forEach((element) -> action.accept(element, value));
} }
private void customizeAccessLog(ConfigurableJettyWebServerFactory factory, private void customizeAccessLog(ConfigurableJettyWebServerFactory factory,
@ -176,61 +201,10 @@ public class JettyWebServerFactoryCustomizer
if (properties.getCustomFormat() != null) { if (properties.getCustomFormat() != null) {
return properties.getCustomFormat(); return properties.getCustomFormat();
} }
else if (ServerProperties.Jetty.Accesslog.FORMAT.EXTENDED_NCSA.equals(properties.getFormat())) { if (ServerProperties.Jetty.Accesslog.FORMAT.EXTENDED_NCSA.equals(properties.getFormat())) {
return CustomRequestLog.EXTENDED_NCSA_FORMAT; return CustomRequestLog.EXTENDED_NCSA_FORMAT;
} }
return CustomRequestLog.NCSA_FORMAT; return CustomRequestLog.NCSA_FORMAT;
} }
private static class MaxHttpRequestHeaderSizeCustomizer implements JettyServerCustomizer {
private final int maxRequestHeaderSize;
MaxHttpRequestHeaderSizeCustomizer(int maxRequestHeaderSize) {
this.maxRequestHeaderSize = maxRequestHeaderSize;
}
@Override
public void customize(Server server) {
Arrays.stream(server.getConnectors()).forEach(this::customize);
}
private void customize(org.eclipse.jetty.server.Connector connector) {
connector.getConnectionFactories().forEach(this::customize);
}
private void customize(ConnectionFactory factory) {
if (factory instanceof HttpConfiguration.ConnectionFactory) {
((HttpConfiguration.ConnectionFactory) factory).getHttpConfiguration()
.setRequestHeaderSize(this.maxRequestHeaderSize);
}
}
}
private static class MaxHttpResponseHeaderSizeCustomizer implements JettyServerCustomizer {
private final int maxResponseHeaderSize;
MaxHttpResponseHeaderSizeCustomizer(int maxResponseHeaderSize) {
this.maxResponseHeaderSize = maxResponseHeaderSize;
}
@Override
public void customize(Server server) {
Arrays.stream(server.getConnectors()).forEach(this::customize);
}
private void customize(org.eclipse.jetty.server.Connector connector) {
connector.getConnectionFactories().forEach(this::customize);
}
private void customize(ConnectionFactory factory) {
if (factory instanceof HttpConfiguration.ConnectionFactory httpConnectionFactory) {
httpConnectionFactory.getHttpConfiguration().setResponseHeaderSize(this.maxResponseHeaderSize);
}
}
}
} }

9
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java

@ -465,6 +465,15 @@ class ServerPropertiesTests {
.isEqualTo(((ServletContextHandler) server.getHandler()).getMaxFormContentSize()); .isEqualTo(((ServletContextHandler) server.getHandler()).getMaxFormContentSize());
} }
@Test
void jettyMaxFormKeysMatchesDefault() {
JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0);
JettyWebServer jetty = (JettyWebServer) jettyFactory.getWebServer();
Server server = jetty.getServer();
assertThat(this.properties.getJetty().getMaxFormKeys())
.isEqualTo(((ServletContextHandler) server.getHandler()).getMaxFormKeys());
}
@Test @Test
void undertowMaxHttpPostSizeMatchesDefault() { void undertowMaxHttpPostSizeMatchesDefault() {
assertThat(this.properties.getUndertow().getMaxHttpPostSize().toBytes()) assertThat(this.properties.getUndertow().getMaxHttpPostSize().toBytes())

31
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue; import java.util.concurrent.SynchronousQueue;
import java.util.function.Function; import java.util.function.Function;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.CustomRequestLog;
@ -324,10 +325,23 @@ class JettyWebServerFactoryCustomizerTests {
assertThat(timeouts).containsOnly(60000L); assertThat(timeouts).containsOnly(60000L);
} }
@Test
void customMaxFormKeys() {
bind("server.jetty.max-form-keys=2048");
JettyWebServer server = customizeAndGetServer();
startAndStopToMakeInternalsAvailable(server);
List<Integer> maxFormKeys = server.getServer()
.getHandlers()
.stream()
.filter(ServletContextHandler.class::isInstance)
.map(ServletContextHandler.class::cast)
.map(ServletContextHandler::getMaxFormKeys)
.toList();
assertThat(maxFormKeys).containsOnly(2048);
}
private List<Long> connectorsIdleTimeouts(JettyWebServer server) { private List<Long> connectorsIdleTimeouts(JettyWebServer server) {
// Start (and directly stop) server to have connectors available startAndStopToMakeInternalsAvailable(server);
server.start();
server.stop();
return Arrays.stream(server.getServer().getConnectors()) return Arrays.stream(server.getServer().getConnectors())
.filter((connector) -> connector instanceof AbstractConnector) .filter((connector) -> connector instanceof AbstractConnector)
.map(Connector::getIdleTimeout) .map(Connector::getIdleTimeout)
@ -344,9 +358,7 @@ class JettyWebServerFactoryCustomizerTests {
private List<Integer> getHeaderSizes(JettyWebServer server, Function<HttpConfiguration, Integer> provider) { private List<Integer> getHeaderSizes(JettyWebServer server, Function<HttpConfiguration, Integer> provider) {
List<Integer> requestHeaderSizes = new ArrayList<>(); List<Integer> requestHeaderSizes = new ArrayList<>();
// Start (and directly stop) server to have connectors available startAndStopToMakeInternalsAvailable(server);
server.start();
server.stop();
Connector[] connectors = server.getServer().getConnectors(); Connector[] connectors = server.getServer().getConnectors();
for (Connector connector : connectors) { for (Connector connector : connectors) {
connector.getConnectionFactories() connector.getConnectionFactories()
@ -361,6 +373,11 @@ class JettyWebServerFactoryCustomizerTests {
return requestHeaderSizes; return requestHeaderSizes;
} }
private void startAndStopToMakeInternalsAvailable(JettyWebServer server) {
server.start();
server.stop();
}
private BlockingQueue<?> getQueue(ThreadPool threadPool) { private BlockingQueue<?> getQueue(ThreadPool threadPool) {
return ReflectionTestUtils.invokeMethod(threadPool, "getQueue"); return ReflectionTestUtils.invokeMethod(threadPool, "getQueue");
} }

Loading…
Cancel
Save