diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 26af62ef..fc0cdf68 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.config; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Set; import com.netflix.hystrix.HystrixObservableCommand; import io.netty.channel.ChannelOption; @@ -92,6 +93,9 @@ import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayF import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.rewrite.GzipMessageBodyResolver; +import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyDecoder; +import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncoder; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter; @@ -441,8 +445,10 @@ public class GatewayAutoConfiguration { @Bean public ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory( - ServerCodecConfigurer codecConfigurer) { - return new ModifyResponseBodyGatewayFilterFactory(codecConfigurer); + ServerCodecConfigurer codecConfigurer, Set bodyDecoders, + Set bodyEncoders) { + return new ModifyResponseBodyGatewayFilterFactory(codecConfigurer, bodyDecoders, + bodyEncoders); } @Bean @@ -560,6 +566,11 @@ public class GatewayAutoConfiguration { return new RequestHeaderSizeGatewayFilterFactory(); } + @Bean + public GzipMessageBodyResolver gzipMessageBodyResolver() { + return new GzipMessageBodyResolver(); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HttpClient.class) protected static class NettyConfiguration { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/GzipMessageBodyResolver.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/GzipMessageBodyResolver.java new file mode 100644 index 00000000..63a9d806 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/GzipMessageBodyResolver.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory.rewrite; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.FileCopyUtils; + +public class GzipMessageBodyResolver implements MessageBodyDecoder, MessageBodyEncoder { + + @Override + public String encodingType() { + return "gzip"; + } + + @Override + public byte[] decode(byte[] encoded) { + try { + ByteArrayInputStream bis = new ByteArrayInputStream(encoded); + GZIPInputStream gis = new GZIPInputStream(bis); + return FileCopyUtils.copyToByteArray(gis); + } + catch (IOException e) { + throw new IllegalStateException("couldn't decode body from gzip", e); + } + } + + @Override + public byte[] encode(DataBuffer original) { + try { + ByteArrayOutputStream bis = new ByteArrayOutputStream(); + GZIPOutputStream gos = new GZIPOutputStream(bis); + FileCopyUtils.copy(original.asInputStream(), gos); + return bis.toByteArray(); + } + catch (IOException e) { + throw new IllegalStateException("couldn't encode body to gzip", e); + } + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/MessageBodyDecoder.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/MessageBodyDecoder.java new file mode 100644 index 00000000..6ed438d4 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/MessageBodyDecoder.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory.rewrite; + +/** + * Decoder that is used to decode message body in case it's encoding from Content-Encoding + * header matches encoding returned by {@code encodingType()} call. + */ +public interface MessageBodyDecoder { + + byte[] decode(byte[] encoded); + + String encodingType(); + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/MessageBodyEncoder.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/MessageBodyEncoder.java new file mode 100644 index 00000000..08b65f31 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/MessageBodyEncoder.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory.rewrite; + +import org.springframework.core.io.buffer.DataBuffer; + +/** + * Encoder that is used to encode message body in case it's encoding from Content-Encoding + * header matches encoding returned by {@code encodingType()} call. + */ +public interface MessageBodyEncoder { + + byte[] encode(DataBuffer original); + + String encodingType(); + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java index 0e4d7fda..3352e234 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java @@ -16,11 +16,16 @@ package org.springframework.cloud.gateway.filter.factory.rewrite; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; @@ -30,6 +35,8 @@ import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.support.BodyInserterContext; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -44,6 +51,7 @@ import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; +import static java.util.function.Function.identity; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR; @@ -56,15 +64,35 @@ public class ModifyResponseBodyGatewayFilterFactory extends @Nullable private final ServerCodecConfigurer codecConfigurer; + private final Map messageBodyDecoders; + + private final Map messageBodyEncoders; + @Deprecated public ModifyResponseBodyGatewayFilterFactory() { super(Config.class); this.codecConfigurer = null; + messageBodyDecoders = Collections.emptyMap(); + messageBodyEncoders = Collections.emptyMap(); } + @Deprecated public ModifyResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) { super(Config.class); this.codecConfigurer = codecConfigurer; + messageBodyDecoders = Collections.emptyMap(); + messageBodyEncoders = Collections.emptyMap(); + } + + public ModifyResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer, + Set messageBodyDecoders, + Set messageBodyEncoders) { + super(Config.class); + this.codecConfigurer = codecConfigurer; + this.messageBodyDecoders = messageBodyDecoders.stream() + .collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity())); + this.messageBodyEncoders = messageBodyEncoders.stream() + .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); } @Override @@ -175,79 +203,14 @@ public class ModifyResponseBodyGatewayFilterFactory extends @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - return chain.filter(exchange.mutate().response(decorate(exchange)).build()); + return chain.filter(exchange.mutate() + .response(new ModifiedServerHttpResponse(exchange, config)).build()); } @SuppressWarnings("unchecked") + @Deprecated ServerHttpResponse decorate(ServerWebExchange exchange) { - return new ServerHttpResponseDecorator(exchange.getResponse()) { - - @Override - public Mono writeWith(Publisher body) { - - Class inClass = config.getInClass(); - Class outClass = config.getOutClass(); - - String originalResponseContentType = exchange - .getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); - HttpHeaders httpHeaders = new HttpHeaders(); - // explicitly add it in this way instead of - // 'httpHeaders.setContentType(originalResponseContentType)' - // this will prevent exception in case of using non-standard media - // types like "Content-Type: image" - httpHeaders.add(HttpHeaders.CONTENT_TYPE, - originalResponseContentType); - - ClientResponse clientResponse = prepareClientResponse(body, - httpHeaders); - - // TODO: flux or mono - Mono modifiedBody = clientResponse.bodyToMono(inClass) - .flatMap(originalBody -> config.getRewriteFunction() - .apply(exchange, originalBody)) - .switchIfEmpty(Mono.defer(() -> (Mono) config - .getRewriteFunction().apply(exchange, null))); - - BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, - outClass); - CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage( - exchange, exchange.getResponse().getHeaders()); - return bodyInserter.insert(outputMessage, new BodyInserterContext()) - .then(Mono.defer(() -> { - Flux messageBody = outputMessage.getBody(); - HttpHeaders headers = getDelegate().getHeaders(); - if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { - messageBody = messageBody.doOnNext(data -> headers - .setContentLength(data.readableByteCount())); - } - // TODO: fail if isStreamingMediaType? - return getDelegate().writeWith(messageBody); - })); - } - - @Override - public Mono writeAndFlushWith( - Publisher> body) { - return writeWith(Flux.from(body).flatMapSequential(p -> p)); - } - - private ClientResponse prepareClientResponse( - Publisher body, HttpHeaders httpHeaders) { - ClientResponse.Builder builder; - if (codecConfigurer != null) { - builder = ClientResponse.create( - exchange.getResponse().getStatusCode(), - codecConfigurer.getReaders()); - } - else { - builder = ClientResponse - .create(exchange.getResponse().getStatusCode()); - } - return builder.headers(headers -> headers.putAll(httpHeaders)) - .body(Flux.from(body)).build(); - } - - }; + return new ModifiedServerHttpResponse(exchange, config); } @Override @@ -271,6 +234,132 @@ public class ModifyResponseBodyGatewayFilterFactory extends } + protected class ModifiedServerHttpResponse extends ServerHttpResponseDecorator { + + private final ServerWebExchange exchange; + + private final Config config; + + public ModifiedServerHttpResponse(ServerWebExchange exchange, Config config) { + super(exchange.getResponse()); + this.exchange = exchange; + this.config = config; + } + + @SuppressWarnings("unchecked") + @Override + public Mono writeWith(Publisher body) { + + Class inClass = config.getInClass(); + Class outClass = config.getOutClass(); + + String originalResponseContentType = exchange + .getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); + HttpHeaders httpHeaders = new HttpHeaders(); + // explicitly add it in this way instead of + // 'httpHeaders.setContentType(originalResponseContentType)' + // this will prevent exception in case of using non-standard media + // types like "Content-Type: image" + httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType); + + ClientResponse clientResponse = prepareClientResponse(body, httpHeaders); + + // TODO: flux or mono + Mono modifiedBody = extractBody(exchange, clientResponse, inClass) + .flatMap(originalBody -> config.getRewriteFunction().apply(exchange, + originalBody)) + .switchIfEmpty(Mono.defer(() -> (Mono) config.getRewriteFunction() + .apply(exchange, null))); + + BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, + outClass); + CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, + exchange.getResponse().getHeaders()); + return bodyInserter.insert(outputMessage, new BodyInserterContext()) + .then(Mono.defer(() -> { + Mono messageBody = writeBody(getDelegate(), + outputMessage, outClass); + HttpHeaders headers = getDelegate().getHeaders(); + if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING) + || headers.containsKey(HttpHeaders.CONTENT_LENGTH)) { + messageBody = messageBody.doOnNext(data -> headers + .setContentLength(data.readableByteCount())); + } + // TODO: fail if isStreamingMediaType? + return getDelegate().writeWith(messageBody); + })); + } + + @Override + public Mono writeAndFlushWith( + Publisher> body) { + return writeWith(Flux.from(body).flatMapSequential(p -> p)); + } + + private ClientResponse prepareClientResponse(Publisher body, + HttpHeaders httpHeaders) { + ClientResponse.Builder builder; + if (codecConfigurer != null) { + builder = ClientResponse.create(exchange.getResponse().getStatusCode(), + codecConfigurer.getReaders()); + } + else { + builder = ClientResponse.create(exchange.getResponse().getStatusCode()); + } + return builder.headers(headers -> headers.putAll(httpHeaders)) + .body(Flux.from(body)).build(); + } + + private Mono extractBody(ServerWebExchange exchange, + ClientResponse clientResponse, Class inClass) { + // if inClass is byte[] then just return body, otherwise check if + // decoding required + if (byte[].class.isAssignableFrom(inClass)) { + return clientResponse.bodyToMono(inClass); + } + + List encodingHeaders = exchange.getResponse().getHeaders() + .getOrEmpty(HttpHeaders.CONTENT_ENCODING); + for (String encoding : encodingHeaders) { + MessageBodyDecoder decoder = messageBodyDecoders.get(encoding); + if (decoder != null) { + return clientResponse.bodyToMono(byte[].class) + .publishOn(Schedulers.parallel()).map(decoder::decode) + .map(bytes -> exchange.getResponse().bufferFactory() + .wrap(bytes)) + .map(buffer -> prepareClientResponse(Mono.just(buffer), + exchange.getResponse().getHeaders())) + .flatMap(response -> response.bodyToMono(inClass)); + } + } + + return clientResponse.bodyToMono(inClass); + } + + private Mono writeBody(ServerHttpResponse httpResponse, + CachedBodyOutputMessage message, Class outClass) { + Mono response = DataBufferUtils.join(message.getBody()); + if (byte[].class.isAssignableFrom(outClass)) { + return response; + } + + List encodingHeaders = httpResponse.getHeaders() + .getOrEmpty(HttpHeaders.CONTENT_ENCODING); + for (String encoding : encodingHeaders) { + MessageBodyEncoder encoder = messageBodyEncoders.get(encoding); + if (encoder != null) { + DataBufferFactory dataBufferFactory = httpResponse.bufferFactory(); + response = response.publishOn(Schedulers.parallel()) + .map(encoder::encode).map(dataBufferFactory::wrap); + break; + } + } + + return response; + } + + } + @Deprecated @SuppressWarnings("unchecked") public class ResponseAdapter implements ClientHttpResponse { diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryGzipTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryGzipTests.java new file mode 100644 index 00000000..a779fc5b --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryGzipTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.factory.rewrite; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class ModifyResponseBodyGatewayFilterFactoryGzipTests extends BaseWebClientTests { + + @Test + public void testModificationOfResponseBody() { + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/gzip").build(true) + .toUri(); + + testClient.get().uri(uri).header("Host", "www.modifyresponsebodyjava.org") + .accept(MediaType.APPLICATION_JSON).exchange().expectBody() + .json("{\"length\":25,\"value\":\"\\\"httpbin compatible home\\\"\"}"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes().route("modify_response_java_test_gzip", + r -> r.path("/gzip").and().host("www.modifyresponsebodyjava.org") + .filters(f -> f.modifyResponseBody(String.class, Map.class, + (webExchange, originalResponse) -> { + Map modifiedResponse = new HashMap<>(); + modifiedResponse.put("value", originalResponse); + modifiedResponse.put("length", + originalResponse.length()); + return Mono.just(modifiedResponse); + })) + .uri(uri)) + .build(); + } + + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryUnitTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryUnitTests.java index 02040ebf..2ccfbe7b 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryUnitTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryUnitTests.java @@ -22,6 +22,7 @@ import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory.Config; import org.springframework.http.codec.support.DefaultServerCodecConfigurer; +import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; public class ModifyResponseBodyGatewayFilterFactoryUnitTests { @@ -33,7 +34,7 @@ public class ModifyResponseBodyGatewayFilterFactoryUnitTests { config.setOutClass(Integer.class); config.setNewContentType("mycontenttype"); GatewayFilter filter = new ModifyResponseBodyGatewayFilterFactory( - new DefaultServerCodecConfigurer()).apply(config); + new DefaultServerCodecConfigurer(), emptySet(), emptySet()).apply(config); assertThat(filter.toString()).contains("String").contains("Integer") .contains("mycontenttype"); } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java index 530c64e0..6be85d32 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java @@ -16,22 +16,32 @@ package org.springframework.cloud.gateway.test; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.zip.GZIPOutputStream; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.Part; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.FileCopyUtils; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -46,22 +56,23 @@ public class HttpBinCompatibleController { private static final Log log = LogFactory.getLog(HttpBinCompatibleController.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + @RequestMapping("/") public String home() { return "httpbin compatible home"; } - @RequestMapping(path = "/headers", method = { RequestMethod.GET, RequestMethod.POST }, - produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(path = "/headers", method = { RequestMethod.GET, + RequestMethod.POST }, produces = MediaType.APPLICATION_JSON_VALUE) public Map headers(ServerWebExchange exchange) { Map result = new HashMap<>(); result.put("headers", getHeaders(exchange)); return result; } - @RequestMapping(path = "/multivalueheaders", - method = { RequestMethod.GET, RequestMethod.POST }, - produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(path = "/multivalueheaders", method = { RequestMethod.GET, + RequestMethod.POST }, produces = MediaType.APPLICATION_JSON_VALUE) public Map multiValueHeaders(ServerWebExchange exchange) { Map result = new HashMap<>(); result.put("headers", exchange.getRequest().getHeaders()); @@ -75,8 +86,7 @@ public class HttpBinCompatibleController { return Mono.just(get(exchange)).delayElement(Duration.ofSeconds(delay)); } - @RequestMapping(path = "/anything/{anything}", - produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(path = "/anything/{anything}", produces = MediaType.APPLICATION_JSON_VALUE) public Map anything(ServerWebExchange exchange, @PathVariable(required = false) String anything) { return get(exchange); @@ -97,8 +107,7 @@ public class HttpBinCompatibleController { return result; } - @RequestMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public Mono> postFormData( @RequestBody Mono> parts) { // StringDecoder decoder = StringDecoder.allMimeTypes(true); @@ -114,16 +123,13 @@ public class HttpBinCompatibleController { }).map(files -> Collections.singletonMap("files", files)); } - @RequestMapping(path = "/post", - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(path = "/post", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public Mono> postUrlEncoded(ServerWebExchange exchange) throws IOException { return post(exchange, null); } - @RequestMapping(path = "/post", method = RequestMethod.POST, - produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(path = "/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public Mono> post(ServerWebExchange exchange, @RequestBody(required = false) String body) throws IOException { HashMap ret = new HashMap<>(); @@ -153,6 +159,29 @@ public class HttpBinCompatibleController { return Mono.empty(); } + @RequestMapping(path = "/gzip", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono gzip(ServerWebExchange exchange) throws IOException { + if (log.isDebugEnabled()) { + log.debug("httpbin /gzip"); + } + + String jsonResponse = OBJECT_MAPPER.writeValueAsString("httpbin compatible home"); + byte[] bytes = jsonResponse.getBytes(StandardCharsets.UTF_8); + + ServerHttpResponse response = exchange.getResponse(); + response.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip"); + DataBufferFactory dataBufferFactory = response.bufferFactory(); + response.setStatusCode(HttpStatus.OK); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + GZIPOutputStream is = new GZIPOutputStream(bos); + FileCopyUtils.copy(bytes, is); + + byte[] gzippedResponse = bos.toByteArray(); + DataBuffer wrap = dataBufferFactory.wrap(gzippedResponse); + return response.writeWith(Flux.just(wrap)); + } + public Map getHeaders(ServerWebExchange exchange) { return exchange.getRequest().getHeaders().toSingleValueMap(); }