From 6e885c423f904f81a6f53a3096f378f8a5dc47e2 Mon Sep 17 00:00:00 2001 From: qingbozhang Date: Wed, 25 Sep 2024 23:04:55 +0800 Subject: [PATCH 1/2] Add support for 'server.jetty.max-form-key' property Add a new 'server.jetty.max-form-key' property that can be used to configure Jetty's Handler.setMaxFormKeys(...). See gh-42448 --- .../autoconfigure/web/ServerProperties.java | 13 +++++++++++ .../JettyWebServerFactoryCustomizer.java | 23 +++++++++++-------- .../web/ServerPropertiesTests.java | 9 ++++++++ .../JettyWebServerFactoryCustomizerTests.java | 18 +++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) 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..59387856bb8 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 @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.server.AbstractConnector; @@ -93,7 +94,11 @@ public class JettyWebServerFactoryCustomizer map.from(properties::getMaxHttpFormPostSize) .asInt(DataSize::toBytes) .when(this::isPositive) - .to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize)); + .to((maxHttpFormPostSize) -> customizeServletContextHandler(factory, contextHandler -> contextHandler.setMaxFormContentSize(maxHttpFormPostSize))); + map.from(properties::getMaxFormKeys) + .when(this::isPositive) + .to((maxFormKeys) -> customizeServletContextHandler(factory, contextHandler -> contextHandler.setMaxFormKeys(maxFormKeys))); + map.from(properties::getConnectionIdleTimeout).to((idleTimeout) -> customizeIdleTimeout(factory, idleTimeout)); map.from(properties::getAccesslog) .when(ServerProperties.Jetty.Accesslog::isEnabled) @@ -122,29 +127,29 @@ public class JettyWebServerFactoryCustomizer }); } - private void customizeMaxHttpFormPostSize(ConfigurableJettyWebServerFactory factory, int maxHttpFormPostSize) { + private void customizeServletContextHandler(ConfigurableJettyWebServerFactory factory, Consumer customFunc) { factory.addServerCustomizers(new JettyServerCustomizer() { @Override public void customize(Server server) { - setHandlerMaxHttpFormPostSize(server.getHandlers()); + acceptCustomizeServletContextHandler(server.getHandlers()); } - private void setHandlerMaxHttpFormPostSize(List handlers) { + private void acceptCustomizeServletContextHandler(List handlers) { for (Handler handler : handlers) { - setHandlerMaxHttpFormPostSize(handler); + acceptCustomizeServletContextHandler(handler); } } - private void setHandlerMaxHttpFormPostSize(Handler handler) { + private void acceptCustomizeServletContextHandler(Handler handler) { if (handler instanceof ServletContextHandler contextHandler) { - contextHandler.setMaxFormContentSize(maxHttpFormPostSize); + customFunc.accept(contextHandler); } else if (handler instanceof Handler.Wrapper wrapper) { - setHandlerMaxHttpFormPostSize(wrapper.getHandler()); + acceptCustomizeServletContextHandler(wrapper.getHandler()); } else if (handler instanceof Handler.Collection collection) { - setHandlerMaxHttpFormPostSize(collection.getHandlers()); + acceptCustomizeServletContextHandler(collection.getHandlers()); } } 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..01baba2092d 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..70a2b2c3e9e 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 @@ -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,6 +325,23 @@ class JettyWebServerFactoryCustomizerTests { assertThat(timeouts).containsOnly(60000L); } + @Test + void customMaxFormKeys() { + bind("server.jetty.max-form-keys=2048"); + JettyWebServer server = customizeAndGetServer(); + List maxFormKeys = getMaxFormKeys(server); + assertThat(maxFormKeys).containsOnly(2048); + } + + private List getMaxFormKeys(JettyWebServer server) { + server.start(); + server.stop(); + return server.getServer().getHandlers().stream() + .filter(handler -> handler instanceof ServletContextHandler) + .map(handler -> ((ServletContextHandler) handler).getMaxFormKeys()) + .toList(); + } + private List connectorsIdleTimeouts(JettyWebServer server) { // Start (and directly stop) server to have connectors available server.start(); From 58a1b2bea5d6d528c08631e152dd13fcb0762f1c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 2 Oct 2024 17:53:15 -0700 Subject: [PATCH 2/2] Polish 'Add support for 'server.jetty.max-form-key' property' See gh-42448 --- .../JettyWebServerFactoryCustomizer.java | 155 +++++++----------- .../web/ServerPropertiesTests.java | 2 +- .../JettyWebServerFactoryCustomizerTests.java | 33 ++-- 3 files changed, 79 insertions(+), 111 deletions(-) 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 59387856bb8..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,22 +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; @@ -84,22 +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) -> customizeServletContextHandler(factory, contextHandler -> contextHandler.setMaxFormContentSize(maxHttpFormPostSize))); + .to(customizeServletContextHandler(factory, ServletContextHandler::setMaxFormContentSize)); map.from(properties::getMaxFormKeys) - .when(this::isPositive) - .to((maxFormKeys) -> customizeServletContextHandler(factory, contextHandler -> contextHandler.setMaxFormKeys(maxFormKeys))); - - map.from(properties::getConnectionIdleTimeout).to((idleTimeout) -> customizeIdleTimeout(factory, idleTimeout)); + .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)); @@ -117,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 customizeServletContextHandler(ConfigurableJettyWebServerFactory factory, Consumer customFunc) { - factory.addServerCustomizers(new JettyServerCustomizer() { + private Consumer customizeAbstractConnectors(ConfigurableJettyWebServerFactory factory, + BiConsumer action) { + return customizeConnectors(factory, AbstractConnector.class, action); + } - @Override - public void customize(Server server) { - acceptCustomizeServletContextHandler(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 acceptCustomizeServletContextHandler(List handlers) { - for (Handler handler : handlers) { - acceptCustomizeServletContextHandler(handler); - } - } + private Consumer customizeServletContextHandler(ConfigurableJettyWebServerFactory factory, + BiConsumer action) { + return customizeHandlers(factory, ServletContextHandler.class, action); + } - private void acceptCustomizeServletContextHandler(Handler handler) { - if (handler instanceof ServletContextHandler contextHandler) { - customFunc.accept(contextHandler); - } - else if (handler instanceof Handler.Wrapper wrapper) { - acceptCustomizeServletContextHandler(wrapper.getHandler()); - } - else if (handler instanceof Handler.Collection collection) { - acceptCustomizeServletContextHandler(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, @@ -181,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 01baba2092d..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 @@ -471,7 +471,7 @@ class ServerPropertiesTests { JettyWebServer jetty = (JettyWebServer) jettyFactory.getWebServer(); Server server = jetty.getServer(); assertThat(this.properties.getJetty().getMaxFormKeys()) - .isEqualTo(((ServletContextHandler) server.getHandler()).getMaxFormKeys()); + .isEqualTo(((ServletContextHandler) server.getHandler()).getMaxFormKeys()); } @Test 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 70a2b2c3e9e..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. @@ -329,23 +329,19 @@ class JettyWebServerFactoryCustomizerTests { void customMaxFormKeys() { bind("server.jetty.max-form-keys=2048"); JettyWebServer server = customizeAndGetServer(); - List maxFormKeys = getMaxFormKeys(server); + 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 getMaxFormKeys(JettyWebServer server) { - server.start(); - server.stop(); - return server.getServer().getHandlers().stream() - .filter(handler -> handler instanceof ServletContextHandler) - .map(handler -> ((ServletContextHandler) handler).getMaxFormKeys()) - .toList(); - } - 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) @@ -362,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() @@ -379,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"); }