diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index c3935e9c..785de171 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1143,6 +1143,29 @@ spring: This will remove the `red` parameter before it is sent downstream. +=== `RequestHeaderSize` `GatewayFilter` Factory + +The `RequestHeaderSize` `GatewayFilter` factory takes `maxSize` and `errorHeaderName` parameters. +The `maxSize` parameter is the maximum data size allowed of the request header (incuding key and value). The `errorHeaderName` parameter sets the name of the response header containing an error message, by default it is "errorMessage". +The following listing configures a `RequestHeaderSize` `GatewayFilter`: + +.application.yml +==== +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: requestheadersize_route + uri: https://example.org + filters: + - RequestHeaderSize=1000B +---- +==== + +This will send a status 431 if size of any request header is greater than 1000 Bytes. + === The `RewritePath` `GatewayFilter` Factory The `RewritePath` `GatewayFilter` factory takes a path `regexp` parameter and a `replacement` parameter. diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactory.java index 8c3c80f3..3817dca7 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactory.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.filter.factory; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,18 +35,20 @@ import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; /** - * This filter validates the size of each Request Header in the request. If size of any of - * the request header is greater than the configured maxSize,it blocks the request. - * Default max size of request header is 16KB. + * This filter validates the size of each Request Header in the request, including the + * key. If size of the request header is greater than the configured maxSize, it blocks + * the request. Default max size of request header is 16KB. * * @author Sakalya Deshpande + * @author Marta Medio */ public class RequestHeaderSizeGatewayFilterFactory extends AbstractGatewayFilterFactory { - private static String ERROR = "Request Header/s size is larger than permissible limit." - + " Request Header/s size is %s where permissible limit is %s"; + private static String ERROR_PREFIX = "Request Header/s size is larger than permissible limit (%s)."; + + private static String ERROR = " Request Header/s size for '%s' is %s."; public RequestHeaderSizeGatewayFilterFactory() { super(RequestHeaderSizeGatewayFilterFactory.Config.class); @@ -58,24 +61,30 @@ public class RequestHeaderSizeGatewayFilterFactory @Override public GatewayFilter apply(RequestHeaderSizeGatewayFilterFactory.Config config) { + String errorHeaderName = config.getErrorHeaderName() != null ? config.getErrorHeaderName() : "errorMessage"; return new GatewayFilter() { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); - Long headerSizeInBytes = 0L; + HashMap longHeaders = new HashMap<>(); for (Map.Entry> headerEntry : headers.entrySet()) { + long headerSizeInBytes = 0L; + headerSizeInBytes += headerEntry.getKey().getBytes().length; List values = headerEntry.getValue(); for (String value : values) { - headerSizeInBytes += (long) value.getBytes().length; + headerSizeInBytes += value.getBytes().length; + } + if (headerSizeInBytes > config.getMaxSize().toBytes()) { + longHeaders.put(headerEntry.getKey(), headerSizeInBytes); } } - if (headerSizeInBytes > config.getMaxSize().toBytes()) { + if (!longHeaders.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE); - exchange.getResponse().getHeaders().add("errorMessage", - getErrorMessage(headerSizeInBytes, config.getMaxSize())); + exchange.getResponse().getHeaders().add(errorHeaderName, + getErrorMessage(longHeaders, config.getMaxSize())); return exchange.getResponse().setComplete(); } @@ -86,19 +95,24 @@ public class RequestHeaderSizeGatewayFilterFactory @Override public String toString() { return filterToStringCreator(RequestHeaderSizeGatewayFilterFactory.this) - .append("max", config.getMaxSize()).toString(); + .append("maxSize", config.getMaxSize()).toString(); } }; } - private static String getErrorMessage(Long currentRequestSize, DataSize maxSize) { - return String.format(ERROR, DataSize.of(currentRequestSize, DataUnit.BYTES), maxSize); + private static String getErrorMessage(HashMap longHeaders, DataSize maxSize) { + StringBuilder msg = new StringBuilder(String.format(ERROR_PREFIX, maxSize)); + longHeaders + .forEach((header, size) -> msg.append(String.format(ERROR, header, DataSize.of(size, DataUnit.BYTES)))); + return msg.toString(); } public static class Config { private DataSize maxSize = DataSize.ofBytes(16000L); + private String errorHeaderName; + public DataSize getMaxSize() { return maxSize; } @@ -107,6 +121,14 @@ public class RequestHeaderSizeGatewayFilterFactory this.maxSize = maxSize; } + public String getErrorHeaderName() { + return errorHeaderName; + } + + public void setErrorHeaderName(String errorHeaderName) { + this.errorHeaderName = errorHeaderName; + } + } } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactoryTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactoryTest.java index c0dcf5d9..70b23798 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactoryTest.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestHeaderSizeGatewayFilterFactoryTest.java @@ -39,29 +39,47 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen /** * @author Sakalya Deshpande + * @author Marta Medio */ @SpringBootTest(webEnvironment = RANDOM_PORT) @DirtiesContext public class RequestHeaderSizeGatewayFilterFactoryTest extends BaseWebClientTests { - private static final String responseMesssage = "Request Header/s size is larger than " - + "permissible limit. Request Header/s size is \\d*B where permissible limit is \\d*B"; + private static final String longString = "11111111112222222222333333333344444444445555555"; @Test public void setRequestHeaderSizeFilterWorks() { + System.err.println("Here: " + longString.length() + ", " + longString.getBytes().length); testClient.get().uri("/headers").header("Host", "www.testrequestheadersizefilter.org") - .header("HeaderName", "Some Very Large Header Name").exchange().expectStatus() - .isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE).expectHeader() - .valueMatches("errorMessage", responseMesssage); + .header("HeaderName", longString).exchange().expectStatus() + .isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE).expectHeader().value("errorMessage", + header -> assertThat(header).contains("permissible limit (46B)", "'HeaderName' is 57B")); } @Test public void setRequestHeaderSizeFilterShortcutWorks() { - testClient.get().uri("/headers").header("Host", "www.requestheadersize.org") - .header("HeaderName", "Some Very Large Header Name").exchange().expectStatus() + testClient.get().uri("/headers").header("Host", "www.requestheadersize.org").header("HeaderName", longString) + .exchange().expectStatus().isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE).expectHeader() + .value("errorMessage", + header -> assertThat(header).contains("permissible limit (46B)", "'HeaderName' is 57B")); + } + + @Test + public void setRequestHeaderSizeFilterMultipleHeadersWorks() { + testClient.get().uri("/headers").header("Host", "www.requestheadersize.org").header("HeaderName", longString) + .header("HeaderName2", longString).exchange().expectStatus() .isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE).expectHeader() - .valueMatches("errorMessage", responseMesssage); + .value("errorMessage", header -> assertThat(header).contains("permissible limit (46B)", + "'HeaderName' is 57B", "'HeaderName2' is 58B")); + } + + @Test + public void setRequestHeaderSizeFilterTakesIntoAccountHeaderName() { + testClient.get().uri("/headers").header("Host", "www.testrequestheadersizefiltername.org") + .header("HeaderName", longString).exchange().expectStatus() + .isEqualTo(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE).expectHeader().value("errorMessage", + header -> assertThat(header).contains("permissible limit (47B)", "'HeaderName' is 57B")); } @Test @@ -69,7 +87,7 @@ public class RequestHeaderSizeGatewayFilterFactoryTest extends BaseWebClientTest Config config = new Config(); config.setMaxSize(DataSize.ofBytes(1000L)); GatewayFilter filter = new RequestHeaderSizeGatewayFilterFactory().apply(config); - assertThat(filter.toString()).contains("max").contains("1000B"); + assertThat(filter.toString()).contains("maxSize").contains("1000B"); } @EnableAutoConfiguration @@ -86,6 +104,9 @@ public class RequestHeaderSizeGatewayFilterFactoryTest extends BaseWebClientTest .route("test_request_header_size", r -> r.order(-1).host("**.testrequestheadersizefilter.org") .filters(f -> f.setRequestHeaderSize(DataSize.of(46L, DataUnit.BYTES))).uri(uri)) + .route("test_request_header_size_name", + r -> r.order(1).host("**.testrequestheadersizefiltername.org") + .filters(f -> f.setRequestHeaderSize(DataSize.of(47L, DataUnit.BYTES))).uri(uri)) .build(); } diff --git a/spring-cloud-gateway-server/src/test/resources/application.yml b/spring-cloud-gateway-server/src/test/resources/application.yml index 2628f122..bc4f2c43 100644 --- a/spring-cloud-gateway-server/src/test/resources/application.yml +++ b/spring-cloud-gateway-server/src/test/resources/application.yml @@ -304,7 +304,7 @@ spring: predicates: - Host=**.requestheadersize.org filters: - - RequestHeaderSize=4 + - RequestHeaderSize=46 # ===================================== - id: rewrite_location_response_header_test