From b435ce357504114f7949cad581f36ecd515d44e2 Mon Sep 17 00:00:00 2001 From: Joris Kuipers Date: Wed, 3 Jul 2024 17:32:23 +0200 Subject: [PATCH] Prevent Content-Length and Host headers from being copied by default (#3313) Also remove the bogus spring.cloud.gateway.proxy.auto-forward setting from the tests. Fixes gh-3154 --- .../spring-cloud-gateway-proxy-exchange.adoc | 2 +- .../cloud/gateway/mvc/ProxyExchange.java | 34 ++++++++---------- .../config/ProxyExchangeArgumentResolver.java | 14 ++++---- .../gateway/mvc/config/ProxyProperties.java | 18 ++++++++-- .../ProxyResponseAutoConfiguration.java | 11 +++++- .../gateway/mvc/GetWithBodyRequestTests.java | 2 +- .../mvc/ProductionConfigurationTests.java | 36 +++++++++++++++++-- .../cloud/gateway/webflux/ProxyExchange.java | 34 ++++++++---------- .../config/ProxyExchangeArgumentResolver.java | 10 +++--- .../webflux/config/ProxyProperties.java | 16 ++++++++- .../ProxyResponseAutoConfiguration.java | 11 +++++- .../webflux/ProductionConfigurationTests.java | 34 ++++++++++++++++-- 12 files changed, 158 insertions(+), 64 deletions(-) diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc index 5fb8d8f7..8e39d751 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc @@ -76,5 +76,5 @@ You can add headers to the downstream response by using the `header()` methods o You can also manipulate response headers (and anything else you like in the response) by adding a mapper to the `get()` method (and other methods). The mapper is a `Function` that takes the incoming `ResponseEntity` and converts it to an outgoing one. -First-class support is provided for "`sensitive`" headers (by default, `cookie` and `authorization`), which are not passed downstream, and for "`proxy`" (`x-forwarded-*`) headers. +First-class support is provided for "`sensitive`" headers (by default, `cookie` and `authorization`) and "`skipped`" headers (by default, `content-length` and `host`), which are not passed downstream, and for "`proxy`" (`x-forwarded-*`) headers. The idea behind "`skipped`" headers is that they may result in problems when copied over to the downstream request. For example: because of the way that the `ProxyExchange` calls the downstream endpoint the content's length might have changed or even use a `Transfer-Encoding: chunked` instead of a `Content-Length` header. diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java index e1bb63f8..531bee0f 100644 --- a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java +++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java @@ -85,11 +85,11 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBody * *

* By default the incoming request body and headers are sent intact to the downstream - * service (with the exception of "sensitive" headers). To manipulate the downstream + * service (with the exception of "excluded" headers). To manipulate the downstream * request there are "builder" style methods in {@link ProxyExchange}, but only the - * {@link #uri(String)} is mandatory. You can change the sensitive headers by calling the - * {@link #sensitive(String...)} method (Authorization and Cookie are sensitive by - * default). + * {@link #uri(String)} is mandatory. You can change the excluded headers by calling the + * {@link #excluded(String...)} method (the argument resolver will populate these with + * some sensible defaults). *

*

* The type parameter T in ProxyExchange<T> is the type of @@ -137,12 +137,6 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBody */ public class ProxyExchange { - /** - * Contains headers that are considered case-sensitive by default. - */ - public static Set DEFAULT_SENSITIVE = Collections - .unmodifiableSet(new HashSet<>(Arrays.asList("cookie", "authorization"))); - private URI uri; private RestTemplate rest; @@ -157,7 +151,7 @@ public class ProxyExchange { private WebDataBinderFactory binderFactory; - private Set sensitive; + private Set excluded; private HttpHeaders headers = new HttpHeaders(); @@ -210,19 +204,19 @@ public class ProxyExchange { } /** - * Sets the names of sensitive headers that are not passed downstream to the backend + * Sets the names of excluded headers that are not passed downstream to the backend * service. - * @param names the names of sensitive headers + * @param names the names of excluded headers * @return this for convenience */ - public ProxyExchange sensitive(String... names) { - if (this.sensitive == null) { - this.sensitive = new HashSet<>(); + public ProxyExchange excluded(String... names) { + if (this.excluded == null) { + this.excluded = new HashSet<>(); } - this.sensitive.clear(); + this.excluded.clear(); for (String name : names) { - this.sensitive.add(name.toLowerCase()); + this.excluded.add(name.toLowerCase()); } return this; } @@ -369,8 +363,8 @@ public class ProxyExchange { } private Set filterHeaderKeys(Collection headerNames) { - final Set sensitiveHeaders = this.sensitive != null ? this.sensitive : DEFAULT_SENSITIVE; - return headerNames.stream().filter(header -> !sensitiveHeaders.contains(header.toLowerCase())) + final Set excludedHeaders = this.excluded != null ? this.excluded : Collections.emptySet(); + return headerNames.stream().filter(header -> !excludedHeaders.contains(header.toLowerCase())) .collect(Collectors.toSet()); } diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java index 01f05cf1..b7a27418 100644 --- a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java +++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java @@ -47,7 +47,7 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol private Set autoForwardedHeaders; - private Set sensitive; + private Set excluded; public ProxyExchangeArgumentResolver(RestTemplate builder) { this.rest = builder; @@ -62,8 +62,8 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol : autoForwardedHeaders.stream().map(String::toLowerCase).collect(toSet()); } - public void setSensitive(Set sensitive) { - this.sensitive = sensitive; + public void setExcluded(Set excluded) { + this.excluded = excluded; } @Override @@ -77,7 +77,7 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol ProxyExchange proxy = new ProxyExchange<>(rest, webRequest, mavContainer, binderFactory, type(parameter)); configureHeaders(proxy); configureAutoForwardedHeaders(proxy, webRequest); - configureSensitive(proxy); + configureExcluded(proxy); return proxy; } @@ -115,9 +115,9 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol } } - private void configureSensitive(final ProxyExchange proxy) { - if (sensitive != null) { - proxy.sensitive(sensitive.toArray(new String[0])); + private void configureExcluded(final ProxyExchange proxy) { + if (excluded != null) { + proxy.excluded(excluded.toArray(new String[0])); } } diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyProperties.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyProperties.java index 5ee4b605..c7f2f3f3 100644 --- a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyProperties.java +++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyProperties.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders; * * @author Dave Syer * @author Tim Ysewyn + * @author Joris Kuipers * */ @ConfigurationProperties("spring.cloud.gateway.proxy") @@ -42,14 +43,19 @@ public class ProxyProperties { private Map headers = new LinkedHashMap<>(); /** - * A set of header names that should be send downstream by default. + * A set of header names that should be sent downstream by default. */ private Set autoForward = new HashSet<>(); /** * A set of sensitive header names that will not be sent downstream by default. */ - private Set sensitive = null; + private Set sensitive = Set.of("cookie", "authorization"); + + /** + * A set of header names that will not be sent downstream because they could be problematic. + */ + private Set skipped = Set.of("content-length", "host"); public Map getHeaders() { return headers; @@ -75,6 +81,14 @@ public class ProxyProperties { this.sensitive = sensitive; } + public Set getSkipped() { + return skipped; + } + + public void setSkipped(Set skipped) { + this.skipped = skipped; + } + public HttpHeaders convertHeaders() { HttpHeaders headers = new HttpHeaders(); for (String key : this.headers.keySet()) { diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java index 0b69bab0..4086dea6 100644 --- a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java +++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyResponseAutoConfiguration.java @@ -17,8 +17,10 @@ package org.springframework.cloud.gateway.mvc.config; import java.io.IOException; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -70,7 +72,14 @@ public class ProxyResponseAutoConfiguration implements WebMvcConfigurer { ProxyExchangeArgumentResolver resolver = new ProxyExchangeArgumentResolver(template); resolver.setHeaders(proxy.convertHeaders()); resolver.setAutoForwardedHeaders(proxy.getAutoForward()); - resolver.setSensitive(proxy.getSensitive()); // can be null + Set excludedHeaderNames = new HashSet<>(); + if (proxy.getSensitive() != null) { + excludedHeaderNames.addAll(proxy.getSensitive()); + } + if (proxy.getSkipped() != null) { + excludedHeaderNames.addAll(proxy.getSkipped()); + } + resolver.setExcluded(excludedHeaderNames); return resolver; } diff --git a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/GetWithBodyRequestTests.java b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/GetWithBodyRequestTests.java index 5b2a0981..ea506dbb 100644 --- a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/GetWithBodyRequestTests.java +++ b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/GetWithBodyRequestTests.java @@ -111,7 +111,7 @@ public class GetWithBodyRequestTests { generateConfiguredRestTemplate()); resolver.setHeaders(proxy.convertHeaders()); resolver.setAutoForwardedHeaders(proxy.getAutoForward()); - resolver.setSensitive(proxy.getSensitive()); + resolver.setExcluded(proxy.getSensitive()); return resolver; } diff --git a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java index a0295e10..7fd4e976 100644 --- a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java +++ b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java @@ -40,6 +40,7 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.client.SimpleClientHttpRequestFactory; @@ -56,8 +57,7 @@ import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest(properties = { "spring.cloud.gateway.proxy.auto-forward=Baz" }, - webEnvironment = WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ContextConfiguration(classes = TestApplication.class) public class ProductionConfigurationTests { @@ -112,6 +112,21 @@ public class ProductionConfigurationTests { .isEqualTo("host=localhost:" + port + ";foo"); } + @Test + public void postJsonWithWhitespace() { + var json = """ + { + "foo": "bar" + }"""; + + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setContentLength(json.length()); + var request = new HttpEntity<>(json, headers); + assertThat(rest.postForEntity("/proxy/checkContentLength", request, Void.class).getStatusCode()) + .isEqualTo(HttpStatus.OK); + } + @Test public void forward() { assertThat(rest.getForObject("/forward/foos/0", Foo.class).getName()).isEqualTo("bye"); @@ -424,7 +439,7 @@ public class ProductionConfigurationTests { @GetMapping("/proxy/headers") @SuppressWarnings("Duplicates") public ResponseEntity>> headers(ProxyExchange>> proxy) { - proxy.sensitive("foo", "hello"); + proxy.excluded("foo", "hello"); proxy.header("bar", "hello"); proxy.header("abc", "123"); proxy.header("hello", "world"); @@ -440,6 +455,12 @@ public class ProductionConfigurationTests { return proxy.uri(home.toString() + "/headers").get(); } + @PostMapping("/proxy/checkContentLength") + public ResponseEntity checkContentLength( + ProxyExchange proxy) { + return proxy.uri(home.toString() + "/checkContentLength").post(); + } + private ResponseEntity first(ResponseEntity> response) { return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()) .body(response.getBody().iterator().next()); @@ -484,6 +505,15 @@ public class ProductionConfigurationTests { return Arrays.asList(new Bar(custom + foos.iterator().next().getName())); } + @PostMapping("/checkContentLength") + public ResponseEntity checkContentLength(@RequestHeader(name = "Content-Length", required = false) Integer contentLength, + @RequestBody String json) { + if (contentLength != null && contentLength != json.length()) { + return ResponseEntity.badRequest().build(); + } + return ResponseEntity.ok().build(); + } + @GetMapping("/headers") public Map> headers(@RequestHeader HttpHeaders headers) { return new LinkedMultiValueMap<>(headers); diff --git a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/ProxyExchange.java b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/ProxyExchange.java index 62cb7f07..9d99445c 100644 --- a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/ProxyExchange.java +++ b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/ProxyExchange.java @@ -64,11 +64,11 @@ import org.springframework.web.server.ServerWebExchange; * *

* By default the incoming request body and headers are sent intact to the downstream - * service (with the exception of "sensitive" headers). To manipulate the downstream + * service (with the exception of "excluded" headers). To manipulate the downstream * request there are "builder" style methods in {@link ProxyExchange}, but only the - * {@link #uri(String)} is mandatory. You can change the sensitive headers by calling the - * {@link #sensitive(String...)} method (Authorization and Cookie are sensitive by - * default). + * {@link #uri(String)} is mandatory. You can change the excluded headers by calling the + * {@link #excluded(String...)} method (the argument resolver will populate these with + * some sensible defaults). *

*

* The type parameter T in ProxyExchange<T> is the type of @@ -111,12 +111,6 @@ import org.springframework.web.server.ServerWebExchange; */ public class ProxyExchange { - /** - * Contains headers that are considered case-sensitive by default. - */ - public static Set DEFAULT_SENSITIVE = Collections - .unmodifiableSet(new HashSet<>(Arrays.asList("cookie", "authorization"))); - private HttpMethod httpMethod; private URI uri; @@ -131,7 +125,7 @@ public class ProxyExchange { private BindingContext bindingContext; - private Set sensitive; + private Set excluded; private HttpHeaders headers = new HttpHeaders(); @@ -197,19 +191,19 @@ public class ProxyExchange { } /** - * Sets the names of sensitive headers that are not passed downstream to the backend + * Sets the names of excluded headers that are not passed downstream to the backend * service. - * @param names the names of sensitive headers + * @param names the names of excluded headers * @return this for convenience */ - public ProxyExchange sensitive(String... names) { - if (this.sensitive == null) { - this.sensitive = new HashSet<>(); + public ProxyExchange excluded(String... names) { + if (this.excluded == null) { + this.excluded = new HashSet<>(); } - this.sensitive.clear(); + this.excluded.clear(); for (String name : names) { - this.sensitive.add(name.toLowerCase()); + this.excluded.add(name.toLowerCase()); } return this; } @@ -389,8 +383,8 @@ public class ProxyExchange { } private Set filterHeaderKeys(HttpHeaders headers) { - final Set sensitiveHeaders = this.sensitive != null ? this.sensitive : DEFAULT_SENSITIVE; - return headers.keySet().stream().filter(header -> !sensitiveHeaders.contains(header.toLowerCase())) + final Set excludedHeaders = this.excluded != null ? this.excluded : Collections.emptySet(); + return headers.keySet().stream().filter(header -> !excludedHeaders.contains(header.toLowerCase())) .collect(Collectors.toSet()); } diff --git a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyExchangeArgumentResolver.java b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyExchangeArgumentResolver.java index 7c37b83d..16430803 100644 --- a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyExchangeArgumentResolver.java +++ b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyExchangeArgumentResolver.java @@ -44,7 +44,7 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol private Set autoForwardedHeaders; - private Set sensitive; + private Set excluded; public ProxyExchangeArgumentResolver(WebClient builder) { this.rest = builder; @@ -58,8 +58,8 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol this.autoForwardedHeaders = autoForwardedHeaders; } - public void setSensitive(Set sensitive) { - this.sensitive = sensitive; + public void setExcluded(Set excluded) { + this.excluded = excluded; } @Override @@ -87,8 +87,8 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol if (this.autoForwardedHeaders.size() > 0) { proxy.headers(extractAutoForwardedHeaders(exchange)); } - if (sensitive != null) { - proxy.sensitive(sensitive.toArray(new String[0])); + if (excluded != null) { + proxy.excluded(excluded.toArray(new String[0])); } return Mono.just(proxy); } diff --git a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyProperties.java b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyProperties.java index d7796a90..c45e2949 100644 --- a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyProperties.java +++ b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyProperties.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders; * * @author Dave Syer * @author Tim Ysewyn + * @author Joris Kuipers * */ @ConfigurationProperties("spring.cloud.gateway.proxy") @@ -49,7 +50,12 @@ public class ProxyProperties { /** * A set of sensitive header names that will not be sent downstream by default. */ - private Set sensitive = null; + private Set sensitive = Set.of("cookie", "authorization"); + + /** + * A set of header names that will not be sent downstream because they could be problematic. + */ + private Set skipped = Set.of("content-length", "host"); public Map getHeaders() { return headers; @@ -75,6 +81,14 @@ public class ProxyProperties { this.sensitive = sensitive; } + public Set getSkipped() { + return skipped; + } + + public void setSkipped(Set skipped) { + this.skipped = skipped; + } + public HttpHeaders convertHeaders() { HttpHeaders headers = new HttpHeaders(); for (String key : this.headers.keySet()) { diff --git a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java index 89011553..d5f79dce 100644 --- a/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java +++ b/spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyResponseAutoConfiguration.java @@ -16,7 +16,9 @@ package org.springframework.cloud.gateway.webflux.config; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -57,7 +59,14 @@ public class ProxyResponseAutoConfiguration implements WebFluxConfigurer { ProxyExchangeArgumentResolver resolver = new ProxyExchangeArgumentResolver(template); resolver.setHeaders(proxy.convertHeaders()); resolver.setAutoForwardedHeaders(proxy.getAutoForward()); - resolver.setSensitive(proxy.getSensitive()); // can be null + Set excludedHeaderNames = new HashSet<>(); + if (proxy.getSensitive() != null) { + excludedHeaderNames.addAll(proxy.getSensitive()); + } + if (proxy.getSkipped() != null) { + excludedHeaderNames.addAll(proxy.getSkipped()); + } + resolver.setExcluded(excludedHeaderNames); return resolver; } diff --git a/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java b/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java index bdf880e6..972b6f07 100644 --- a/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java +++ b/spring-cloud-gateway-webflux/src/test/java/org/springframework/cloud/gateway/webflux/ProductionConfigurationTests.java @@ -58,7 +58,7 @@ import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest(properties = { "spring.cloud.gateway.proxy.auto-forward=baz" }, +@SpringBootTest(properties = { "spring.cloud.gateway.proxy.skipped=host" }, webEnvironment = WebEnvironment.RANDOM_PORT) @ContextConfiguration(classes = TestApplication.class) @DirtiesContext @@ -119,6 +119,21 @@ public class ProductionConfigurationTests { .isEqualTo("host=localhost:" + port + ";foo"); } + @Test + public void postJsonWithWhitespace() { + var json = """ + { + "foo": "bar" + }"""; + + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setContentLength(json.length()); + var request = new HttpEntity<>(json, headers); + assertThat(rest.postForEntity("/proxy/checkContentLength", request, Void.class).getStatusCode()) + .isEqualTo(HttpStatus.OK); + } + @Test public void forwardPost() throws Exception { assertThat(rest.postForObject("/proxy/forward/0", Collections.singletonMap("name", "foo"), Bar.class).getName()) @@ -337,7 +352,7 @@ public class ProductionConfigurationTests { @GetMapping("/proxy/headers") public Mono>>> headers( ProxyExchange>> proxy) { - proxy.sensitive("foo", "hello"); + proxy.excluded("foo", "hello"); proxy.header("bar", "hello"); proxy.header("abc", "123"); proxy.header("hello", "world"); @@ -353,6 +368,12 @@ public class ProductionConfigurationTests { return proxy.uri(home.toString() + "/headers").get(); } + @PostMapping("/proxy/checkContentLength") + public Mono> checkContentLength( + ProxyExchange proxy) { + return proxy.uri(home.toString() + "/checkContentLength").post(); + } + private ResponseEntity first(ResponseEntity> response) { return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()) .body(response.getBody().iterator().next()); @@ -408,6 +429,15 @@ public class ProductionConfigurationTests { return Arrays.asList(new Bar(custom + foos.iterator().next().getName())); } + @PostMapping("/checkContentLength") + public ResponseEntity checkContentLength(@RequestHeader(name = "Content-Length", required = false) Integer contentLength, + @RequestBody String json) { + if (contentLength != null && contentLength != json.length()) { + return ResponseEntity.badRequest().build(); + } + return ResponseEntity.ok().build(); + } + @GetMapping("/headers") public Map> headers(@RequestHeader HttpHeaders headers) { return headers;