From fa70b31f1ed80bf8a30e598b8dc62b9cd977547e Mon Sep 17 00:00:00 2001 From: Alvin <329772643@qq.com> Date: Wed, 20 May 2020 03:24:53 +0800 Subject: [PATCH 1/5] Cache the success routes until next successful refresh. (#1602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refreshWorksWhenFirstRefreshSuccessAndOtherError * update format * use isSuccess instred of Enum Co-authored-by: 田雪峰 Fixes gh-600 --- .../event/RefreshRoutesResultEvent.java | 45 ++++++++++++ .../gateway/route/CachingRouteLocator.java | 40 +++++++++- .../route/CachingRouteLocatorTests.java | 73 +++++++++++++++++++ 3 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/RefreshRoutesResultEvent.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/RefreshRoutesResultEvent.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/RefreshRoutesResultEvent.java new file mode 100644 index 00000000..031b0d92 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/RefreshRoutesResultEvent.java @@ -0,0 +1,45 @@ +/* + * 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.event; + +import org.springframework.context.ApplicationEvent; + +/** + * @author alvin + */ +public class RefreshRoutesResultEvent extends ApplicationEvent { + + private Throwable throwable; + + public RefreshRoutesResultEvent(Object source, Throwable throwable) { + super(source); + this.throwable = throwable; + } + + public RefreshRoutesResultEvent(Object source) { + super(source); + } + + public Throwable getThrowable() { + return throwable; + } + + public boolean isSuccess() { + return throwable == null; + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java index f5c8d636..cda9daf5 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java @@ -21,10 +21,15 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import reactor.cache.CacheFlux; import reactor.core.publisher.Flux; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; @@ -32,8 +37,10 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; /** * @author Spencer Gibb */ -public class CachingRouteLocator - implements Ordered, RouteLocator, ApplicationListener { +public class CachingRouteLocator implements Ordered, RouteLocator, + ApplicationListener, ApplicationEventPublisherAware { + + private static final Log log = LogFactory.getLog(CachingRouteLocator.class); private static final String CACHE_KEY = "routes"; @@ -43,6 +50,8 @@ public class CachingRouteLocator private final Map cache = new ConcurrentHashMap<>(); + private ApplicationEventPublisher applicationEventPublisher; + public CachingRouteLocator(RouteLocator delegate) { this.delegate = delegate; routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class) @@ -69,8 +78,25 @@ public class CachingRouteLocator @Override public void onApplicationEvent(RefreshRoutesEvent event) { - fetch().materialize().collect(Collectors.toList()) - .doOnNext(routes -> cache.put(CACHE_KEY, routes)).subscribe(); + try { + fetch().collect(Collectors.toList()).subscribe(list -> Flux.fromIterable(list) + .materialize().collect(Collectors.toList()).subscribe(signals -> { + applicationEventPublisher + .publishEvent(new RefreshRoutesResultEvent(this)); + cache.put(CACHE_KEY, signals); + }, throwable -> handleRefreshError(throwable))); + } + catch (Throwable e) { + handleRefreshError(e); + } + } + + private void handleRefreshError(Throwable throwable) { + if (log.isErrorEnabled()) { + log.error("Refresh routes error !!!", throwable); + } + applicationEventPublisher + .publishEvent(new RefreshRoutesResultEvent(this, throwable)); } @Deprecated @@ -83,4 +109,10 @@ public class CachingRouteLocator return 0; } + @Override + public void setApplicationEventPublisher( + ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/CachingRouteLocatorTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/CachingRouteLocatorTests.java index 4a0a408f..7308e655 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/CachingRouteLocatorTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/CachingRouteLocatorTests.java @@ -16,11 +16,17 @@ package org.springframework.cloud.gateway.route; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.junit.Test; import reactor.core.publisher.Flux; +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent; + import static org.assertj.core.api.Assertions.assertThat; public class CachingRouteLocatorTests { @@ -60,6 +66,73 @@ public class CachingRouteLocatorTests { assertThat(routes).containsExactly(route1, route2); } + @Test + public void refreshWorksWhenFirstRefreshSuccessAndOtherError() + throws InterruptedException { + Route route1 = route(1); + Route route2 = route(2); + + CachingRouteLocator locator = new CachingRouteLocator(new RouteLocator() { + int i = 0; + + @Override + public Flux getRoutes() { + if (i == 0) { + i++; + return Flux.just(route1); + } + else if (i == 1) { + i++; + return Flux.just(route2).map(route -> { + throw new RuntimeException("in chain."); + }); + } + else if (i == 2) { + i++; + throw new RuntimeException("call getRoutes error."); + } + return Flux.just(route2); + } + }); + + List routes = locator.getRoutes().collectList().block(); + assertThat(routes).containsExactly(route1); + + List resultEvents = new ArrayList<>(); + + waitUntilRefreshFinished(locator, resultEvents); + assertThat(resultEvents).hasSize(1); + assertThat(resultEvents.get(0).getThrowable().getCause().getMessage()) + .isEqualTo("in chain."); + assertThat(resultEvents.get(0).isSuccess()).isEqualTo(false); + assertThat(locator.getRoutes().collectList().block()).containsExactly(route1); + + waitUntilRefreshFinished(locator, resultEvents); + assertThat(resultEvents).hasSize(2); + assertThat(resultEvents.get(1).getThrowable().getMessage()) + .isEqualTo("call getRoutes error."); + assertThat(resultEvents.get(1).isSuccess()).isEqualTo(false); + assertThat(locator.getRoutes().collectList().block()).containsExactly(route1); + + waitUntilRefreshFinished(locator, resultEvents); + assertThat(resultEvents).hasSize(3); + assertThat(resultEvents.get(2).isSuccess()).isEqualTo(true); + assertThat(locator.getRoutes().collectList().block()).containsExactly(route2); + + } + + private void waitUntilRefreshFinished(CachingRouteLocator locator, + List resultEvents) throws InterruptedException { + CountDownLatch cdl = new CountDownLatch(1); + locator.setApplicationEventPublisher(o -> { + resultEvents.add((RefreshRoutesResultEvent) o); + cdl.countDown(); + }); + locator.onApplicationEvent(new RefreshRoutesEvent(this)); + + assertThat(cdl.await(5, TimeUnit.SECONDS)).isTrue(); + } + Route route(int id) { return Route.async().id(String.valueOf(id)).uri("http://localhost/" + id) .order(id).predicate(exchange -> true).build(); From e0b4d7e321d1a811fc9485fb59f8b8d08ec75310 Mon Sep 17 00:00:00 2001 From: Nikita Konev <3160384+nkonev@users.noreply.github.com> Date: Tue, 19 May 2020 22:26:29 +0300 Subject: [PATCH 2/5] make static inner classes in HttpClientProperties (#1618) --- .../gateway/config/HttpClientProperties.java | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java index b97d6ffa..dfc5c72c 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java @@ -276,7 +276,7 @@ public class HttpClientProperties { } - public class Proxy { + public static class Proxy { /** Hostname for proxy configuration of Netty HttpClient. */ private String host; @@ -345,7 +345,7 @@ public class HttpClientProperties { } - public class Ssl { + public static class Ssl { /** * Installs the netty InsecureTrustManagerFactory. This is insecure and not @@ -437,7 +437,7 @@ public class HttpClientProperties { CertificateFactory certificateFactory = CertificateFactory .getInstance("X.509"); ArrayList allCerts = new ArrayList<>(); - for (String trustedCert : ssl.getTrustedX509Certificates()) { + for (String trustedCert : getTrustedX509Certificates()) { try { URL url = ResourceUtils.getURL(trustedCert); Collection certs = certificateFactory @@ -459,14 +459,14 @@ public class HttpClientProperties { public KeyManagerFactory getKeyManagerFactory() { try { - if (ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) { + if (getKeyStore() != null && getKeyStore().length() > 0) { KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - char[] keyPassword = ssl.getKeyPassword() != null - ? ssl.getKeyPassword().toCharArray() : null; + char[] keyPassword = getKeyPassword() != null + ? getKeyPassword().toCharArray() : null; - if (keyPassword == null && ssl.getKeyStorePassword() != null) { - keyPassword = ssl.getKeyStorePassword().toCharArray(); + if (keyPassword == null && getKeyStorePassword() != null) { + keyPassword = getKeyStorePassword().toCharArray(); } keyManagerFactory.init(this.createKeyStore(), keyPassword); @@ -483,18 +483,17 @@ public class HttpClientProperties { public KeyStore createKeyStore() { try { - KeyStore store = ssl.getKeyStoreProvider() != null - ? KeyStore.getInstance(ssl.getKeyStoreType(), - ssl.getKeyStoreProvider()) - : KeyStore.getInstance(ssl.getKeyStoreType()); + KeyStore store = getKeyStoreProvider() != null + ? KeyStore.getInstance(getKeyStoreType(), getKeyStoreProvider()) + : KeyStore.getInstance(getKeyStoreType()); try { - URL url = ResourceUtils.getURL(ssl.getKeyStore()); - store.load(url.openStream(), ssl.getKeyStorePassword() != null - ? ssl.getKeyStorePassword().toCharArray() : null); + URL url = ResourceUtils.getURL(getKeyStore()); + store.load(url.openStream(), getKeyStorePassword() != null + ? getKeyStorePassword().toCharArray() : null); } catch (Exception e) { throw new WebServerException( - "Could not load key store ' " + ssl.getKeyStore() + "'", e); + "Could not load key store ' " + getKeyStore() + "'", e); } return store; @@ -598,7 +597,7 @@ public class HttpClientProperties { } - public class Websocket { + public static class Websocket { /** Max frame payload length. */ private Integer maxFramePayloadLength; From eedcceee203f16158fe7c3e9b25b61e7f05c39ae Mon Sep 17 00:00:00 2001 From: tooSun Date: Wed, 20 May 2020 04:06:36 +0800 Subject: [PATCH 3/5] Fix spring.codec.max-in-memory-size configuration is invalid for ReadBodyPredicateFactory. Fixes # 1658 (#1686) * Fix spring.codec.max-in-memory-size configuration is invalid for ReadBodyPredicateFactory. Co-authored-by: huangluob Fixes gh-1658 --- .../cloud/gateway/config/GatewayAutoConfiguration.java | 4 ++-- .../handler/predicate/ReadBodyPredicateFactory.java | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) 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 d1ef0917..c2a50f8e 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 @@ -394,8 +394,8 @@ public class GatewayAutoConfiguration { } @Bean - public ReadBodyPredicateFactory readBodyPredicateFactory() { - return new ReadBodyPredicateFactory(); + public ReadBodyPredicateFactory readBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) { + return new ReadBodyPredicateFactory(codecConfigurer.getReaders()); } @Bean diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java index 52f707f0..8c239024 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java @@ -46,11 +46,16 @@ public class ReadBodyPredicateFactory private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject"; - private static final List> messageReaders = HandlerStrategies - .withDefaults().messageReaders(); + private final List> messageReaders; public ReadBodyPredicateFactory() { super(Config.class); + this.messageReaders = HandlerStrategies.withDefaults().messageReaders(); + } + + public ReadBodyPredicateFactory(List> messageReaders) { + super(Config.class); + this.messageReaders = messageReaders; } @Override From 16944d61d03afa58677e15bfeb4378c5c4dc3894 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Thu, 10 Oct 2019 21:12:20 -0400 Subject: [PATCH 4/5] Adds a filter to replace the host header fixes gh-530 fixes gh-1344 --- .../main/asciidoc/spring-cloud-gateway.adoc | 27 ++++++ .../config/GatewayAutoConfiguration.java | 6 ++ ...RequestHostHeaderGatewayFilterFactory.java | 91 +++++++++++++++++++ .../route/builder/GatewayFilterSpec.java | 12 +++ ...stHostHeaderGatewayFilterFactoryTests.java | 70 ++++++++++++++ .../src/test/resources/application.yml | 9 ++ 6 files changed, 215 insertions(+) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactoryTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 3e6cec88..25130f29 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1596,6 +1596,33 @@ errorMessage` : `Request size is larger than permissible limit. Request size is NOTE: The default request size is set to five MB if not provided as a filter argument in the route definition. +=== The `SetRequestHost` `GatewayFilter` Factory + +There are certain situation when the host header may need to be overridden. In this situation, the `SetRequestHost` `GatewayFilter` factory can replace the existing host header with a specified vaue. +The filter takes a `host` parameter. +The following listing configures a `SetRequestHost` `GatewayFilter`: + +.application.yml +==== +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: set_request_host_header_route + uri: http://localhost:8080/headers + predicates: + - Path=/headers + filters: + - name: SetRequestHost + args: + host: example.org +---- +==== + +The `SetRequestHost` `GatewayFilter` factory replaces the value of the host header with `example.org`. + === Modify a Request Body `GatewayFilter` Factory You can use the `ModifyRequestBody` filter filter to modify the request body before it is sent downstream by the gateway. 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 c2a50f8e..f97fca84 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 @@ -90,6 +90,7 @@ import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilt import org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties; import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; @@ -525,6 +526,11 @@ public class GatewayAutoConfiguration { return new SetRequestHeaderGatewayFilterFactory(); } + @Bean + public SetRequestHostHeaderGatewayFilterFactory setRequestHostHeaderGatewayFilterFactory() { + return new SetRequestHostHeaderGatewayFilterFactory(); + } + @Bean public SetResponseHeaderGatewayFilterFactory setResponseHeaderGatewayFilterFactory() { return new SetResponseHeaderGatewayFilterFactory(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactory.java new file mode 100644 index 00000000..1024c651 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactory.java @@ -0,0 +1,91 @@ +/* + * 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; + +import java.util.Collections; +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE; + +/** + * @author Andrew Fitzgerald + */ +public class SetRequestHostHeaderGatewayFilterFactory extends + AbstractGatewayFilterFactory { + + public SetRequestHostHeaderGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Collections.singletonList("host"); + } + + @Override + public GatewayFilter apply(Config config) { + return new GatewayFilter() { + @Override + public Mono filter(ServerWebExchange exchange, + GatewayFilterChain chain) { + String value = ServerWebExchangeUtils.expand(exchange, config.getHost()); + + ServerHttpRequest request = exchange.getRequest().mutate() + .headers(httpHeaders -> { + httpHeaders.remove("Host"); + httpHeaders.add("Host", value); + }).build(); + + // Make sure the header we just set is preserved + exchange.getAttributes().put(PRESERVE_HOST_HEADER_ATTRIBUTE, true); + + return chain.filter(exchange.mutate().request(request).build()); + } + + @Override + public String toString() { + return filterToStringCreator( + SetRequestHostHeaderGatewayFilterFactory.this) + .append(config.getHost()).toString(); + } + }; + } + + public static class Config { + + private String host; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 9871d5cc..e41a69d0 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -63,6 +63,7 @@ import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilter import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; @@ -383,6 +384,17 @@ public class GatewayFilterSpec extends UriSpec { return filter(getBean(PreserveHostHeaderGatewayFilterFactory.class).apply()); } + /** + * A filter that will set the Host header to + * {@param hostName} on the outgoing request + * @param hostName the updated Host header value + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec setHostHeader(String hostName) { + return filter(getBean(SetRequestHostHeaderGatewayFilterFactory.class) + .apply(c -> c.setHost(hostName))); + } + /** * A filter that will return a redirect response back to the client. * @param status an HTTP status code, should be a {@code 300} series redirect diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactoryTests.java new file mode 100644 index 00000000..048f3e38 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestHostHeaderGatewayFilterFactoryTests.java @@ -0,0 +1,70 @@ +/* + * 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; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.cloud.gateway.test.TestUtils.getMap; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class SetRequestHostHeaderGatewayFilterFactoryTests extends BaseWebClientTests { + + @Test + public void setRequestHostHeaderFilterWorks() { + testClient.get().uri("/headers").header("Host", "www.setrequesthostheader.org") + .exchange().expectStatus().isOk().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).hasEntrySatisfying("Host", + val -> assertThat(val).isEqualTo("otherhost.io")); + }); + } + + @Test + public void toStringFormat() { + SetRequestHostHeaderGatewayFilterFactory.Config config = new SetRequestHostHeaderGatewayFilterFactory.Config(); + config.setHost("myhost"); + GatewayFilter filter = new SetRequestHostHeaderGatewayFilterFactory() + .apply(config); + assertThat(filter.toString()).contains("myhost"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + } + +} diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 0d176368..455782b7 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -364,6 +364,15 @@ spring: filters: - SecureHeaders + # ===================================== + - id: set_request_host_header_test + uri: ${test.uri} + predicates: + - Host=**.setrequesthostheader.org + - Path=/headers + filters: + - SetRequestHostHeader=otherhost.io + # ===================================== - id: set_path_test uri: ${test.uri} From e0a78c81f1a6a89488467102120535a2b6358ced Mon Sep 17 00:00:00 2001 From: fangfeikun <784945099@qq.com> Date: Sun, 19 Apr 2020 22:36:37 +0800 Subject: [PATCH 5/5] Releases body after sending modified body to downstream fails. Fixes gh-1520 --- .../rewrite/CachedBodyOutputMessage.java | 7 + ...ModifyRequestBodyGatewayFilterFactory.java | 15 +- ...dyGatewayFilterFactorySslTimeoutTests.java | 182 ++++++++++++++++++ 3 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactorySslTimeoutTests.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/CachedBodyOutputMessage.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/CachedBodyOutputMessage.java index 872a2764..cfd97597 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/CachedBodyOutputMessage.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/CachedBodyOutputMessage.java @@ -39,6 +39,8 @@ public class CachedBodyOutputMessage implements ReactiveHttpOutputMessage { private final HttpHeaders httpHeaders; + private boolean cached = false; + private Flux body = Flux.error(new IllegalStateException( "The body is not set. " + "Did handling complete with success?")); @@ -57,6 +59,10 @@ public class CachedBodyOutputMessage implements ReactiveHttpOutputMessage { return false; } + boolean isCached() { + return this.cached; + } + @Override public HttpHeaders getHeaders() { return this.httpHeaders; @@ -82,6 +88,7 @@ public class CachedBodyOutputMessage implements ReactiveHttpOutputMessage { public Mono writeWith(Publisher body) { this.body = Flux.from(body); + this.cached = true; return Mono.empty(); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java index 3c4b2dfc..653e9870 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java @@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.filter.factory.rewrite; import java.util.List; import java.util.Map; +import java.util.function.Function; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +28,7 @@ import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.cloud.gateway.support.BodyInserterContext; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.ServerCodecConfigurer; @@ -105,7 +107,9 @@ public class ModifyRequestBodyGatewayFilterFactory extends outputMessage); return chain .filter(exchange.mutate().request(decorator).build()); - })); + })).onErrorResume( + (Function>) throwable -> release( + exchange, outputMessage, throwable)); } @Override @@ -118,6 +122,15 @@ public class ModifyRequestBodyGatewayFilterFactory extends }; } + protected Mono release(ServerWebExchange exchange, + CachedBodyOutputMessage outputMessage, Throwable throwable) { + if (outputMessage.isCached()) { + return outputMessage.getBody().map(DataBufferUtils::release) + .then(Mono.error(throwable)); + } + return Mono.error(throwable); + } + ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) { return new ServerHttpRequestDecorator(exchange.getRequest()) { diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactorySslTimeoutTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactorySslTimeoutTests.java new file mode 100644 index 00000000..6e161982 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactorySslTimeoutTests.java @@ -0,0 +1,182 @@ +/* + * 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.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLException; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.internal.PlatformDependent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; + +import org.springframework.beans.factory.annotation.Autowired; +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.DependsOn; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author fangfeikun + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT, + properties = { "spring.cloud.gateway.httpclient.ssl.handshake-timeout=1ms", + "spring.main.allow-bean-definition-overriding=true" }) +@DirtiesContext +@ActiveProfiles("single-cert-ssl") +public class ModifyRequestBodyGatewayFilterFactorySslTimeoutTests + extends BaseWebClientTests { + + @Autowired + AtomicInteger releaseCount; + + @Before + public void setup() { + try { + SslContext sslContext = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); + HttpClient httpClient = HttpClient.create() + .secure(ssl -> ssl.sslContext(sslContext)); + setup(new ReactorClientHttpConnector(httpClient), + "https://localhost:" + port); + } + catch (SSLException e) { + throw new RuntimeException(e); + } + } + + @Test + public void modifyRequestBodySSLTimeout() { + testClient.post().uri("/post") + .header("Host", "www.modifyrequestbodyssltimeout.org") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .body(BodyInserters.fromValue("request")).exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody() + .jsonPath("message").isEqualTo("handshake timed out after 1ms"); + } + + @Test + public void modifyRequestBodyRelease() { + releaseCount.set(0); + // long initialUsedDirectMemory = PlatformDependent.usedDirectMemory(); + for (int i = 0; i < 10; i++) { + testClient.post().uri("/post") + .header("Host", "www.modifyrequestbodyssltimeout.org") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .body(BodyInserters.fromValue("request")).exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + long usedDirectMemory = PlatformDependent.usedDirectMemory(); + // Assert.assertTrue(usedDirectMemory - initialUsedDirectMemory < 2 * 10 * 10 + // * 1024 * 1024); + } + assertThat(releaseCount).hasValue(10); + } + + @Test + public void modifyRequestBodyHappenedError() { + testClient.post().uri("/post") + .header("Host", "www.modifyrequestbodyexception.org") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .body(BodyInserters.fromValue("request")).exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody() + .jsonPath("message").isEqualTo("modify body exception"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration(proxyBeanMethods = false) + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + @DependsOn("testModifyRequestBodyGatewayFilterFactory") + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes().route("test_modify_request_body_ssl_timeout", + r -> r.order(-1).host("**.modifyrequestbodyssltimeout.org") + .filters(f -> f.modifyRequestBody(String.class, String.class, + MediaType.APPLICATION_JSON_VALUE, + (serverWebExchange, aVoid) -> { + byte[] largeBody = new byte[10 * 1024 * 1024]; + return Mono.just(new String(largeBody)); + })) + .uri(uri)) + .route("test_modify_request_body_exception", r -> r.order(-1) + .host("**.modifyrequestbodyexception.org") + .filters(f -> f.modifyRequestBody(String.class, String.class, + MediaType.APPLICATION_JSON_VALUE, + (serverWebExchange, body) -> { + return Mono.error( + new Exception("modify body exception")); + })) + .uri(uri)) + .build(); + } + + @Bean + public AtomicInteger count() { + return new AtomicInteger(); + } + + @Bean + @Primary + public ModifyRequestBodyGatewayFilterFactory testModifyRequestBodyGatewayFilterFactory( + ServerCodecConfigurer codecConfigurer, AtomicInteger count) { + return new ModifyRequestBodyGatewayFilterFactory( + codecConfigurer.getReaders()) { + @Override + protected Mono release(ServerWebExchange exchange, + CachedBodyOutputMessage outputMessage, Throwable throwable) { + if (outputMessage.isCached()) { + count.incrementAndGet(); + } + return super.release(exchange, outputMessage, throwable); + } + }; + } + + } + +}