diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 31e15f6a98a..95c687b0f4b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/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); + /** + * Maximum number of form keys. + */ + private int maxFormKeys = 1000; + /** * Time that the connection can be idle before it is closed. */ @@ -1180,6 +1185,14 @@ public class ServerProperties { this.maxHttpFormPostSize = maxHttpFormPostSize; } + public int getMaxFormKeys() { + return this.maxFormKeys; + } + + public void setMaxFormKeys(int maxFormKeys) { + this.maxFormKeys = maxFormKeys; + } + public Duration getConnectionIdleTimeout() { return this.connectionIdleTimeout; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java index c12333dc9cf..7799bcd817e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java +++ b/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"); * 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.util.Arrays; 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.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.RequestLogWriter; -import org.eclipse.jetty.server.Server; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.PropertyMapper; 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.core.Ordered; import org.springframework.core.env.Environment; @@ -83,18 +85,21 @@ public class JettyWebServerFactoryCustomizer map.from(this.serverProperties::getMaxHttpRequestHeaderSize) .asInt(DataSize::toBytes) .when(this::isPositive) - .to((maxHttpRequestHeaderSize) -> factory - .addServerCustomizers(new MaxHttpRequestHeaderSizeCustomizer(maxHttpRequestHeaderSize))); + .to(customizeHttpConfigurations(factory, HttpConfiguration::setRequestHeaderSize)); map.from(properties::getMaxHttpResponseHeaderSize) .asInt(DataSize::toBytes) .when(this::isPositive) - .to((maxHttpResponseHeaderSize) -> factory - .addServerCustomizers(new MaxHttpResponseHeaderSizeCustomizer(maxHttpResponseHeaderSize))); + .to(customizeHttpConfigurations(factory, HttpConfiguration::setResponseHeaderSize)); map.from(properties::getMaxHttpFormPostSize) .asInt(DataSize::toBytes) .when(this::isPositive) - .to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize)); - map.from(properties::getConnectionIdleTimeout).to((idleTimeout) -> customizeIdleTimeout(factory, idleTimeout)); + .to(customizeServletContextHandler(factory, ServletContextHandler::setMaxFormContentSize)); + 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) .when(ServerProperties.Jetty.Accesslog::isEnabled) .to((accesslog) -> customizeAccessLog(factory, accesslog)); @@ -112,43 +117,63 @@ public class JettyWebServerFactoryCustomizer return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } - private void customizeIdleTimeout(ConfigurableJettyWebServerFactory factory, Duration connectionTimeout) { - factory.addServerCustomizers((server) -> { - for (org.eclipse.jetty.server.Connector connector : server.getConnectors()) { - if (connector instanceof AbstractConnector abstractConnector) { - abstractConnector.setIdleTimeout(connectionTimeout.toMillis()); - } - } + private Consumer customizeHttpConfigurations(ConfigurableJettyWebServerFactory factory, + BiConsumer action) { + return customizeConnectionFactories(factory, HttpConfiguration.ConnectionFactory.class, + (connectionFactory, value) -> action.accept(connectionFactory.getHttpConfiguration(), value)); + } + + private Consumer customizeConnectionFactories(ConfigurableJettyWebServerFactory factory, + Class connectionFactoryType, BiConsumer action) { + return customizeConnectors(factory, Connector.class, (connector, value) -> { + Stream connectionFactories = connector.getConnectionFactories().stream(); + forEach(connectionFactories, connectionFactoryType, action, value); }); } - private void customizeMaxHttpFormPostSize(ConfigurableJettyWebServerFactory factory, int maxHttpFormPostSize) { - factory.addServerCustomizers(new JettyServerCustomizer() { + private Consumer customizeAbstractConnectors(ConfigurableJettyWebServerFactory factory, + BiConsumer action) { + return customizeConnectors(factory, AbstractConnector.class, action); + } - @Override - public void customize(Server server) { - setHandlerMaxHttpFormPostSize(server.getHandlers()); - } + private Consumer customizeConnectors(ConfigurableJettyWebServerFactory factory, Class connectorType, + BiConsumer action) { + return (value) -> factory.addServerCustomizers((server) -> { + Stream connectors = Arrays.stream(server.getConnectors()); + forEach(connectors, connectorType, action, value); + }); + } - private void setHandlerMaxHttpFormPostSize(List handlers) { - for (Handler handler : handlers) { - setHandlerMaxHttpFormPostSize(handler); - } - } + private Consumer customizeServletContextHandler(ConfigurableJettyWebServerFactory factory, + BiConsumer action) { + return customizeHandlers(factory, ServletContextHandler.class, action); + } - private void setHandlerMaxHttpFormPostSize(Handler handler) { - if (handler instanceof ServletContextHandler contextHandler) { - contextHandler.setMaxFormContentSize(maxHttpFormPostSize); - } - else if (handler instanceof Handler.Wrapper wrapper) { - setHandlerMaxHttpFormPostSize(wrapper.getHandler()); - } - else if (handler instanceof Handler.Collection collection) { - setHandlerMaxHttpFormPostSize(collection.getHandlers()); - } + private Consumer customizeHandlers(ConfigurableJettyWebServerFactory factory, Class handlerType, + BiConsumer action) { + return (value) -> factory.addServerCustomizers((server) -> { + List handlers = server.getHandlers(); + forEachHandler(handlers, handlerType, action, value); + }); + } + + @SuppressWarnings("unchecked") + private void forEachHandler(List handlers, Class handlerType, BiConsumer 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 void forEach(Stream elements, Class type, BiConsumer action, V value) { + elements.filter(type::isInstance).map(type::cast).forEach((element) -> action.accept(element, value)); } private void customizeAccessLog(ConfigurableJettyWebServerFactory factory, @@ -176,61 +201,10 @@ public class JettyWebServerFactoryCustomizer if (properties.getCustomFormat() != null) { 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.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); - } - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 0bfeacf8ec6..ddca47e7f6a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/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()); } + @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 void undertowMaxHttpPostSizeMatchesDefault() { assertThat(this.properties.getUndertow().getMaxHttpPostSize().toBytes()) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java index eef94bb88a2..cf2970c1ecc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java +++ b/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"); * 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.function.Function; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.CustomRequestLog; @@ -324,10 +325,23 @@ class JettyWebServerFactoryCustomizerTests { assertThat(timeouts).containsOnly(60000L); } + @Test + void customMaxFormKeys() { + bind("server.jetty.max-form-keys=2048"); + JettyWebServer server = customizeAndGetServer(); + startAndStopToMakeInternalsAvailable(server); + List maxFormKeys = server.getServer() + .getHandlers() + .stream() + .filter(ServletContextHandler.class::isInstance) + .map(ServletContextHandler.class::cast) + .map(ServletContextHandler::getMaxFormKeys) + .toList(); + assertThat(maxFormKeys).containsOnly(2048); + } + private List connectorsIdleTimeouts(JettyWebServer server) { - // Start (and directly stop) server to have connectors available - server.start(); - server.stop(); + startAndStopToMakeInternalsAvailable(server); return Arrays.stream(server.getServer().getConnectors()) .filter((connector) -> connector instanceof AbstractConnector) .map(Connector::getIdleTimeout) @@ -344,9 +358,7 @@ class JettyWebServerFactoryCustomizerTests { private List getHeaderSizes(JettyWebServer server, Function provider) { List requestHeaderSizes = new ArrayList<>(); - // Start (and directly stop) server to have connectors available - server.start(); - server.stop(); + startAndStopToMakeInternalsAvailable(server); Connector[] connectors = server.getServer().getConnectors(); for (Connector connector : connectors) { connector.getConnectionFactories() @@ -361,6 +373,11 @@ class JettyWebServerFactoryCustomizerTests { return requestHeaderSizes; } + private void startAndStopToMakeInternalsAvailable(JettyWebServer server) { + server.start(); + server.stop(); + } + private BlockingQueue getQueue(ThreadPool threadPool) { return ReflectionTestUtils.invokeMethod(threadPool, "getQueue"); }