From ebbd041cb99a5e8f183d4350e99bf034945247a6 Mon Sep 17 00:00:00 2001 From: sullis Date: Sat, 23 Mar 2024 14:01:30 -0700 Subject: [PATCH] verify http response compression - validate gzip responses - validate brotli responses --- .../http2/pom.xml | 50 +++++++++++ .../gateway/tests/http2/Http2Application.java | 7 ++ .../http2/src/main/resources/application.yml | 2 + .../tests/http2/Http2ApplicationTests.java | 83 +++++++++++++++++++ 4 files changed, 142 insertions(+) diff --git a/spring-cloud-gateway-integration-tests/http2/pom.xml b/spring-cloud-gateway-integration-tests/http2/pom.xml index dcbd888e1c..1dd529ce0f 100644 --- a/spring-cloud-gateway-integration-tests/http2/pom.xml +++ b/spring-cloud-gateway-integration-tests/http2/pom.xml @@ -11,6 +11,7 @@ Spring Cloud Gateway HTTP2 Integration Test + 1.16.0 @@ -37,6 +38,49 @@ io.netty netty-tcnative-boringssl-static + + + com.aayushatharva.brotli4j + brotli4j + ${brotli4j.version} + runtime + + + com.aayushatharva.brotli4j + native-linux-x86_64 + ${brotli4j.version} + runtime + + + com.aayushatharva.brotli4j + native-linux-aarch64 + ${brotli4j.version} + runtime + + + com.aayushatharva.brotli4j + native-linux-armv7 + ${brotli4j.version} + runtime + + + com.aayushatharva.brotli4j + native-osx-x86_64 + ${brotli4j.version} + runtime + + + com.aayushatharva.brotli4j + native-osx-aarch64 + ${brotli4j.version} + runtime + + + com.aayushatharva.brotli4j + native-windows-x86_64 + ${brotli4j.version} + runtime + org.springframework.boot spring-boot-starter-test @@ -56,6 +100,12 @@ assertj-core test + + commons-io + commons-io + 2.15.1 + test + diff --git a/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java b/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java index 0fe5ed34c1..14c0142d0c 100644 --- a/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java +++ b/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java @@ -16,6 +16,7 @@ package org.springframework.cloud.gateway.tests.http2; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,6 +33,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** @@ -52,6 +54,11 @@ public String hello() { return "Hello"; } + @GetMapping("data") + public String getData(@RequestParam(name = "size", defaultValue = "5000", required = true) Integer size) { + return StringUtils.repeat("a", size); + } + @Bean public RouteLocator myRouteLocator(RouteLocatorBuilder builder) { return builder.routes().route(r -> r.path("/myprefix/**").filters(f -> f.stripPrefix(1)).uri("lb://myservice")) diff --git a/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml index 8438b1d581..948fc6e4b3 100644 --- a/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml +++ b/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml @@ -4,6 +4,8 @@ logging: reactor.netty.http.client: DEBUG server: + compression: + enabled: true ssl: key-store: classpath:sample.jks key-store-password: secret diff --git a/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java index f609a031b3..583b2bde55 100644 --- a/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java +++ b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java @@ -16,12 +16,26 @@ package org.springframework.cloud.gateway.tests.http2; +import com.aayushatharva.brotli4j.decoder.DecoderJNI; +import com.aayushatharva.brotli4j.decoder.DirectDecompress; +import io.netty.handler.codec.compression.Brotli; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.time.Duration; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import java.util.List; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.io.IOUtils; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.HttpHeaders; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.netty.http.Http2SslContextSpec; @@ -63,6 +77,75 @@ public void http2Works(CapturedOutput output) { Assertions.assertThat(output).contains("Negotiated application-level protocol [h2]", "PRI * HTTP/2.0"); } + @ParameterizedTest + @MethodSource("httpContentCompressionParameters") + public void httpContentCompression(String acceptEncoding, String expectedContentEncoding) throws Throwable { + Brotli.ensureAvailability(); + final int uncompressedResponseSize = 6_666; + final String expectedUncompressedResponse = StringUtils.repeat('a', uncompressedResponseSize); + WebClient client = WebClient.builder().clientConnector(new ReactorClientHttpConnector(getHttpClient())).build(); + Mono> responseEntityMono = client.get() + .uri(uriBuilder -> + uriBuilder.host("localhost").path("/myprefix/data").port(port).scheme("https").queryParam("size", uncompressedResponseSize).build()) + .header("Accept-Encoding", acceptEncoding) + .retrieve() + .toEntity(byte[].class); + StepVerifier.create(responseEntityMono).assertNext(entity -> { + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + HttpHeaders responseHeaders = entity.getHeaders(); + assertThat(responseHeaders.getContentType().toString()).isEqualTo("text/plain;charset=UTF-8"); + List contentEncoding = responseHeaders.get("Content-Encoding"); + if (expectedContentEncoding == null) { + assertThat(contentEncoding).isNull(); + assertThat(entity.hasBody()).isTrue(); + assertThat(new String(entity.getBody())).isEqualTo(expectedUncompressedResponse); + assertThat(responseHeaders.getContentLength()).isEqualTo(uncompressedResponseSize); + } else { + assertThat(contentEncoding).containsExactly(expectedContentEncoding); + assertThat(entity.hasBody()).isTrue(); + String decompressedResponseBody = new String(decompress(entity)); + assertThat(decompressedResponseBody).isEqualTo(expectedUncompressedResponse); + long contentLength = responseHeaders.getContentLength(); + assertThat(contentLength).isGreaterThan(0); + assertThat(contentLength).isLessThan(uncompressedResponseSize); + } + }).expectComplete().verify(); + + } + + private static byte[] decompress(ResponseEntity entity) { + if (!entity.hasBody()) { + return null; + } + String contentEncoding = entity.getHeaders().get("Content-Encoding").get(0); + byte[] compressedData = entity.getBody(); + try { + if ("br".equals(contentEncoding)) { + DirectDecompress directDecompress = DirectDecompress.decompress(compressedData); + assertThat(directDecompress.getResultStatus()).isEqualTo(DecoderJNI.Status.DONE); + return directDecompress.getDecompressedData(); + } else if ("gzip".equals(contentEncoding)) { + ByteArrayInputStream input = new ByteArrayInputStream(compressedData); + GZIPInputStream gzInput = new GZIPInputStream(input); + return IOUtils.toByteArray(gzInput); + } else { + throw new IllegalStateException("unexpected contentEncoding: " + contentEncoding); + } + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + public static Stream httpContentCompressionParameters() { + return Stream.of(Arguments.of("", null), + Arguments.of("br", "br"), + Arguments.of("gzip", "gzip"), + Arguments.of("br, gzip", "br"), + Arguments.of("gzip, br", "br"), + Arguments.of("garbage, gzip", "gzip"), + Arguments.of("garbage", null)); + } + public static void assertResponse(String uri, String expected) { WebClient client = WebClient.builder().clientConnector(new ReactorClientHttpConnector(getHttpClient())).build(); Mono> responseEntityMono = client.get().uri(uri).retrieve().toEntity(String.class);