Binds configuration to Configurable factory beans directly using Boot Binder.

Deprecates the use of Tuple.

Depracates ArgumentHints in favor of ShortcutConfigurable.

Creates Configurable interface to allow factories to create a
configuration object that can be bound to properties.

Creates StatefulConfigurable interface that extends Configurable and
allows singlton beans (like RedisRateLimiter) to store a map of
configuration keyed on route id. These beans use events like
FilterArgsEvent and PredicateArgsEvent to gather configuration as it is
read for each route.
This commit is contained in:
Spencer Gibb
2018-03-05 16:36:48 -05:00
parent fb4a425f56
commit 589ed9c4f3
92 changed files with 2202 additions and 935 deletions

View File

@@ -118,7 +118,7 @@ public class GatewayControllerEndpoint implements ApplicationEventPublisherAware
return Mono.zip(routeDefs, routes).map(tuple -> {
Map<String, List> allRoutes = new HashMap<>();
allRoutes.put("routeDefinitions", tuple.getT1());
allRoutes.put("routes", tuple.getT2());
// allRoutes.put("routes", tuple.getT2());
return allRoutes;
});
}

View File

@@ -20,7 +20,6 @@ package org.springframework.cloud.gateway.config;
import java.util.List;
import java.util.function.Consumer;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
@@ -37,11 +36,8 @@ import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.filter.ForwardRoutingFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.filter.NettyRoutingFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
import org.springframework.cloud.gateway.filter.WebsocketRoutingFilter;
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
@@ -65,6 +61,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.headers.ForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver;
@@ -105,6 +104,9 @@ import org.springframework.web.reactive.socket.server.support.HandshakeWebSocket
import com.netflix.hystrix.HystrixObservableCommand;
import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.FIXED;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import reactor.core.publisher.Flux;
import reactor.ipc.netty.http.client.HttpClient;
import reactor.ipc.netty.http.client.HttpClientOptions;
@@ -112,8 +114,6 @@ import reactor.ipc.netty.options.ClientProxyOptions;
import reactor.ipc.netty.resources.PoolResources;
import rx.RxReactiveStreams;
import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.FIXED;
/**
* @author Spencer Gibb
*/

View File

@@ -12,7 +12,6 @@ import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
@@ -22,6 +21,7 @@ import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.DispatcherHandler;
@Configuration
@@ -43,8 +43,7 @@ class GatewayRedisAutoConfiguration {
@Bean
//TODO: replace with ReactiveStringRedisTemplate in future
public ReactiveRedisTemplate<String, String> stringReactiveRedisTemplate(
ReactiveRedisConnectionFactory reactiveRedisConnectionFactory,
ResourceLoader resourceLoader) {
ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
RedisSerializer<String> serializer = new StringRedisSerializer();
RedisSerializationContext<String , String> serializationContext = RedisSerializationContext
.<String, String>newSerializationContext()
@@ -59,7 +58,8 @@ class GatewayRedisAutoConfiguration {
@Bean
public RedisRateLimiter redisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
@Qualifier("redisRequestRateLimiterScript") RedisScript<List<Long>> redisScript) {
return new RedisRateLimiter(redisTemplate, redisScript);
@Qualifier("redisRequestRateLimiterScript") RedisScript<List<Long>> redisScript,
Validator validator) {
return new RedisRateLimiter(redisTemplate, redisScript, validator);
}
}

View File

@@ -29,9 +29,9 @@ import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REGEXP_KEY;
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REPLACEMENT_KEY;
import static org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory.PATTERN_KEY;
import static org.springframework.cloud.gateway.support.NameUtils.normalizeFilterName;
import static org.springframework.cloud.gateway.support.NameUtils.normalizePredicateName;
import static org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory.PATTERN_KEY;
import static org.springframework.cloud.gateway.support.NameUtils.normalizeFilterFactoryName;
import static org.springframework.cloud.gateway.support.NameUtils.normalizeRoutePredicateName;
import reactor.core.publisher.Flux;
@@ -60,13 +60,13 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc
// add a predicate that matches the url at /serviceId
/*PredicateDefinition barePredicate = new PredicateDefinition();
barePredicate.setName(normalizePredicateName(PathRoutePredicateFactory.class));
barePredicate.setName(normalizePredicateName(PathRoutePredicate.class));
barePredicate.addArg(PATTERN_KEY, "/" + serviceId);
routeDefinition.getPredicates().add(barePredicate);*/
// add a predicate that matches the url at /serviceId/**
PredicateDefinition subPredicate = new PredicateDefinition();
subPredicate.setName(normalizePredicateName(PathRoutePredicateFactory.class));
subPredicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class));
subPredicate.addArg(PATTERN_KEY, "/" + serviceId + "/**");
routeDefinition.getPredicates().add(subPredicate);
@@ -74,7 +74,7 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc
// add a filter that removes /serviceId by default
FilterDefinition filter = new FilterDefinition();
filter.setName(normalizeFilterName(RewritePathGatewayFilterFactory.class));
filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));
String regex = "/" + serviceId + "/(?<remaining>.*)";
String replacement = "/${remaining}";
filter.addArg(REGEXP_KEY, regex);

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2013-2018 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
*
* http://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 java.util.Map;
import org.springframework.context.ApplicationEvent;
public class FilterArgsEvent extends ApplicationEvent {
private String routeId;
private final Map<String, Object> args;
public FilterArgsEvent(Object source, String routeId, Map<String, Object> args) {
super(source);
this.routeId = routeId;
this.args = args;
}
public String getRouteId() {
return routeId;
}
public Map<String, Object> getArgs() {
return args;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2013-2018 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
*
* http://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 java.util.Map;
import org.springframework.context.ApplicationEvent;
public class PredicateArgsEvent extends ApplicationEvent {
private String routeId;
private final Map<String, Object> args;
public PredicateArgsEvent(Object source, String routeId, Map<String, Object> args) {
super(source);
this.routeId = routeId;
this.args = args;
}
public String getRouteId() {
return routeId;
}
public Map<String, Object> getArgs() {
return args;
}
}

View File

@@ -16,6 +16,7 @@ package org.springframework.cloud.gateway.filter;
* limitations under the License.
*/
import org.springframework.cloud.gateway.support.ShortcutConfigurable;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@@ -30,7 +31,10 @@ import reactor.core.publisher.Mono;
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface GatewayFilter {
public interface GatewayFilter extends ShortcutConfigurable {
String NAME_KEY = "name";
String VALUE_KEY = "value";
/**
* Process the Web request and (optionally) delegate to the next

View File

@@ -35,6 +35,10 @@ public class OrderedGatewayFilter implements GatewayFilter, Ordered {
this.order = order;
}
public GatewayFilter getDelegate() {
return delegate;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return this.delegate.filter(exchange, chain);

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2013-2018 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
*
* http://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 org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.support.AbstractConfigurable;
import org.springframework.tuple.Tuple;
public abstract class AbstractGatewayFilterFactory<C>
extends AbstractConfigurable<C> implements GatewayFilterFactory<C> {
@SuppressWarnings("unchecked")
public AbstractGatewayFilterFactory() {
super((Class<C>) Object.class);
}
public AbstractGatewayFilterFactory(Class<C> configClass) {
super(configClass);
}
@Override
public boolean isConfigurable() {
return true;
}
@Override
public GatewayFilter apply(Tuple args) {
throw new UnsupportedOperationException("apply(Tuple) not supported");
}
public static class NameConfig {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2013-2018 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
*
* http://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.Arrays;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.core.style.ToStringCreator;
import org.springframework.validation.annotation.Validated;
public abstract class AbstractNameValueGatewayFilterFactory extends AbstractGatewayFilterFactory<AbstractNameValueGatewayFilterFactory.NameValueConfig> {
public AbstractNameValueGatewayFilterFactory() {
super(NameValueConfig.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList(GatewayFilter.NAME_KEY, GatewayFilter.VALUE_KEY);
}
@Validated
public static class NameValueConfig {
@NotEmpty
protected String name;
@NotEmpty
protected String value;
public String getName() {
return name;
}
public NameValueConfig setName(String name) {
this.name = name;
return this;
}
public String getValue() {
return value;
}
public NameValueConfig setValue(String value) {
this.value = value;
return this;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("name", name)
.append("value", value)
.toString();
}
}
}

View File

@@ -17,37 +17,23 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import java.util.Arrays;
import java.util.List;
/**
* @author Spencer Gibb
*/
public class AddRequestHeaderGatewayFilterFactory implements GatewayFilterFactory {
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public List<String> argNames() {
return Arrays.asList(NAME_KEY, VALUE_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
String name = args.getString(NAME_KEY);
String value = args.getString(VALUE_KEY);
return apply(name, value);
}
public GatewayFilter apply(String name, String value) {
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(name, value)
.header(config.getName(), config.getValue())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
}

View File

@@ -18,36 +18,20 @@
package org.springframework.cloud.gateway.filter.factory;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import org.springframework.util.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Spencer Gibb
*/
public class AddRequestParameterGatewayFilterFactory implements GatewayFilterFactory {
public class AddRequestParameterGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public List<String> argNames() {
return Arrays.asList(NAME_KEY, VALUE_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
String parameter = args.getString(NAME_KEY);
String value = args.getString(VALUE_KEY);
return apply(parameter, value);
}
public GatewayFilter apply(String parameter, String value) {
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
URI uri = exchange.getRequest().getURI();
StringBuilder query = new StringBuilder();
String originalQuery = uri.getRawQuery();
@@ -60,9 +44,9 @@ public class AddRequestParameterGatewayFilterFactory implements GatewayFilterFac
}
//TODO urlencode?
query.append(parameter);
query.append(config.getName());
query.append('=');
query.append(value);
query.append(config.getValue());
try {
URI newUri = UriComponentsBuilder.fromUri(uri)
@@ -78,4 +62,5 @@ public class AddRequestParameterGatewayFilterFactory implements GatewayFilterFac
}
};
}
}

View File

@@ -17,32 +17,17 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import java.util.Arrays;
import java.util.List;
/**
* @author Spencer Gibb
*/
public class AddResponseHeaderGatewayFilterFactory implements GatewayFilterFactory {
public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public List<String> argNames() {
return Arrays.asList(NAME_KEY, VALUE_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
final String header = args.getString(NAME_KEY);
final String value = args.getString(VALUE_KEY);
return apply(header, value);
}
public GatewayFilter apply(String header, String value) {
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
exchange.getResponse().getHeaders().add(header, value);
exchange.getResponse().getHeaders().add(config.getName(), config.getValue());
return chain.filter(exchange);
};

View File

@@ -18,24 +18,57 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.support.ArgumentHints;
import org.springframework.cloud.gateway.support.ShortcutConfigurable;
import org.springframework.cloud.gateway.support.Configurable;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import java.util.function.Consumer;
/**
* @author Spencer Gibb
*/
@FunctionalInterface
public interface GatewayFilterFactory extends ArgumentHints {
public interface GatewayFilterFactory<C> extends ShortcutConfigurable, Configurable<C> {
String NAME_KEY = "name";
String VALUE_KEY = "value";
@Deprecated //TODO: remove when apply(Tuple) is removed
default boolean isConfigurable() {
return false;
}
// useful for javadsl
default GatewayFilter apply(Consumer<C> consumer) {
C config = newConfig();
consumer.accept(config);
return apply(config);
}
//TODO: remove after apply(Tuple) removed
@Override
default Class<C> getConfigClass() {
throw new UnsupportedOperationException("getConfigClass() not implemented");
}
//TODO: remove after apply(Tuple) removed
@Override
default C newConfig() {
throw new UnsupportedOperationException("newConfig() not implemented");
}
//TODO: remove default impl after apply(Tuple) removed
default GatewayFilter apply(C config) {
throw new UnsupportedOperationException("apply(C config) not implemented");
}
@Deprecated
GatewayFilter apply(Tuple args);
default String name() {
return NameUtils.normalizeFilterName(getClass());
return NameUtils.normalizeFilterFactoryName(getClass());
}
@Deprecated

View File

@@ -26,7 +26,6 @@ import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
@@ -50,60 +49,35 @@ import rx.Subscription;
/**
* @author Spencer Gibb
*/
public class HystrixGatewayFilterFactory implements GatewayFilterFactory {
public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<HystrixGatewayFilterFactory.Config> {
public static final String FALLBACK_URI = "fallbackUri";
private final DispatcherHandler dispatcherHandler;
public HystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) {
super(Config.class);
this.dispatcherHandler = dispatcherHandler;
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY);
}
@Override
public boolean validateArgs() {
return false;
}
@Override
public GatewayFilter apply(Tuple args) {
public GatewayFilter apply(Config config) {
//TODO: if no name is supplied, generate one from command id (useful for default filter)
String commandName = args.getString(NAME_KEY);
if (args.hasFieldName(FALLBACK_URI)) {
URI fallbackUri = URI.create(args.getString(FALLBACK_URI));
if (!"forward".equals(fallbackUri.getScheme())) {
throw new IllegalArgumentException("Hystrix Filter currently only supports 'forward' URIs, found "+ fallbackUri);
}
return apply(commandName, fallbackUri);
if (config.setter == null) {
HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey(getClass().getSimpleName());
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(config.name);
config.setter = Setter.withGroupKey(groupKey)
.andCommandKey(commandKey);
}
return apply(commandName, null);
}
public GatewayFilter apply(String commandName) {
return apply(commandName, null);
}
public GatewayFilter apply(String commandName, URI fallbackUri) {
final HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey(getClass().getSimpleName());
final HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(commandName);
final Setter setter = Setter.withGroupKey(groupKey)
.andCommandKey(commandKey);
return apply(setter, fallbackUri);
}
public GatewayFilter apply(Setter setter) {
return apply(setter, null);
}
public GatewayFilter apply(Setter setter, URI fallbackUri) {
return (exchange, chain) -> {
RouteHystrixCommand command = new RouteHystrixCommand(setter, fallbackUri, exchange, chain);
RouteHystrixCommand command = new RouteHystrixCommand(config.setter, config.fallback, exchange, chain);
return Mono.create(s -> {
Subscription sub = command.toObservable().subscribe(s::success, s::error, s::success);
@@ -163,4 +137,40 @@ public class HystrixGatewayFilterFactory implements GatewayFilterFactory {
return RxReactiveStreams.toObservable(HystrixGatewayFilterFactory.this.dispatcherHandler.handle(mutated));
}
}
public static class Config {
private String name;
private String fallbackUri;
private Setter setter;
private URI fallback;
public String getName() {
return name;
}
public Config setName(String name) {
this.name = name;
return this;
}
public String getFallbackUri() {
return fallbackUri;
}
public Config setFallbackUri(String fallbackUri) {
this.fallbackUri = fallbackUri;
if (this.fallbackUri != null) {
fallback = URI.create(fallbackUri);
if (!"forward".equals(fallback.getScheme())) {
throw new IllegalArgumentException("Hystrix Filter currently only supports 'forward' URIs, found " + fallbackUri);
}
}
return this;
}
public Config setSetter(Setter setter) {
this.setter = setter;
return this;
}
}
}

View File

@@ -22,9 +22,8 @@ import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
@@ -32,28 +31,27 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.a
/**
* @author Spencer Gibb
*/
public class PrefixPathGatewayFilterFactory implements GatewayFilterFactory {
public class PrefixPathGatewayFilterFactory extends AbstractGatewayFilterFactory<PrefixPathGatewayFilterFactory.Config> {
private static final Log log = LogFactory.getLog(PrefixPathGatewayFilterFactory.class);
public static final String PREFIX_KEY = "prefix";
public PrefixPathGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(PREFIX_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
final String prefix = args.getString(PREFIX_KEY);
return apply(prefix);
}
public GatewayFilter apply(String prefix) {
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
addOriginalRequestUrl(exchange, req.getURI());
String newPath = prefix + req.getURI().getPath();
String newPath = config.prefix + req.getURI().getPath();
ServerHttpRequest request = req.mutate()
.path(newPath)
@@ -62,10 +60,22 @@ public class PrefixPathGatewayFilterFactory implements GatewayFilterFactory {
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());
if (log.isTraceEnabled()) {
log.trace("Prefixed URI with: "+prefix+" -> "+request.getURI());
log.trace("Prefixed URI with: "+config.prefix+" -> "+request.getURI());
}
return chain.filter(exchange.mutate().request(request).build());
};
}
public static class Config {
private String prefix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
}
}

View File

@@ -23,12 +23,11 @@ import java.net.URL;
import java.util.Arrays;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.tuple.Tuple;
import org.springframework.util.Assert;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.parse;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus;
@@ -38,21 +37,23 @@ import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public class RedirectToGatewayFilterFactory implements GatewayFilterFactory {
public class RedirectToGatewayFilterFactory extends AbstractGatewayFilterFactory<RedirectToGatewayFilterFactory.Config> {
public static final String STATUS_KEY = "status";
public static final String URL_KEY = "url";
public RedirectToGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(STATUS_KEY, URL_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
String statusString = args.getRawString(STATUS_KEY);
String urlString = args.getString(URL_KEY);
return apply(statusString, urlString);
public GatewayFilter apply(Config config) {
return apply(config.status, config.url);
}
public GatewayFilter apply(String statusString, String urlString) {
@@ -82,4 +83,25 @@ public class RedirectToGatewayFilterFactory implements GatewayFilterFactory {
}));
}
public static class Config {
String status;
String url;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
}

View File

@@ -17,33 +17,31 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import java.util.Arrays;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
/**
* @author Spencer Gibb
*/
public class RemoveRequestHeaderGatewayFilterFactory implements GatewayFilterFactory {
public class RemoveRequestHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {
public RemoveRequestHeaderGatewayFilterFactory() {
super(NameConfig.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
final String header = args.getString(NAME_KEY);
return apply(header);
}
public GatewayFilter apply(String header) {
public GatewayFilter apply(NameConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.remove(header))
.headers(httpHeaders -> httpHeaders.remove(config.getName()))
.build();
return chain.filter(exchange.mutate().request(request).build());

View File

@@ -20,7 +20,6 @@ package org.springframework.cloud.gateway.filter.factory;
import java.util.Arrays;
import java.util.List;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import reactor.core.publisher.Mono;
@@ -28,22 +27,21 @@ import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public class RemoveResponseHeaderGatewayFilterFactory implements GatewayFilterFactory {
public class RemoveResponseHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {
public RemoveResponseHeaderGatewayFilterFactory() {
super(NameConfig.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
final String header = args.getString(NAME_KEY);
return apply(header);
}
public GatewayFilter apply(String header) {
public GatewayFilter apply(NameConfig config) {
return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
exchange.getResponse().getHeaders().remove(header);
exchange.getResponse().getHeaders().remove(config.getName());
}));
}
}

View File

@@ -20,22 +20,24 @@ package org.springframework.cloud.gateway.filter.factory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.HttpStatus;
import org.springframework.tuple.Tuple;
/**
* User Request Rate Limiter filter. See https://stripe.com/blog/rate-limiters and
*/
public class RequestRateLimiterGatewayFilterFactory implements GatewayFilterFactory {
public class RequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {
public static final String KEY_RESOLVER_KEY = "keyResolver";
private final RateLimiter rateLimiter;
private final RateLimiter defaultRateLimiter;
private final KeyResolver defaultKeyResolver;
public RequestRateLimiterGatewayFilterFactory(RateLimiter rateLimiter,
KeyResolver defaultKeyResolver) {
this.rateLimiter = rateLimiter;
public RequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
KeyResolver defaultKeyResolver) {
super(Config.class);
this.defaultRateLimiter = defaultRateLimiter;
this.defaultKeyResolver = defaultKeyResolver;
}
@@ -43,32 +45,54 @@ public class RequestRateLimiterGatewayFilterFactory implements GatewayFilterFact
return defaultKeyResolver;
}
@SuppressWarnings("unchecked")
@Override
public GatewayFilter apply(Tuple args) {
KeyResolver keyResolver;
if (args.hasFieldName(KEY_RESOLVER_KEY)) {
keyResolver = args.getValue(KEY_RESOLVER_KEY, KeyResolver.class);
} else {
keyResolver = defaultKeyResolver;
}
return apply(keyResolver, args);
public RateLimiter getDefaultRateLimiter() {
return defaultRateLimiter;
}
public GatewayFilter apply(KeyResolver keyResolver, Tuple args) {
@SuppressWarnings("unchecked")
@Override
public GatewayFilter apply(Config config) {
KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;
return (exchange, chain) -> keyResolver.resolve(exchange).flatMap(key ->
// TODO: if key is empty?
rateLimiter.isAllowed(key, args).flatMap(response -> {
// TODO: set some headers for rate, tokens left
return (exchange, chain) -> {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
if (response.isAllowed()) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}));
return resolver.resolve(exchange).flatMap(key ->
// TODO: if key is empty?
limiter.isAllowed(route.getId(), key).flatMap(response -> {
// TODO: set some headers for rate, tokens left
if (response.isAllowed()) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}));
};
}
public static class Config {
private KeyResolver keyResolver;
private RateLimiter rateLimiter;
public KeyResolver getKeyResolver() {
return keyResolver;
}
public Config setKeyResolver(KeyResolver keyResolver) {
this.keyResolver = keyResolver;
return this;
}
public RateLimiter getRateLimiter() {
return rateLimiter;
}
public Config setRateLimiter(RateLimiter rateLimiter) {
this.rateLimiter = rateLimiter;
return this;
}
}
}

View File

@@ -22,7 +22,6 @@ import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
@@ -30,29 +29,28 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.a
/**
* @author Spencer Gibb
*/
public class RewritePathGatewayFilterFactory implements GatewayFilterFactory {
public class RewritePathGatewayFilterFactory extends AbstractGatewayFilterFactory<RewritePathGatewayFilterFactory.Config> {
public static final String REGEXP_KEY = "regexp";
public static final String REPLACEMENT_KEY = "replacement";
public RewritePathGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(REGEXP_KEY, REPLACEMENT_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
final String regex = args.getString(REGEXP_KEY);
String replacement = args.getString(REPLACEMENT_KEY).replace("$\\", "$");
return apply(regex, replacement);
}
public GatewayFilter apply(String regex, String replacement) {
public GatewayFilter apply(Config config) {
String replacement = config.replacement.replace("$\\", "$");
return (exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
addOriginalRequestUrl(exchange, req.getURI());
String path = req.getURI().getPath();
String newPath = path.replaceAll(regex, replacement);
String newPath = path.replaceAll(config.regexp, replacement);
ServerHttpRequest request = req.mutate()
.path(newPath)
@@ -63,4 +61,27 @@ public class RewritePathGatewayFilterFactory implements GatewayFilterFactory {
return chain.filter(exchange.mutate().request(request).build());
};
}
public static class Config {
private String regexp;
private String replacement;
public String getRegexp() {
return regexp;
}
public Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
public String getReplacement() {
return replacement;
}
public Config setReplacement(String replacement) {
this.replacement = replacement;
return this;
}
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.WebSession;
/**
@@ -27,10 +26,10 @@ import org.springframework.web.server.WebSession;
*
* @author Greg Turnquist
*/
public class SaveSessionGatewayFilterFactory implements GatewayFilterFactory {
public class SaveSessionGatewayFilterFactory extends AbstractGatewayFilterFactory {
@Override
public GatewayFilter apply(Tuple args) {
public GatewayFilter apply(Object config) {
return (exchange, chain) -> exchange.getSession()
.map(WebSession::save)
.then(chain.filter(exchange));

View File

@@ -17,15 +17,14 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.http.HttpHeaders;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.HttpHeaders;
/**
* https://blog.appcanary.com/2017/http-security-headers.html
* @author Spencer Gibb
*/
public class SecureHeadersGatewayFilterFactory implements GatewayFilterFactory {
public class SecureHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory {
public static final String X_XSS_PROTECTION_HEADER = "X-Xss-Protection";
public static final String STRICT_TRANSPORT_SECURITY_HEADER = "Strict-Transport-Security";
@@ -43,7 +42,7 @@ public class SecureHeadersGatewayFilterFactory implements GatewayFilterFactory {
}
@Override
public GatewayFilter apply(Tuple args) {
public GatewayFilter apply(Object config) {
//TODO: allow args to override properties
return (exchange, chain) -> {

View File

@@ -23,9 +23,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.util.UriTemplate;
import org.springframework.web.util.pattern.PathPattern.PathMatchInfo;
@@ -36,25 +35,22 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.a
/**
* @author Spencer Gibb
*/
public class SetPathGatewayFilterFactory implements GatewayFilterFactory {
public class SetPathGatewayFilterFactory extends AbstractGatewayFilterFactory<SetPathGatewayFilterFactory.Config> {
public static final String TEMPLATE_KEY = "template";
public SetPathGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(TEMPLATE_KEY);
}
@Override
@SuppressWarnings("unchecked")
public GatewayFilter apply(Tuple args) {
String template = args.getString(TEMPLATE_KEY);
return apply(template);
}
public GatewayFilter apply(String template) {
UriTemplate uriTemplate = new UriTemplate(template);
public GatewayFilter apply(Config config) {
UriTemplate uriTemplate = new UriTemplate(config.template);
return (exchange, chain) -> {
PathMatchInfo variables = exchange.getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE);
@@ -80,4 +76,16 @@ public class SetPathGatewayFilterFactory implements GatewayFilterFactory {
return chain.filter(exchange.mutate().request(request).build());
};
}
public static class Config {
private String template;
public String getTemplate() {
return template;
}
public void setTemplate(String template) {
this.template = template;
}
}
}

View File

@@ -17,34 +17,19 @@
package org.springframework.cloud.gateway.filter.factory;
import java.util.Arrays;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
/**
* @author Spencer Gibb
*/
public class SetRequestHeaderGatewayFilterFactory implements GatewayFilterFactory {
public class SetRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public List<String> argNames() {
return Arrays.asList(NAME_KEY, VALUE_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
String name = args.getString(NAME_KEY);
String value = args.getString(VALUE_KEY);
return apply(name, value);
}
public GatewayFilter apply(String name, String value) {
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.set(name, value))
.headers(httpHeaders -> httpHeaders.set(config.name, config.value))
.build();
return chain.filter(exchange.mutate().request(request).build());

View File

@@ -17,33 +17,19 @@
package org.springframework.cloud.gateway.filter.factory;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public class SetResponseHeaderGatewayFilterFactory implements GatewayFilterFactory {
public class SetResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public List<String> argNames() {
return Arrays.asList(NAME_KEY, VALUE_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
final String header = args.getString(NAME_KEY);
final String value = args.getString(VALUE_KEY);
return apply(header, value);
}
public GatewayFilter apply(String header, String value) {
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
exchange.getResponse().getHeaders().set(header, value);
exchange.getResponse().getHeaders().set(config.name, config.value);
}));
}
}

View File

@@ -17,42 +17,36 @@
package org.springframework.cloud.gateway.filter.factory;
import java.util.Arrays;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.HttpStatus;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* @author Spencer Gibb
*/
public class SetStatusGatewayFilterFactory implements GatewayFilterFactory {
public class SetStatusGatewayFilterFactory extends AbstractGatewayFilterFactory<SetStatusGatewayFilterFactory.Config> {
public static final String STATUS_KEY = "status";
public SetStatusGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(STATUS_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
String status = args.getRawString(STATUS_KEY);
return apply(status);
}
public GatewayFilter apply(String status) {
final HttpStatus httpStatus = ServerWebExchangeUtils.parse(status);
return apply(httpStatus);
}
public GatewayFilter apply(HttpStatus httpStatus) {
public GatewayFilter apply(Config config) {
final HttpStatus status = ServerWebExchangeUtils.parse(config.status);
return (exchange, chain) -> {
// option 1 (runs in filter order)
@@ -67,10 +61,23 @@ public class SetStatusGatewayFilterFactory implements GatewayFilterFactory {
// check not really needed, since it is guarded in setStatusCode,
// but it's a good example
if (!exchange.getResponse().isCommitted()) {
setResponseStatus(exchange, httpStatus);
setResponseStatus(exchange, status);
}
}));
};
}
public static class Config {
//TODO: relaxed HttpStatus converter
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
}

View File

@@ -19,9 +19,9 @@ package org.springframework.cloud.gateway.filter.factory;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
@@ -32,28 +32,27 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.a
* before sending it downstream
* @author Ryan Baxter
*/
public class StripPrefixGatewayFilterFactory implements GatewayFilterFactory {
public class StripPrefixGatewayFilterFactory extends AbstractGatewayFilterFactory<StripPrefixGatewayFilterFactory.Config> {
public static final String PARTS_KEY = "parts";
public StripPrefixGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(PARTS_KEY);
}
@Override
public GatewayFilter apply(Tuple args) {
final int parts = args.getInt(PARTS_KEY);
return apply(parts);
}
public GatewayFilter apply(int parts) {
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
addOriginalRequestUrl(exchange, request.getURI());
String path = request.getURI().getRawPath();
String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(path, "/"))
.skip(parts).collect(Collectors.joining("/"));
.skip(config.parts).collect(Collectors.joining("/"));
ServerHttpRequest newRequest = request.mutate()
.path(newPath)
.build();
@@ -63,4 +62,16 @@ public class StripPrefixGatewayFilterFactory implements GatewayFilterFactory {
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
public static class Config {
private int parts;
public int getParts() {
return parts;
}
public void setParts(int parts) {
this.parts = parts;
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2013-2018 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
*
* http://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.ratelimit;
import java.util.Map;
import org.springframework.cloud.gateway.event.FilterArgsEvent;
import org.springframework.cloud.gateway.support.AbstractStatefulConfigurable;
import org.springframework.cloud.gateway.support.ConfigurationUtils;
import org.springframework.context.ApplicationListener;
import org.springframework.core.style.ToStringCreator;
import org.springframework.validation.Validator;
public abstract class AbstractRateLimiter<C> extends AbstractStatefulConfigurable<C> implements RateLimiter<C>, ApplicationListener<FilterArgsEvent> {
private String configurationPropertyName;
private final Validator validator;
protected AbstractRateLimiter(Class<C> configClass, String configurationPropertyName, Validator validator) {
super(configClass);
this.configurationPropertyName = configurationPropertyName;
this.validator = validator;
}
protected String getConfigurationPropertyName() {
return configurationPropertyName;
}
protected Validator getValidator() {
return validator;
}
@Override
public void onApplicationEvent(FilterArgsEvent event) {
Map<String, Object> args = event.getArgs();
if (args.isEmpty() || !hasRelevantKey(args)) {
return;
}
String routeId = event.getRouteId();
C routeConfig = newConfig();
ConfigurationUtils.bind(routeConfig, args,
configurationPropertyName, configurationPropertyName, validator);
getConfig().put(routeId, routeConfig);
}
private boolean hasRelevantKey(Map<String, Object> args) {
return args.keySet().stream()
.anyMatch(key -> key.startsWith(configurationPropertyName + "."));
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("configurationPropertyName", configurationPropertyName)
.append("config", getConfig())
.append("configClass", getConfigClass())
.toString();
}
}

View File

@@ -1,14 +1,15 @@
package org.springframework.cloud.gateway.filter.ratelimit;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.support.StatefulConfigurable;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public interface RateLimiter {
public interface RateLimiter<C> extends StatefulConfigurable<C> {
Mono<Response> isAllowed(String id, Tuple args);
Mono<Response> isAllowed(String routeId, String id);
class Response {
private final boolean allowed;

View File

@@ -5,13 +5,14 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.validation.constraints.Min;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.tuple.Tuple;
import static org.springframework.tuple.TupleBuilder.tuple;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -22,9 +23,12 @@ import reactor.core.publisher.Mono;
*
* @author Spencer Gibb
*/
public class RedisRateLimiter implements RateLimiter {
public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config> {
@Deprecated
public static final String REPLENISH_RATE_KEY = "replenishRate";
@Deprecated
public static final String BURST_CAPACITY_KEY = "burstCapacity";
public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter";
private Log log = LogFactory.getLog(getClass());
@@ -32,36 +36,32 @@ public class RedisRateLimiter implements RateLimiter {
private final RedisScript<List<Long>> script;
public RedisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
RedisScript<List<Long>> script) {
RedisScript<List<Long>> script, Validator validator) {
super(Config.class, CONFIGURATION_PROPERTY_NAME, validator);
this.redisTemplate = redisTemplate;
this.script = script;
}
public static Tuple args(int replenishRate, int burstCapacity) {
return tuple().of(REPLENISH_RATE_KEY, replenishRate, BURST_CAPACITY_KEY, burstCapacity);
}
/**
* This uses a basic token bucket algorithm and relies on the fact that Redis scripts
* execute atomically. No other operations can run between fetching the count and
* writing the new count.
* @param id
* @param args
* @return
*/
@Override
@SuppressWarnings("unchecked")
public Mono<Response> isAllowed(String id, Tuple args) {
public Mono<Response> isAllowed(String routeId, String id) {
Config routeConfig = getConfig().get(routeId);
if (routeConfig == null) {
throw new IllegalArgumentException("No Configuration found for route "+ routeId);
}
// How many requests per second do you want a user to be allowed to do?
int replenishRate = args.getInt(REPLENISH_RATE_KEY);
int replenishRate = routeConfig.getReplenishRate();
// How much bursting do you want to allow?
int burstCapacity;
if (args.hasFieldName(BURST_CAPACITY_KEY)) {
burstCapacity = args.getInt(BURST_CAPACITY_KEY);
} else {
burstCapacity = 0;
}
int burstCapacity = routeConfig.getBurstCapacity();
try {
// Make a unique key per user.
@@ -102,4 +102,39 @@ public class RedisRateLimiter implements RateLimiter {
}
return Mono.just(new Response(true, -1));
}
@Validated
public static class Config {
@Min(1)
private int replenishRate;
@Min(0)
private int burstCapacity = 0;
public int getReplenishRate() {
return replenishRate;
}
public Config setReplenishRate(int replenishRate) {
this.replenishRate = replenishRate;
return this;
}
public int getBurstCapacity() {
return burstCapacity;
}
public Config setBurstCapacity(int burstCapacity) {
this.burstCapacity = burstCapacity;
return this;
}
@Override
public String toString() {
return "Config{" +
"replenishRate=" + replenishRate +
", burstCapacity=" + burstCapacity +
'}';
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2013-2018 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
*
* http://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.handler.predicate;
import org.springframework.cloud.gateway.support.AbstractConfigurable;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.ServerWebExchange;
import java.util.function.Predicate;
public abstract class AbstractRoutePredicateFactory<C> extends AbstractConfigurable<C>
implements RoutePredicateFactory<C> {
public AbstractRoutePredicateFactory(Class<C> configClass) {
super(configClass);
}
@Override
public boolean isConfigurable() {
return true;
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
throw new UnsupportedOperationException("apply(Tuple) not supported");
}
}

View File

@@ -22,33 +22,45 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory.getZonedDateTime;
/**
* @author Spencer Gibb
*/
public class AfterRoutePredicateFactory implements RoutePredicateFactory {
public class AfterRoutePredicateFactory extends AbstractRoutePredicateFactory<AfterRoutePredicateFactory.Config> {
public static final String DATETIME_KEY = "datetime";
public AfterRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Collections.singletonList(DATETIME_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
Object value = args.getValue(DATETIME_KEY);
final ZonedDateTime dateTime = BetweenRoutePredicateFactory.getZonedDateTime(value);
return apply(dateTime);
}
public Predicate<ServerWebExchange> apply(ZonedDateTime dateTime) {
public Predicate<ServerWebExchange> apply(Config config) {
ZonedDateTime datetime = getZonedDateTime(config.getDatetime());
return exchange -> {
final ZonedDateTime now = ZonedDateTime.now();
return now.isAfter(dateTime);
return now.isAfter(datetime);
};
}
public static class Config {
private String datetime;
public String getDatetime() {
return datetime;
}
public void setDatetime(String datetime) {
this.datetime = datetime;
}
}
}

View File

@@ -22,35 +22,44 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory.parseZonedDateTime;
import static org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory.getZonedDateTime;
/**
* @author Spencer Gibb
*/
public class BeforeRoutePredicateFactory implements RoutePredicateFactory {
public class BeforeRoutePredicateFactory extends AbstractRoutePredicateFactory<BeforeRoutePredicateFactory.Config> {
public static final String DATETIME_KEY = "datetime";
public BeforeRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Collections.singletonList(DATETIME_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
Object value = args.getValue(DATETIME_KEY);
final ZonedDateTime dateTime = BetweenRoutePredicateFactory.getZonedDateTime(value);
return apply(dateTime);
}
public Predicate<ServerWebExchange> apply(ZonedDateTime dateTime) {
public Predicate<ServerWebExchange> apply(Config config) {
ZonedDateTime datetime = getZonedDateTime(config.getDatetime());
return exchange -> {
final ZonedDateTime now = ZonedDateTime.now();
return now.isBefore(dateTime);
return now.isBefore(datetime);
};
}
public static class Config {
private String datetime;
public String getDatetime() {
return datetime;
}
public void setDatetime(String datetime) {
this.datetime = datetime;
}
}
}

View File

@@ -22,36 +22,59 @@ import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.function.Predicate;
import org.springframework.tuple.Tuple;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Spencer Gibb
*/
public class BetweenRoutePredicateFactory implements RoutePredicateFactory {
public class BetweenRoutePredicateFactory extends AbstractRoutePredicateFactory<BetweenRoutePredicateFactory.Config> {
public static final String DATETIME1_KEY = "datetime1";
public static final String DATETIME2_KEY = "datetime2";
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
//TODO: is ZonedDateTime the right thing to use?
final ZonedDateTime dateTime1 = getZonedDateTime(args.getValue(DATETIME1_KEY));
final ZonedDateTime dateTime2 = getZonedDateTime(args.getValue(DATETIME2_KEY));
return apply(dateTime1, dateTime2);
public BetweenRoutePredicateFactory() {
super(Config.class);
}
public Predicate<ServerWebExchange> apply(ZonedDateTime dateTime1, ZonedDateTime dateTime2) {
Assert.isTrue(dateTime1.isBefore(dateTime2), dateTime1 +
" must be before " + dateTime2);
@Override
public Predicate<ServerWebExchange> apply(Config config) {
//TODO: figure out boot conversion
ZonedDateTime datetime1 = getZonedDateTime(config.datetime1);
ZonedDateTime datetime2 = getZonedDateTime(config.datetime2);
Assert.isTrue(datetime1.isBefore(datetime2),
config.datetime1 +
" must be before " + config.datetime2);
return exchange -> {
final ZonedDateTime now = ZonedDateTime.now();
return now.isAfter(dateTime1) && now.isBefore(dateTime2);
return now.isAfter(datetime1) && now.isBefore(datetime2);
};
}
public static class Config {
private String datetime1;
private String datetime2;
public String getDatetime1() {
return datetime1;
}
public Config setDatetime1(String datetime1) {
this.datetime1 = datetime1;
return this;
}
public String getDatetime2() {
return datetime2;
}
public Config setDatetime2(String datetime2) {
this.datetime2 = datetime2;
return this;
}
}
public static ZonedDateTime getZonedDateTime(Object value) {
ZonedDateTime dateTime;
if (value instanceof ZonedDateTime) {

View File

@@ -21,39 +21,66 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import javax.validation.constraints.NotEmpty;
import org.springframework.http.HttpCookie;
import org.springframework.tuple.Tuple;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Spencer Gibb
*/
public class CookieRoutePredicateFactory implements RoutePredicateFactory {
public class CookieRoutePredicateFactory extends AbstractRoutePredicateFactory<CookieRoutePredicateFactory.Config> {
public static final String NAME_KEY = "name";
public static final String REGEXP_KEY = "regexp";
public CookieRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY, REGEXP_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
String name = args.getString(NAME_KEY);
String regexp = args.getString(REGEXP_KEY);
return apply(name, regexp);
}
public Predicate<ServerWebExchange> apply(String name, String regexp) {
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
List<HttpCookie> cookies = exchange.getRequest().getCookies().get(name);
List<HttpCookie> cookies = exchange.getRequest().getCookies().get(config.name);
for (HttpCookie cookie : cookies) {
if (cookie.getValue().matches(regexp)) {
if (cookie.getValue().matches(config.regexp)) {
return true;
}
}
return false;
};
}
@Validated
public static class Config {
@NotEmpty
private String name;
@NotEmpty
private String regexp;
public String getName() {
return name;
}
public Config setName(String name) {
this.name = name;
return this;
}
public String getRegexp() {
return regexp;
}
public Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
}
}

View File

@@ -21,39 +21,35 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.tuple.Tuple;
import javax.validation.constraints.NotEmpty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Spencer Gibb
*/
public class HeaderRoutePredicateFactory implements RoutePredicateFactory {
public class HeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {
public static final String HEADER_KEY = "header";
public static final String REGEXP_KEY = "regexp";
public HeaderRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(HEADER_KEY, REGEXP_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
String header = args.getString(HEADER_KEY);
String regexp = args.getString(REGEXP_KEY);
return apply(header, regexp);
}
public Predicate<ServerWebExchange> apply(String header) {
return apply(header, ".*");
}
public Predicate<ServerWebExchange> apply(String header, String regexp) {
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
List<String> values = exchange.getRequest().getHeaders().get(header);
List<String> values = exchange.getRequest().getHeaders().get(config.header);
if (values != null) {
for (String value : values) {
if (value.matches(regexp)) {
if (value.matches(config.regexp)) {
return true;
}
}
@@ -61,4 +57,29 @@ public class HeaderRoutePredicateFactory implements RoutePredicateFactory {
return false;
};
}
@Validated
public static class Config {
@NotEmpty
private String header;
private String regexp;
public String getHeader() {
return header;
}
public Config setHeader(String header) {
this.header = header;
return this;
}
public String getRegexp() {
return regexp;
}
public Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
}
}

View File

@@ -21,37 +21,58 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.tuple.Tuple;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Spencer Gibb
*/
public class HostRoutePredicateFactory implements RoutePredicateFactory {
public class HostRoutePredicateFactory extends AbstractRoutePredicateFactory<HostRoutePredicateFactory.Config> {
private PathMatcher pathMatcher = new AntPathMatcher(".");
public HostRoutePredicateFactory() {
super(Config.class);
}
public void setPathMatcher(PathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Collections.singletonList(PATTERN_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
String pattern = args.getString(PATTERN_KEY);
return apply(pattern);
}
public Predicate<ServerWebExchange> apply(String pattern) {
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String host = exchange.getRequest().getHeaders().getFirst("Host");
return this.pathMatcher.match(pattern, host);
return this.pathMatcher.match(config.getPattern(), host);
};
}
@Validated
public static class Config {
private String pattern;
public String getPattern() {
return pattern;
}
public Config setPattern(String pattern) {
this.pattern = pattern;
return this;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("pattern", pattern)
.toString();
}
}
}

View File

@@ -22,36 +22,41 @@ import java.util.List;
import java.util.function.Predicate;
import org.springframework.http.HttpMethod;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Spencer Gibb
*/
public class MethodRoutePredicateFactory implements RoutePredicateFactory {
public class MethodRoutePredicateFactory extends AbstractRoutePredicateFactory<MethodRoutePredicateFactory.Config> {
public static final String METHOD_KEY = "method";
public MethodRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(METHOD_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
String method = args.getString(METHOD_KEY);
return apply(method);
}
public Predicate<ServerWebExchange> apply(String method) {
HttpMethod httpMethod = HttpMethod.resolve(method);
return apply(httpMethod);
}
public Predicate<ServerWebExchange> apply(HttpMethod httpMethod) {
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
HttpMethod requestMethod = exchange.getRequest().getMethod();
return requestMethod == httpMethod;
return requestMethod == config.getMethod();
};
}
public static class Config {
private HttpMethod method;
public HttpMethod getMethod() {
return method;
}
public void setMethod(HttpMethod method) {
this.method = method;
}
}
}

View File

@@ -24,8 +24,9 @@ import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.server.PathContainer;
import org.springframework.tuple.Tuple;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPattern.PathMatchInfo;
@@ -37,43 +38,39 @@ import static org.springframework.http.server.PathContainer.parsePath;
/**
* @author Spencer Gibb
*/
public class PathRoutePredicateFactory implements RoutePredicateFactory {
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
private static final Log log = LogFactory.getLog(RoutePredicateFactory.class);
private PathPatternParser pathPatternParser = new PathPatternParser();
public PathRoutePredicateFactory() {
super(Config.class);
}
public void setPathPatternParser(PathPatternParser pathPatternParser) {
this.pathPatternParser = pathPatternParser;
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Collections.singletonList(PATTERN_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
String unparsedPattern = args.getString(PATTERN_KEY);
return apply(unparsedPattern);
}
public Predicate<ServerWebExchange> apply(String unparsedPattern) {
PathPattern pattern;
public Predicate<ServerWebExchange> apply(Config config) {
synchronized (this.pathPatternParser) {
pattern = this.pathPatternParser.parse(unparsedPattern);
config.pathPattern = this.pathPatternParser.parse(config.pattern);
}
return exchange -> {
PathContainer path = parsePath(exchange.getRequest().getURI().getPath());
boolean match = pattern.matches(path);
traceMatch("Pattern", pattern.getPatternString(), path, match);
boolean match = config.pathPattern.matches(path);
traceMatch("Pattern", config.pathPattern.getPatternString(), path, match);
if (match) {
PathMatchInfo uriTemplateVariables = pattern.matchAndExtract(path);
PathMatchInfo uriTemplateVariables = config.pathPattern.matchAndExtract(path);
exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
return true;
}
else {
} else {
return false;
}
};
@@ -87,4 +84,27 @@ public class PathRoutePredicateFactory implements RoutePredicateFactory {
}
}
@Validated
public static class Config {
private String pattern;
private PathPattern pathPattern;
public String getPattern() {
return pattern;
}
public Config setPattern(String pattern) {
this.pattern = pattern;
return this;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("pattern", pattern)
.toString();
}
}
}

View File

@@ -21,58 +21,76 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.tuple.Tuple;
import javax.validation.constraints.NotEmpty;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Spencer Gibb
*/
public class QueryRoutePredicateFactory implements RoutePredicateFactory {
public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory<QueryRoutePredicateFactory.Config> {
public static final String PARAM_KEY = "param";
public static final String REGEXP_KEY = "regexp";
public QueryRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList(PARAM_KEY, REGEXP_KEY);
}
@Override
public boolean validateArgs() {
public boolean validateFieldsExist() {
return false;
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
validateMin(1, args);
String param = args.getString(PARAM_KEY);
final String regexp;
if (args.hasFieldName(REGEXP_KEY)) {
regexp = args.getString(REGEXP_KEY);
} else {
regexp = null;
}
return apply(param, regexp);
}
public Predicate<ServerWebExchange> apply(String param, String regexp) {
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
if (!StringUtils.hasText(regexp)) {
if (!StringUtils.hasText(config.regexp)) {
// check existence of header
return exchange.getRequest().getQueryParams().containsKey(param);
return exchange.getRequest().getQueryParams().containsKey(config.param);
}
List<String> values = exchange.getRequest().getQueryParams().get(param);
List<String> values = exchange.getRequest().getQueryParams().get(config.param);
for (String value : values) {
if (value.matches(regexp)) {
if (value.matches(config.regexp)) {
return true;
}
}
return false;
};
}
@Validated
public static class Config {
@NotEmpty
private String param;
private String regexp;
public String getParam() {
return param;
}
public Config setParam(String param) {
this.param = param;
return this;
}
public String getRegexp() {
return regexp;
}
public Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
}
}

View File

@@ -19,49 +19,58 @@ package org.springframework.cloud.gateway.handler.predicate;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import javax.validation.constraints.NotEmpty;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.tuple.Tuple;
import org.springframework.util.Assert;
import org.jetbrains.annotations.NotNull;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType.GATHER_LIST;
import io.netty.handler.ipfilter.IpFilterRuleType;
import io.netty.handler.ipfilter.IpSubnetFilterRule;
/**
* @author Spencer Gibb
*/
public class RemoteAddrRoutePredicateFactory implements RoutePredicateFactory {
public class RemoteAddrRoutePredicateFactory extends AbstractRoutePredicateFactory<RemoteAddrRoutePredicateFactory.Config> {
private static final Log log = LogFactory.getLog(RemoteAddrRoutePredicateFactory.class);
public RemoteAddrRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Tuple args) {
validateMin(1, args);
List<IpSubnetFilterRule> sources = new ArrayList<>();
if (args != null) {
for (Object arg : args.getValues()) {
addSource(sources, (String) arg);
}
}
return apply(sources);
public ShortcutType shortcutType() {
return GATHER_LIST;
}
public Predicate<ServerWebExchange> apply(String... addrs) {
Assert.notEmpty(addrs, "addrs must not be empty");
List<IpSubnetFilterRule> sources = new ArrayList<>();
for (String addr : addrs) {
addSource(sources, addr);
}
return apply(sources);
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("sources");
}
public Predicate<ServerWebExchange> apply(List<IpSubnetFilterRule> sources) {
@NotNull
private List<IpSubnetFilterRule> convert(List<String> values) {
List<IpSubnetFilterRule> sources = new ArrayList<>();
for (String arg : values) {
addSource(sources, arg);
}
return sources;
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
List<IpSubnetFilterRule> sources = convert(config.sources);
return exchange -> {
InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();
if (remoteAddress != null) {
@@ -94,4 +103,25 @@ public class RemoteAddrRoutePredicateFactory implements RoutePredicateFactory {
sources.add(new IpSubnetFilterRule(ipAddress, cidrPrefix, IpFilterRuleType.ACCEPT));
}
@Validated
public static class Config {
@NotEmpty
private List<String> sources = new ArrayList<>();
public List<String> getSources() {
return sources;
}
public Config setSources(List<String> sources) {
this.sources = sources;
return this;
}
public Config setSources(String... sources) {
this.sources = Arrays.asList(sources);
return this;
}
}
}

View File

@@ -17,10 +17,12 @@
package org.springframework.cloud.gateway.handler.predicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.springframework.cloud.gateway.support.ArgumentHints;
import org.springframework.cloud.gateway.support.Configurable;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.cloud.gateway.support.ShortcutConfigurable;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.ServerWebExchange;
@@ -28,13 +30,43 @@ import org.springframework.web.server.ServerWebExchange;
* @author Spencer Gibb
*/
@FunctionalInterface
public interface RoutePredicateFactory extends ArgumentHints {
public interface RoutePredicateFactory<C> extends ShortcutConfigurable, Configurable<C> {
String PATTERN_KEY = "pattern";
@Deprecated //TODO: remove when apply(Tuple) is removed
default boolean isConfigurable() {
return false;
}
// useful for javadsl
default Predicate<ServerWebExchange> apply(Consumer<C> consumer) {
C config = newConfig();
consumer.accept(config);
return apply(config);
}
//TODO: remove after apply(Tuple) removed
@Override
default Class<C> getConfigClass() {
throw new UnsupportedOperationException("getConfigClass() not implemented");
}
//TODO: remove after apply(Tuple) removed
@Override
default C newConfig() {
throw new UnsupportedOperationException("newConfig() not implemented");
}
//TODO: remove default impl after apply(Tuple) removed
default Predicate<ServerWebExchange> apply(C config) {
throw new UnsupportedOperationException("apply(C config) not implemented");
}
@Deprecated
Predicate<ServerWebExchange> apply(Tuple args);
default String name() {
return NameUtils.normalizePredicateName(getClass());
return NameUtils.normalizeRoutePredicateName(getClass());
}
}

View File

@@ -84,6 +84,10 @@ public class Route implements Ordered {
return this;
}
public String getId() {
return id;
}
public Builder order(int order) {
this.order = order;
return this;

View File

@@ -30,32 +30,37 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.event.FilterArgsEvent;
import org.springframework.cloud.gateway.event.PredicateArgsEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.support.ArgumentHints;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.cloud.gateway.support.ConfigurationUtils;
import org.springframework.cloud.gateway.support.ShortcutConfigurable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.tuple.Tuple;
import org.springframework.tuple.TupleBuilder;
import org.springframework.validation.Validator;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ShortcutConfigurable.getValue;
import static org.springframework.cloud.gateway.support.ShortcutConfigurable.normalizeKey;
import reactor.core.publisher.Flux;
/**
* {@link RouteLocator} that loads routes from a {@link RouteDefinitionLocator}
* @author Spencer Gibb
*/
public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAware {
public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware {
protected final Log logger = LogFactory.getLog(getClass());
private final RouteDefinitionLocator routeDefinitionLocator;
@@ -64,6 +69,7 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
private final GatewayProperties gatewayProperties;
private final SpelExpressionParser parser = new SpelExpressionParser();
private BeanFactory beanFactory;
private ApplicationEventPublisher publisher;
public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
List<RoutePredicateFactory> predicates,
@@ -75,11 +81,19 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
this.gatewayProperties = gatewayProperties;
}
@Autowired
private Validator validator;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
private void initFactories(List<RoutePredicateFactory> predicates) {
predicates.forEach(factory -> {
String key = factory.name();
@@ -124,21 +138,36 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
.build();
}
@SuppressWarnings("unchecked")
private List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {
List<GatewayFilter> filters = filterDefinitions.stream()
.map(definition -> {
GatewayFilterFactory filter = this.gatewayFilterFactories.get(definition.getName());
if (filter == null) {
throw new IllegalArgumentException("Unable to find GatewayFilterFactory with name " + definition.getName());
GatewayFilterFactory factory = this.gatewayFilterFactories.get(definition.getName());
if (factory == null) {
throw new IllegalArgumentException("Unable to find GatewayFilterFactory with name " + definition.getName());
}
Map<String, String> args = definition.getArgs();
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition " + id + " applying filter " + args + " to " + definition.getName());
}
Tuple tuple = getTuple(filter, args, this.parser, this.beanFactory);
if (factory.isConfigurable()) {
Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);
return filter.apply(tuple);
Object configuration = factory.newConfig();
ConfigurationUtils.bind(configuration, properties,
"", definition.getName(), validator);
GatewayFilter gatewayFilter = factory.apply(configuration);
if (this.publisher != null) {
this.publisher.publishEvent(new FilterArgsEvent(this, id, properties));
}
return gatewayFilter;
} else {
Tuple tuple = getTuple(factory, args, this.parser, this.beanFactory);
return factory.apply(tuple);
}
})
.collect(Collectors.toList());
@@ -150,14 +179,15 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
return ordered;
}
//TODO: make argument resolving a strategy
/* for testing */ static Tuple getTuple(ArgumentHints hasArguments, Map<String, String> args, SpelExpressionParser parser, BeanFactory beanFactory) {
@SuppressWarnings("Duplicates")
@Deprecated //TODO: remove after Tuple is removed
/* for testing */ static Tuple getTuple(ShortcutConfigurable shortcutConf, Map<String, String> args, SpelExpressionParser parser, BeanFactory beanFactory) {
TupleBuilder builder = TupleBuilder.tuple();
List<String> argNames = hasArguments.argNames();
List<String> argNames = shortcutConf.shortcutFieldOrder();
if (!argNames.isEmpty()) {
// ensure size is the same for key replacement later
if (hasArguments.validateArgs() && args.size() != argNames.size()) {
if (shortcutConf.validateFieldsExist() && args.size() != argNames.size()) {
throw new IllegalArgumentException("Wrong number of arguments. Expected " + argNames
+ " " + argNames + ". Found " + args.size() + " " + args + "'");
}
@@ -165,29 +195,8 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
int entryIdx = 0;
for (Map.Entry<String, String> entry : args.entrySet()) {
String key = entry.getKey();
// RoutePredicateFactory has name hints and this has a fake key name
// replace with the matching key hint
if (key.startsWith(NameUtils.GENERATED_NAME_PREFIX) && !argNames.isEmpty()
&& entryIdx < args.size()) {
key = argNames.get(entryIdx);
}
Object value;
String rawValue = entry.getValue();
if (rawValue != null) {
rawValue = rawValue.trim();
}
if (rawValue != null && rawValue.startsWith("#{") && entry.getValue().endsWith("}")) {
// assume it's spel
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
Expression expression = parser.parseExpression(entry.getValue(), new TemplateParserContext());
value = expression.getValue(context);
} else {
value = entry.getValue();
}
String key = normalizeKey(entry.getKey(), entryIdx, shortcutConf, args);
Object value = getValue(parser, beanFactory, entry.getValue());
builder.put(key, value);
entryIdx++;
@@ -195,7 +204,7 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
Tuple tuple = builder.build();
if (hasArguments.validateArgs()) {
if (shortcutConf.validateFieldsExist()) {
for (String name : argNames) {
if (!tuple.hasFieldName(name)) {
throw new IllegalArgumentException("Missing argument '" + name + "'. Given " + tuple);
@@ -234,20 +243,30 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
return predicate;
}
private Predicate<ServerWebExchange> lookup(RouteDefinition routeDefinition, PredicateDefinition predicate) {
RoutePredicateFactory found = this.predicates.get(predicate.getName());
if (found == null) {
throw new IllegalArgumentException("Unable to find RoutePredicateFactory with name " + predicate.getName());
@SuppressWarnings("unchecked")
private Predicate<ServerWebExchange> lookup(RouteDefinition route, PredicateDefinition predicate) {
RoutePredicateFactory<Object> factory = this.predicates.get(predicate.getName());
if (factory == null) {
throw new IllegalArgumentException("Unable to find RoutePredicateFactory with name " + predicate.getName());
}
Map<String, String> args = predicate.getArgs();
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition " + routeDefinition.getId() + " applying "
logger.debug("RouteDefinition " + route.getId() + " applying "
+ args + " to " + predicate.getName());
}
Tuple tuple = getTuple(found, args, this.parser, this.beanFactory);
return found.apply(tuple);
if (!factory.isConfigurable()) {
Tuple tuple = getTuple(factory, args, this.parser, this.beanFactory);
return factory.apply(tuple);
} else {
Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);
Object config = factory.newConfig();
ConfigurationUtils.bind(config, properties,
"", predicate.getName(), validator);
if (this.publisher != null) {
this.publisher.publishEvent(new PredicateArgsEvent(this, route.getId(), properties));
}
return factory.apply(config);
}
}
}

View File

@@ -18,16 +18,11 @@ package org.springframework.cloud.gateway.route.builder;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import com.netflix.hystrix.HystrixObservableCommand;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.retry.Repeat;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
@@ -49,26 +44,23 @@ 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.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.tuple.TupleBuilder.tuple;
import reactor.retry.Repeat;
public class GatewayFilterSpec extends UriSpec {
private static final Log log = LogFactory.getLog(GatewayFilterSpec.class);
static final Tuple EMPTY_TUPLE = tuple().build();
public GatewayFilterSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder) {
super(routeBuilder, builder);
}
public GatewayFilterSpec filter(GatewayFilter gatewayFilter) {
if (gatewayFilter instanceof Ordered) {
this.routeBuilder.filter(gatewayFilter);
@@ -99,35 +91,28 @@ public class GatewayFilterSpec extends UriSpec {
}
public GatewayFilterSpec addRequestHeader(String headerName, String headerValue) {
return filter(getBean(AddRequestHeaderGatewayFilterFactory.class).apply(headerName, headerValue));
return filter(getBean(AddRequestHeaderGatewayFilterFactory.class)
.apply(c -> c.setName(headerName).setValue(headerValue)));
}
public GatewayFilterSpec addRequestParameter(String param, String value) {
return filter(getBean(AddRequestParameterGatewayFilterFactory.class).apply(param, value));
return filter(getBean(AddRequestParameterGatewayFilterFactory.class)
.apply(c -> c.setName(param).setValue(value)));
}
public GatewayFilterSpec addResponseHeader(String headerName, String headerValue) {
return filter(getBean(AddResponseHeaderGatewayFilterFactory.class).apply(headerName, headerValue));
return filter(getBean(AddResponseHeaderGatewayFilterFactory.class)
.apply(c -> c.setName(headerName).setValue(headerValue)));
}
public GatewayFilterSpec hystrix(String commandName) {
return filter(getBean(HystrixGatewayFilterFactory.class).apply(commandName));
}
public GatewayFilterSpec hystrix(HystrixObservableCommand.Setter setter) {
return filter(getBean(HystrixGatewayFilterFactory.class).apply(setter));
}
public GatewayFilterSpec hystrix(String commandName, URI fallbackUri) {
return filter(getBean(HystrixGatewayFilterFactory.class).apply(commandName, fallbackUri));
}
public GatewayFilterSpec hystrix(HystrixObservableCommand.Setter setter, URI fallbackUri) {
return filter(getBean(HystrixGatewayFilterFactory.class).apply(setter, fallbackUri));
public GatewayFilterSpec hystrix(Consumer<HystrixGatewayFilterFactory.Config> configConsumer) {
return filter(getBean(HystrixGatewayFilterFactory.class)
.apply(configConsumer));
}
public GatewayFilterSpec prefixPath(String prefix) {
return filter(getBean(PrefixPathGatewayFilterFactory.class).apply(prefix));
return filter(getBean(PrefixPathGatewayFilterFactory.class)
.apply(c -> c.setPrefix(prefix)));
}
public GatewayFilterSpec preserveHostHeader() {
@@ -155,26 +140,54 @@ public class GatewayFilterSpec extends UriSpec {
}
public GatewayFilterSpec removeRequestHeader(String headerName) {
return filter(getBean(RemoveRequestHeaderGatewayFilterFactory.class).apply(headerName));
return filter(getBean(RemoveRequestHeaderGatewayFilterFactory.class)
.apply(c -> c.setName(headerName)));
}
public GatewayFilterSpec removeResponseHeader(String headerName) {
return filter(getBean(RemoveResponseHeaderGatewayFilterFactory.class).apply(headerName));
return filter(getBean(RemoveResponseHeaderGatewayFilterFactory.class)
.apply(c -> c.setName(headerName)));
}
public GatewayFilterSpec requestRateLimiter(Tuple args) {
RequestRateLimiterGatewayFilterFactory factory = getBean(RequestRateLimiterGatewayFilterFactory.class);
KeyResolver keyResolver;
try {
keyResolver = getBean(KeyResolver.class);
} catch (NoSuchBeanDefinitionException e) {
keyResolver = factory.getDefaultKeyResolver();
// public GatewayFilterSpec requestRateLimiter() {
// return filter(getBean(RequestRateLimiterGatewayFilterFactory.class).apply(config -> {}));
// }
public RequestRateLimiterSpec requestRateLimiter() {
return new RequestRateLimiterSpec(getBean(RequestRateLimiterGatewayFilterFactory.class));
}
public class RequestRateLimiterSpec {
private final RequestRateLimiterGatewayFilterFactory filter;
public RequestRateLimiterSpec(RequestRateLimiterGatewayFilterFactory filter) {
this.filter = filter;
}
return filter(factory.apply(keyResolver, args));
public <C, R extends RateLimiter<C>> RequestRateLimiterSpec rateLimiter(Class<R> rateLimiterType,
Consumer<C> configConsumer) {
R rateLimiter = getBean(rateLimiterType);
C config = rateLimiter.newConfig();
configConsumer.accept(config);
rateLimiter.getConfig().put(routeBuilder.getId(), config);
return this;
}
public GatewayFilterSpec configure(Consumer<RequestRateLimiterGatewayFilterFactory.Config> configConsumer) {
filter(this.filter.apply(configConsumer));
return GatewayFilterSpec.this;
}
// useful when nothing to configure
public GatewayFilterSpec and() {
return configure(config -> {});
}
}
public GatewayFilterSpec rewritePath(String regex, String replacement) {
return filter(getBean(RewritePathGatewayFilterFactory.class).apply(regex, replacement));
return filter(getBean(RewritePathGatewayFilterFactory.class)
.apply(c -> c.setRegexp(regex).setReplacement(replacement)));
}
/**
@@ -208,38 +221,47 @@ public class GatewayFilterSpec extends UriSpec {
}
public GatewayFilterSpec secureHeaders() {
return filter(getBean(SecureHeadersGatewayFilterFactory.class).apply(EMPTY_TUPLE));
return filter(getBean(SecureHeadersGatewayFilterFactory.class).apply(c -> {}));
}
public GatewayFilterSpec setPath(String template) {
return filter(getBean(SetPathGatewayFilterFactory.class).apply(template));
return filter(getBean(SetPathGatewayFilterFactory.class)
.apply(c -> c.setTemplate(template)));
}
public GatewayFilterSpec setRequestHeader(String headerName, String headerValue) {
return filter(getBean(SetRequestHeaderGatewayFilterFactory.class).apply(headerName, headerValue));
return filter(getBean(SetRequestHeaderGatewayFilterFactory.class)
.apply(c -> c.setName(headerName).setValue(headerValue)));
}
public GatewayFilterSpec setResponseHeader(String headerName, String headerValue) {
return filter(getBean(SetResponseHeaderGatewayFilterFactory.class).apply(headerName, headerValue));
return filter(getBean(SetResponseHeaderGatewayFilterFactory.class)
.apply(c -> c.setName(headerName).setValue(headerValue)));
}
public GatewayFilterSpec setStatus(int status) {
return setStatus(String.valueOf(status));
}
public GatewayFilterSpec setStatus(String status) {
return filter(getBean(SetStatusGatewayFilterFactory.class).apply(status));
public GatewayFilterSpec setStatus(HttpStatus status) {
return setStatus(status.toString());
}
public GatewayFilterSpec setStatus(HttpStatus status) {
return filter(getBean(SetStatusGatewayFilterFactory.class).apply(status));
public GatewayFilterSpec setStatus(String status) {
return filter(getBean(SetStatusGatewayFilterFactory.class)
.apply(c -> c.setStatus(status)));
}
public GatewayFilterSpec saveSession() {
return filter(getBean(SaveSessionGatewayFilterFactory.class).apply(EMPTY_TUPLE));
return filter(getBean(SaveSessionGatewayFilterFactory.class).apply(c -> {}));
}
public GatewayFilterSpec stripPrefix(int parts) {
return filter(getBean(StripPrefixGatewayFilterFactory.class).apply(parts));
return filter(getBean(StripPrefixGatewayFilterFactory.class)
.apply(c -> c.setParts(parts)));
}
private String routeId() {
return routeBuilder.getId();
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.cloud.gateway.route.builder;
import java.time.ZonedDateTime;
import java.util.function.Predicate;
import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory;
@@ -30,9 +33,6 @@ import org.springframework.cloud.gateway.route.Route;
import org.springframework.http.HttpMethod;
import org.springframework.web.server.ServerWebExchange;
import java.time.ZonedDateTime;
import java.util.function.Predicate;
public class PredicateSpec extends UriSpec {
PredicateSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder) {
@@ -54,55 +54,67 @@ public class PredicateSpec extends UriSpec {
}
public BooleanSpec after(ZonedDateTime datetime) {
return predicate(getBean(AfterRoutePredicateFactory.class).apply(datetime));
return predicate(getBean(AfterRoutePredicateFactory.class)
.apply(c-> c.setDatetime(datetime.toString())));
}
public BooleanSpec before(ZonedDateTime datetime) {
return predicate(getBean(BeforeRoutePredicateFactory.class).apply(datetime));
return predicate(getBean(BeforeRoutePredicateFactory.class).apply(c -> c.setDatetime(datetime.toString())));
}
public BooleanSpec between(ZonedDateTime datetime1, ZonedDateTime datetime2) {
return predicate(getBean(BetweenRoutePredicateFactory.class).apply(datetime1, datetime2));
return predicate(getBean(BetweenRoutePredicateFactory.class)
.apply(c -> c.setDatetime1(datetime1.toString()).setDatetime2(datetime2.toString())));
}
public BooleanSpec cookie(String name, String regex) {
return predicate(getBean(CookieRoutePredicateFactory.class).apply(name, regex));
return predicate(getBean(CookieRoutePredicateFactory.class)
.apply(c -> c.setName(name).setRegexp(regex)));
}
public BooleanSpec header(String header) {
return predicate(getBean(HeaderRoutePredicateFactory.class).apply(header));
return predicate(getBean(HeaderRoutePredicateFactory.class)
.apply(c -> c.setHeader(header))); //TODO: default regexp
}
public BooleanSpec header(String header, String regex) {
return predicate(getBean(HeaderRoutePredicateFactory.class).apply(header, regex));
return predicate(getBean(HeaderRoutePredicateFactory.class)
.apply(c -> c.setHeader(header).setRegexp(regex)));
}
public BooleanSpec host(String pattern) {
return predicate(getBean(HostRoutePredicateFactory.class).apply(pattern));
return predicate(getBean(HostRoutePredicateFactory.class)
.apply(c-> c.setPattern(pattern)));
}
public BooleanSpec method(String method) {
return predicate(getBean(MethodRoutePredicateFactory.class).apply(method));
return predicate(getBean(MethodRoutePredicateFactory.class)
.apply(c -> c.setMethod(HttpMethod.resolve(method))));
}
public BooleanSpec method(HttpMethod method) {
return predicate(getBean(MethodRoutePredicateFactory.class).apply(method));
return predicate(getBean(MethodRoutePredicateFactory.class)
.apply(c -> c.setMethod(method)));
}
public BooleanSpec path(String pattern) {
return predicate(getBean(PathRoutePredicateFactory.class).apply(pattern));
return predicate(getBean(PathRoutePredicateFactory.class)
.apply(c -> c.setPattern(pattern)));
}
public BooleanSpec query(String param, String regex) {
return predicate(getBean(QueryRoutePredicateFactory.class).apply(param, regex));
return predicate(getBean(QueryRoutePredicateFactory.class)
.apply(c -> c.setParam(param).setRegexp(regex)));
}
public BooleanSpec query(String param) {
return predicate(getBean(QueryRoutePredicateFactory.class).apply(param, null));
return predicate(getBean(QueryRoutePredicateFactory.class)
.apply(c -> c.setParam(param)));
}
public BooleanSpec remoteAddr(String... addrs) {
return predicate(getBean(RemoteAddrRoutePredicateFactory.class).apply(addrs));
return predicate(getBean(RemoteAddrRoutePredicateFactory.class)
.apply(c -> c.setSources(addrs)));
}
public BooleanSpec alwaysTrue() {

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import org.springframework.beans.BeanUtils;
import org.springframework.core.style.ToStringCreator;
public abstract class AbstractConfigurable<C> implements Configurable<C> {
private Class<C> configClass;
protected AbstractConfigurable(Class<C> configClass) {
this.configClass = configClass;
}
public Class<C> getConfigClass() {
return configClass;
}
@Override
public C newConfig() {
return BeanUtils.instantiateClass(this.configClass);
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("configClass", configClass)
.toString();
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.style.ToStringCreator;
public abstract class AbstractStatefulConfigurable<C> extends AbstractConfigurable<C> implements StatefulConfigurable<C> {
private Map<String, C> config = new HashMap<>();
protected AbstractStatefulConfigurable(Class<C> configClass) {
super(configClass);
}
@Override
public Map<String, C> getConfig() {
return this.config;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("config", config)
.append("configClass", getConfigClass())
.toString();
}
}

View File

@@ -20,20 +20,21 @@ package org.springframework.cloud.gateway.support;
import org.springframework.tuple.Tuple;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.List;
/**
* @author Spencer Gibb
* @deprecated Use {@link ShortcutConfigurable} instead
*/
public interface ArgumentHints {
@Deprecated
public interface ArgumentHints extends ShortcutConfigurable {
/**
* Returns hints about the number of args and the order for shortcut parsing.
* @return
*/
default List<String> argNames() {
return Collections.emptyList();
return shortcutFieldOrder();
}
/**
@@ -42,14 +43,16 @@ public interface ArgumentHints {
* @return
*/
default boolean validateArgs() {
return true;
return validateFieldsExist();
}
@Deprecated
default void validate(int requiredSize, Tuple args) {
Assert.isTrue(args != null && args.size() == requiredSize,
"args must have "+ requiredSize +" entry(s)");
}
@Deprecated
default void validateMin(int minSize, Tuple args) {
Assert.isTrue(args != null && args.size() >= minSize,
"args must have at least "+ minSize +" entry(s)");

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
public interface Configurable<C> {
Class<C> getConfigClass();
C newConfig();
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import java.util.Map;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
public abstract class ConfigurationUtils {
public static void bind(Object o, Map<String, Object> properties, String configurationPropertyName, String bindingName, Validator validator) {
Object toBind = getTargetObject(o);
new Binder(new MapConfigurationPropertySource(properties))
.bind(configurationPropertyName, Bindable.ofInstance(toBind));
if (validator != null) {
BindingResult errors = new BeanPropertyBindingResult(toBind, bindingName);
validator.validate(toBind, errors);
if (errors.hasErrors()) {
throw new RuntimeException(new BindException(errors));
}
}
}
public static <T> T getTargetObject(Object candidate) {
try {
if (AopUtils.isAopProxy(candidate) && (candidate instanceof Advised)) {
return (T) ((Advised) candidate).getTargetSource().getTarget();
}
}
catch (Exception ex) {
throw new IllegalStateException("Failed to unwrap proxied object", ex);
}
return (T) candidate;
}
}

View File

@@ -30,11 +30,11 @@ public class NameUtils {
return GENERATED_NAME_PREFIX + i;
}
public static String normalizePredicateName(Class<? extends RoutePredicateFactory> clazz) {
public static String normalizeRoutePredicateName(Class<? extends RoutePredicateFactory> clazz) {
return clazz.getSimpleName().replace(RoutePredicateFactory.class.getSimpleName(), "");
}
public static String normalizeFilterName(Class<? extends GatewayFilterFactory> clazz) {
public static String normalizeFilterFactoryName(Class<? extends GatewayFilterFactory> clazz) {
return clazz.getSimpleName().replace(GatewayFilterFactory.class.getSimpleName(), "");
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2013-2017 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
*
* http://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.support;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author Spencer Gibb
*/
public interface ShortcutConfigurable {
enum ShortcutType {
DEFAULT {
@Override
public Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf, SpelExpressionParser parser, BeanFactory beanFactory) {
Map<String, Object> map = new HashMap<>();
int entryIdx = 0;
for (Map.Entry<String, String> entry : args.entrySet()) {
String key = normalizeKey(entry.getKey(), entryIdx, shortcutConf, args);
Object value = getValue(parser, beanFactory, entry.getValue());
map.put(key, value);
entryIdx++;
}
return map;
}
},
GATHER_LIST {
@Override
public Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf, SpelExpressionParser parser, BeanFactory beanFactory) {
Map<String, Object> map = new HashMap<>();
// field order should be of size 1
List<String> fieldOrder = shortcutConf.shortcutFieldOrder();
Assert.isTrue(fieldOrder != null
&& fieldOrder.size() == 1,
"Shortcut Configuration Type GATHER_LIST must have shortcutFieldOrder of size 1");
String fieldName = fieldOrder.get(0);
map.put(fieldName, args.values().stream()
.map(value -> getValue(parser, beanFactory, value))
.collect(Collectors.toList()));
return map;
}
};
public abstract Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf,
SpelExpressionParser parser, BeanFactory beanFactory);
}
static String normalizeKey(String key, int entryIdx, ShortcutConfigurable argHints, Map<String, String> args) {
// RoutePredicateFactory has name hints and this has a fake key name
// replace with the matching key hint
if (key.startsWith(NameUtils.GENERATED_NAME_PREFIX) && !argHints.shortcutFieldOrder().isEmpty()
&& entryIdx < args.size() && entryIdx < argHints.shortcutFieldOrder().size()) {
key = argHints.shortcutFieldOrder().get(entryIdx);
}
return key;
}
static Object getValue(SpelExpressionParser parser, BeanFactory beanFactory, String entryValue) {
Object value;
String rawValue = entryValue;
if (rawValue != null) {
rawValue = rawValue.trim();
}
if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) {
// assume it's spel
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
Expression expression = parser.parseExpression(entryValue, new TemplateParserContext());
value = expression.getValue(context);
} else {
value = entryValue;
}
return value;
}
default ShortcutType shortcutType() {
return ShortcutType.DEFAULT;
}
/**
* Returns hints about the number of args and the order for shortcut parsing.
* @return
*/
default List<String> shortcutFieldOrder() {
return Collections.emptyList();
}
/**
* Validate supplied argument size against {@see #shortcutFieldOrder} size.
* Useful for variable arg predicates.
* @return
*/
@Deprecated
default boolean validateFieldsExist() {
return true;
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import java.util.Map;
public interface StatefulConfigurable<C> extends Configurable<C> {
Map<String, C> getConfig();
}

View File

@@ -19,10 +19,14 @@ package org.springframework.cloud.gateway.filter.factory;
import org.junit.Test;
import org.junit.runner.RunWith;
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.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
@@ -34,7 +38,6 @@ 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;
/**
* @author Spencer Gibb
* @author Biju Kunjummen
@@ -54,13 +57,40 @@ public class AddRequestHeaderGatewayFilterFactoryTests extends BaseWebClientTest
.expectBody(Map.class)
.consumeWith(result -> {
Map<String, Object> headers = getMap(result.getResponseBody(), "headers");
assertThat(headers).containsEntry("X-Request-Foo", "Bar");
assertThat(headers).containsEntry("X-Request-Example", "ValueA");
});
}
@Test
public void addRequestHeaderFilterWorksJavaDsl() {
testClient.get()
.uri("/headers")
.header("Host", "www.addrequestheaderjava.org")
.exchange()
.expectBody(Map.class)
.consumeWith(result -> {
Map<String, Object> headers = getMap(result.getResponseBody(), "headers");
assertThat(headers).containsEntry("X-Request-Acme", "ValueB");
});
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig { }
public static class TestConfig {
@Value("${test.uri}")
String uri;
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("add_request_header_java_test", r ->
r.path("/headers").and().host("**.addrequestheaderjava.org")
.filters(f -> f.prefixPath("/httpbin").addRequestHeader("X-Request-Acme", "ValueB"))
.uri(uri))
.build();
}
}
}

View File

@@ -24,10 +24,14 @@ import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
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.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
@@ -60,7 +64,16 @@ public class AddRequestParameterGatewayFilterFactoryTests extends BaseWebClientT
testRequestParameterFilter("name", "%E6%89%8E%E6%A0%B9");
}
@Test
public void addRequestParameterFilterWorksEncodedQueryJavaDsl() {
testRequestParameterFilter("www.addreqparamjava.org", "ValueB", "javaname", "%E6%89%8E%E6%A0%B9");
}
private void testRequestParameterFilter(String name, String value) {
testRequestParameterFilter("www.addrequestparameter.org", "ValueA", name, value);
}
private void testRequestParameterFilter(String host, String expectedValue, String name, String value) {
String query;
if (name != null) {
query = "?" + name + "=" + value;
@@ -71,12 +84,12 @@ public class AddRequestParameterGatewayFilterFactoryTests extends BaseWebClientT
boolean checkForEncodedValue = containsEncodedQuery(uri);
testClient.get()
.uri(uri)
.header("Host", "www.addrequestparameter.org")
.header("Host", host)
.exchange()
.expectBody(Map.class)
.consumeWith(response -> {
Map<String, Object> args = getMap(response.getResponseBody(), "args");
assertThat(args).containsEntry("foo", "bar");
assertThat(args).containsEntry("example", expectedValue);
if (name != null) {
if (checkForEncodedValue) {
try {
@@ -94,6 +107,19 @@ public class AddRequestParameterGatewayFilterFactoryTests extends BaseWebClientT
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig { }
public static class TestConfig {
@Value("${test.uri}")
String uri;
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("add_request_param_java_test", r ->
r.path("/get").and().host("**.addreqparamjava.org")
.filters(f -> f.prefixPath("/httpbin").addRequestParameter("example", "ValueB"))
.uri(uri))
.build();
}
}
}

View File

@@ -22,10 +22,14 @@ import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
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.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
@@ -67,15 +71,52 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
.expectBody().json("{\"from\":\"fallbackcontroller\"}");
}
@Test
public void hystrixFilterWorksJavaDsl() {
testClient.get().uri("/get")
.header("Host", "www.hystrixjava.org")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(ROUTE_ID_HEADER, "hystrix_java");
}
@Test
public void hystrixFilterFallbackJavaDsl() {
testClient.get().uri("/delay/3")
.header("Host", "www.hystrixjava.org")
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"from\":\"fallbackcontroller2\"}");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
@RestController
public static class TestConfig {
@Value("${test.uri}")
private String uri;
@RequestMapping("/fallbackcontroller")
public Map<String, String> fallbackcontroller(@RequestParam("a") String a) {
return Collections.singletonMap("from", "fallbackcontroller");
}
@RequestMapping("/fallbackcontroller2")
public Map<String, String> fallbackcontroller2() {
return Collections.singletonMap("from", "fallbackcontroller2");
}
@Bean
public RouteLocator hystrixRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hystrix_java", r -> r.host("**.hystrixjava.org")
.filters(f -> f.prefixPath("/httpbin")
.hystrix(config -> config.setName("javacmd").setFallbackUri("forward:/fallbackcontroller2")))
.uri(uri))
.build();
}
}
}

View File

@@ -28,14 +28,8 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
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.assertStatus;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -44,21 +38,12 @@ public class RedirectToGatewayFilterFactoryTests extends BaseWebClientTests {
@Test
public void redirectToFilterWorks() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/")
.header("Host", "www.redirectto.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertStatus(response, HttpStatus.FOUND);
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders.getFirst(HttpHeaders.LOCATION))
.isEqualTo("http://example.org");
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isEqualTo(HttpStatus.FOUND)
.expectHeader().valueEquals(HttpHeaders.LOCATION, "http://example.org");
}
@EnableAutoConfiguration

View File

@@ -32,10 +32,6 @@ 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;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -44,21 +40,16 @@ public class RemoveRequestHeaderGatewayFilterFactoryTests extends BaseWebClientT
@Test
public void removeRequestHeaderFilterWorks() {
Mono<Map> result = webClient.get()
testClient.get()
.uri("/headers")
.header("Host", "www.removerequestheader.org")
.header("X-Request-Foo", "Bar")
.exchange()
.flatMap(response -> response.body(toMono(Map.class)));
StepVerifier.create(result)
.consumeNextWith(
response -> {
Map<String, Object> headers = getMap(response, "headers");
assertThat(headers).doesNotContainKey("X-Request-Foo");
})
.expectComplete()
.verify(DURATION);
.expectStatus().isOk()
.expectBody(Map.class).consumeWith(result -> {
Map<String, Object> headers = getMap(result.getResponseBody(), "headers");
assertThat(headers).doesNotContainKey("X-Request-Foo");
});
}
@EnableAutoConfiguration

View File

@@ -23,17 +23,12 @@ import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.http.HttpHeaders;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
@@ -41,23 +36,17 @@ public class RemoveResponseHeaderGatewayFilterFactoryTests extends BaseWebClient
@Test
public void removeResponseHeaderFilterWorks() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/headers")
.header("Host", "www.removereresponseheader.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders).doesNotContainKey("X-Request-Foo");
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk()
.expectHeader().doesNotExist("X-Request-Foo");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig { }
}

View File

@@ -8,11 +8,15 @@ import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter.Response;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
@@ -25,8 +29,6 @@ import org.springframework.tuple.Tuple;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter.BURST_CAPACITY_KEY;
import static org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter.REPLENISH_RATE_KEY;
import static org.springframework.tuple.TupleBuilder.tuple;
import reactor.core.publisher.Mono;
@@ -41,7 +43,7 @@ import reactor.core.publisher.Mono;
public class RequestRateLimiterGatewayFilterFactoryTests extends BaseWebClientTests {
@Autowired
private RequestRateLimiterGatewayFilterFactory filterFactory;
private ApplicationContext context;
@MockBean
private RateLimiter rateLimiter;
@@ -70,17 +72,22 @@ public class RequestRateLimiterGatewayFilterFactoryTests extends BaseWebClientTe
private void assertFilterFactory(KeyResolver keyResolver, String key, boolean allowed, HttpStatus expectedStatus) {
Tuple args = tuple().build();
when(rateLimiter.isAllowed(key, args))
when(rateLimiter.isAllowed("myroute", key))
.thenReturn(Mono.just(new Response(allowed, 1)));
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
exchange.getResponse().setStatusCode(HttpStatus.OK);
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR,
Route.builder().id("myroute").predicate(ex -> true)
.uri("http://localhost").build());
when(this.filterChain.filter(exchange)).thenReturn(Mono.empty());
Mono<Void> response = filterFactory.apply(args).filter(exchange, this.filterChain);
RequestRateLimiterGatewayFilterFactory factory = this.context.getBean(RequestRateLimiterGatewayFilterFactory.class);
GatewayFilter filter = factory.apply(config -> config.setKeyResolver(keyResolver));
Mono<Void> response = filter.filter(exchange, this.filterChain);
response.subscribe(aVoid -> assertThat(exchange.getResponse().getStatusCode())
.isEqualTo(expectedStatus));

View File

@@ -24,16 +24,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.test.TestUtils.assertStatus;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -42,18 +36,11 @@ public class RewritePathGatewayFilterFactoryIntegrationTests extends BaseWebClie
@Test
public void rewritePathFilterWorks() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/foo/get")
.header("Host", "www.baz.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertStatus(response, HttpStatus.OK);
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk();
}
@EnableAutoConfiguration

View File

@@ -57,7 +57,7 @@ public class RewritePathGatewayFilterFactoryTests {
}
private ServerWebExchange testRewriteFilter(String regex, String replacement, String actualPath, String expectedPath) {
GatewayFilter filter = new RewritePathGatewayFilterFactory().apply(tuple().of(REGEXP_KEY, regex, REPLACEMENT_KEY, replacement));
GatewayFilter filter = new RewritePathGatewayFilterFactory().apply(c -> c.setRegexp(regex).setReplacement(replacement));
URI url = UriComponentsBuilder.fromUriString("http://localhost"+ actualPath).build(true).toUri();
MockServerHttpRequest request = MockServerHttpRequest

View File

@@ -24,16 +24,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.test.TestUtils.assertStatus;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -42,18 +36,11 @@ public class SetPathGatewayFilterFactoryIntegrationTests extends BaseWebClientTe
@Test
public void setPathFilterDefaultValuesWork() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/foo/get")
.header("Host", "www.setpath.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertStatus(response, HttpStatus.OK);
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk();
}
@EnableAutoConfiguration

View File

@@ -63,7 +63,7 @@ public class SetPathGatewayFilterFactoryTests {
}
private void testRewriteFilter(String template, String actualPath, String expectedPath, HashMap<String, String> variables) {
GatewayFilter filter = new SetPathGatewayFilterFactory().apply(tuple().of(TEMPLATE_KEY, template));
GatewayFilter filter = new SetPathGatewayFilterFactory().apply(c -> c.setTemplate(template));
MockServerHttpRequest request = MockServerHttpRequest
.get("http://localhost"+ actualPath)

View File

@@ -24,38 +24,24 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
public class SetResponseGatewayFilterFactoryTests extends BaseWebClientTests {
public class SetResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests {
@Test
public void setResponseHeaderFilterWorks() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/headers")
.header("Host", "www.setreresponseheader.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders).containsKey("X-Request-Foo");
assertThat(httpHeaders.get("X-Request-Foo")).containsExactly("Bar");
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals("X-Request-Foo", "Bar");
}
@EnableAutoConfiguration

View File

@@ -16,9 +16,6 @@
*/
package org.springframework.cloud.gateway.filter.factory;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootConfiguration;
@@ -26,13 +23,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.test.TestUtils.assertStatus;
/**
* @author Ryan Baxter
@@ -44,23 +38,16 @@ public class StripPrefixGatewayFilterFactoryIntegrationTests extends BaseWebClie
@Test
public void stripPrefixFilterDefaultValuesWork() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/foo/bar/get")
.header("Host", "www.stripprefix.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertStatus(response, HttpStatus.OK);
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk();
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(BaseWebClientTests.DefaultTestConfig.class)
@Import(DefaultTestConfig.class)
public static class TestConfig { }
}

View File

@@ -56,7 +56,7 @@ public class StripPrefixGatewayFilterFactoryTests {
private void testStripPrefixFilter(String actualPath, String expectedPath, int parts) {
GatewayFilter filter = new StripPrefixGatewayFilterFactory().apply(
tuple().of(StripPrefixGatewayFilterFactory.PARTS_KEY, parts));
c -> c.setParts(parts));
MockServerHttpRequest request = MockServerHttpRequest
.get("http://localhost"+ actualPath)

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.filter.ratelimit;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.AfterClass;
@@ -110,15 +111,17 @@ public class PrincipalNameKeyResolverIntegrationTests {
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/myapi/**")
.filters(f -> f.requestRateLimiter(tuple().build())
.filters(f -> f.requestRateLimiter()
.rateLimiter(MyRateLimiter.class, rl -> {})
.and()
.prefixPath("/downstream"))
.uri("http://localhost:"+port))
.build();
}
@Bean
RateLimiter rateLimiter() {
return (id, args) -> Mono.just(new RateLimiter.Response(true, Long.MAX_VALUE));
MyRateLimiter rateLimiter() {
return new MyRateLimiter();
}
@Bean
@@ -136,5 +139,30 @@ public class PrincipalNameKeyResolverIntegrationTests {
UserDetails user = User.withUsername("user").password("{noop}password").roles("USER").build();
return new MapReactiveUserDetailsService(user);
}
class MyRateLimiter implements RateLimiter<Object> {
private HashMap<String, Object> map = new HashMap<>();
@Override
public Mono<Response> isAllowed(String routeId, String id) {
return Mono.just(new RateLimiter.Response(true, Long.MAX_VALUE));
}
@Override
public Class<Object> getConfigClass() {
return Object.class;
}
@Override
public Map<String, Object> getConfig() {
return map;
}
@Override
public Object newConfig() {
return null;
}
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2013-2018 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
*
* http://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.ratelimit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter.Config;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@DirtiesContext
@ActiveProfiles("redis-rate-limiter-config")
public class RedisRateLimiterConfigTests {
@Autowired
private RedisRateLimiter rateLimiter;
@Autowired
private RouteLocator routeLocator;
@Test
public void redisRateConfiguredFromEnvironment() {
assertFilter("redis_rate_limiter_config_test", 10, 20, PrincipalNameKeyResolver.class);
}
@Test
public void redisRateConfiguredFromJavaAPI() {
assertFilter("custom_redis_rate_limiter", 20, 40, MyKeyResolver.class);
}
private void assertFilter(String key, int replenishRate, int burstCapacity, Class<? extends KeyResolver> keyResolverClass) {
assertThat(rateLimiter.getConfig()).containsKey(key);
Config config = rateLimiter.getConfig().get(key);
assertThat(config.getReplenishRate()).isEqualTo(replenishRate);
assertThat(config.getBurstCapacity()).isEqualTo(burstCapacity);
Route route = routeLocator.getRoutes().filter(r -> r.getId().equals(key)).next().block();
assertThat(route).isNotNull();
assertThat(route.getFilters()).hasSize(1);
}
@EnableAutoConfiguration
@SpringBootConfiguration
public static class TestConfig {
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("custom_redis_rate_limiter", r -> r.path("/custom")
.filters(f -> f.requestRateLimiter()
.rateLimiter(RedisRateLimiter.class,
rl -> rl.setBurstCapacity(40).setReplenishRate(20))
.and())
.uri("http://localhost"))
.build();
}
}
private static class MyKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return null;
}
}
}

View File

@@ -26,7 +26,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Spencer Gibb
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "true")
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
public class RedisRateLimiterTests extends BaseWebClientTests {
@@ -43,17 +43,20 @@ public class RedisRateLimiterTests extends BaseWebClientTests {
int replenishRate = 10;
int burstCapacity = 2 * replenishRate;
Tuple args = RedisRateLimiter.args(replenishRate, burstCapacity);
String routeId = "myroute";
rateLimiter.getConfig().put(routeId, new RedisRateLimiter.Config()
.setBurstCapacity(burstCapacity)
.setReplenishRate(replenishRate));
// Bursts work
for (int i = 0; i < burstCapacity; i++) {
Response response = rateLimiter.isAllowed(id, args).block();
Response response = rateLimiter.isAllowed(routeId, id).block();
assertThat(response.isAllowed()).as("Burst # %s is allowed", i).isTrue();
}
Response response = rateLimiter.isAllowed(id, args).block();
Response response = rateLimiter.isAllowed(routeId, id).block();
if (response.isAllowed()) { //TODO: sometimes there is an off by one error
response = rateLimiter.isAllowed(id, args).block();
response = rateLimiter.isAllowed(routeId, id).block();
}
assertThat(response.isAllowed()).as("Burst # %s is not allowed", burstCapacity).isFalse();
@@ -61,11 +64,11 @@ public class RedisRateLimiterTests extends BaseWebClientTests {
// # After the burst is done, check the steady state
for (int i = 0; i < replenishRate; i++) {
response = rateLimiter.isAllowed(id, args).block();
response = rateLimiter.isAllowed(routeId, id).block();
assertThat(response.isAllowed()).as("steady state # %s is allowed", i).isTrue();
}
response = rateLimiter.isAllowed(id, args).block();
response = rateLimiter.isAllowed(routeId, id).block();
assertThat(response.isAllowed()).as("steady state # %s is allowed", replenishRate).isFalse();
}

View File

@@ -73,11 +73,11 @@ public class AfterRoutePredicateFactoryTests {
@Test
public void testPredicates() {
boolean result = new AfterRoutePredicateFactory().apply(ZonedDateTime.now().minusHours(2)).test(getExchange());
boolean result = new AfterRoutePredicateFactory().apply(c -> c.setDatetime(ZonedDateTime.now().minusHours(2).toString())).test(getExchange());
assertThat(result).isTrue();
}
private boolean runPredicate(String dateString) {
return new AfterRoutePredicateFactory().apply(tuple().of(DATETIME_KEY, dateString)).test(getExchange());
return new AfterRoutePredicateFactory().apply(c -> c.setDatetime(dateString)).test(getExchange());
}
}

View File

@@ -73,11 +73,11 @@ public class BeforeRoutePredicateFactoryTests {
@Test
public void testPredicates() {
boolean result = new BeforeRoutePredicateFactory().apply(ZonedDateTime.now().minusHours(2)).test(getExchange());
boolean result = new BeforeRoutePredicateFactory().apply(c -> c.setDatetime(ZonedDateTime.now().minusHours(2).toString())).test(getExchange());
assertThat(result).isFalse();
}
private boolean runPredicate(String dateString) {
return new BeforeRoutePredicateFactory().apply(tuple().of(DATETIME_KEY, dateString)).test(getExchange());
return new BeforeRoutePredicateFactory().apply(c -> c.setDatetime(dateString)).test(getExchange());
}
}

View File

@@ -19,8 +19,10 @@ package org.springframework.cloud.gateway.handler.predicate;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import org.junit.Test;
import org.springframework.cloud.gateway.support.ConfigurationUtils;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
@@ -28,7 +30,6 @@ import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory.DATETIME1_KEY;
import static org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory.DATETIME2_KEY;
import static org.springframework.tuple.TupleBuilder.tuple;
/**
* @author Spencer Gibb
@@ -50,6 +51,8 @@ public class BetweenRoutePredicateFactoryTests {
String dateString1 = minusHours(1);
String dateString2 = plusHours(1);
ZonedDateTime parse = ZonedDateTime.parse(dateString1);
final boolean result = runPredicate(dateString1, dateString2);
assertThat(result).as("Now is not between %s and %s", dateString1, dateString2).isTrue();
@@ -98,14 +101,24 @@ public class BetweenRoutePredicateFactoryTests {
@Test
public void testPredicates() {
boolean result = new BetweenRoutePredicateFactory()
.apply(ZonedDateTime.now().minusHours(2), ZonedDateTime.now().plusHours(1))
.apply(c -> c.setDatetime1(ZonedDateTime.now().minusHours(2).toString())
.setDatetime2(ZonedDateTime.now().plusHours(1).toString()))
.test(getExchange());
assertThat(result).isTrue();
}
boolean runPredicate(String dateString1, String dateString2) {
return new BetweenRoutePredicateFactory().apply(tuple()
.of(DATETIME1_KEY, dateString1, DATETIME2_KEY, dateString2)).test(getExchange());
HashMap<String, Object> map = new HashMap<>();
map.put(DATETIME1_KEY, dateString1);
map.put(DATETIME2_KEY, dateString2);
BetweenRoutePredicateFactory factory = new BetweenRoutePredicateFactory();
BetweenRoutePredicateFactory.Config config = factory.newConfig();
ConfigurationUtils.bind(config, map, "", "myname", null);
return factory.apply(config).test(getExchange());
}
static String minusHoursMillis(int hours) {

View File

@@ -25,18 +25,10 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
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.assertStatus;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -45,42 +37,27 @@ public class HeaderRoutePredicateFactoryTests extends BaseWebClientTests {
@Test
public void headerRouteWorks() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/get")
.header("Foo", "bar")
.exchange();
StepVerifier.create(result)
.consumeNextWith(response -> {
assertStatus(response, HttpStatus.OK);
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER))
.isEqualTo(RoutePredicateHandlerMapping.class.getSimpleName());
assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)).isEqualTo("header_test");
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER,
RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "header_test");
}
@Test
@SuppressWarnings("Duplicates")
public void headerRouteIgnoredWhenHeaderMissing() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/get")
// no headers set. Test used to throw a null pointer exception.
.exchange();
StepVerifier.create(result)
.consumeNextWith(response -> {
assertStatus(response, HttpStatus.OK);
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER))
.isEqualTo(RoutePredicateHandlerMapping.class.getSimpleName());
assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER))
.isEqualTo("default_path_to_httpbin");
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER,
RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin");
}
@EnableAutoConfiguration

View File

@@ -25,18 +25,10 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
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.assertStatus;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -45,23 +37,13 @@ public class HostRoutePredicateFactoryTests extends BaseWebClientTests {
@Test
public void hostRouteWorks() {
Mono<ClientResponse> result = webClient.get()
testClient.get()
.uri("/get")
.header("Host", "www.example.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertStatus(response, HttpStatus.OK);
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER))
.isEqualTo(RoutePredicateHandlerMapping.class.getSimpleName());
assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER))
.isEqualTo("host_example_to_httpbin");
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "host_example_to_httpbin");
}
@EnableAutoConfiguration

View File

@@ -25,18 +25,10 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
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.assertStatus;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -44,24 +36,14 @@ import reactor.test.StepVerifier;
public class MethodRoutePredicateFactoryTests extends BaseWebClientTests {
@Test
public void hostRouteWorks() {
Mono<ClientResponse> result = webClient.get()
public void methodRouteWorks() {
testClient.get()
.uri("/get")
.header("Host", "www.method.org")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertStatus(response, HttpStatus.OK);
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER))
.isEqualTo(RoutePredicateHandlerMapping.class.getSimpleName());
assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER))
.isEqualTo("method_test");
})
.expectComplete()
.verify(DURATION);
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "method_test");
}
@EnableAutoConfiguration

View File

@@ -25,18 +25,10 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
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.assertStatus;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -45,22 +37,11 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
@Test
public void pathRouteWorks() {
Mono<ClientResponse> result = webClient.get()
.uri("/get")
.exchange();
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertStatus(response, HttpStatus.OK);
HttpHeaders httpHeaders = response.headers().asHttpHeaders();
assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER))
.isEqualTo(RoutePredicateHandlerMapping.class.getSimpleName());
assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER))
.isEqualTo("default_path_to_httpbin");
})
.expectComplete()
.verify(DURATION);
testClient.get().uri("/get")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin");
}
@EnableAutoConfiguration

View File

@@ -26,7 +26,7 @@ import reactor.test.StepVerifier;
public class RemoteAddrRoutePredicateFactoryTests extends BaseWebClientTests {
@Test
public void pathRouteWorks() {
public void remoteAddrWorks() {
Mono<ClientResponse> result = webClient.get().uri("/ok/httpbin/").exchange();
StepVerifier.create(result)
@@ -35,7 +35,7 @@ public class RemoteAddrRoutePredicateFactoryTests extends BaseWebClientTests {
}
@Test
public void pathRouteDoNotWork() {
public void remoteAddrRejects() {
Mono<ClientResponse> result = webClient.get().uri("/nok/httpbin/").exchange();
StepVerifier

View File

@@ -11,7 +11,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.support.ArgumentHints;
import org.springframework.cloud.gateway.support.ShortcutConfigurable;
import org.springframework.context.annotation.Bean;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.test.context.junit4.SpringRunner;
@@ -31,16 +31,17 @@ public class RouteDefinitionRouteLocatorTests {
@Test
public void testGetTupleWithSpel() {
parser = new SpelExpressionParser();
ArgumentHints argumentHints = new ArgumentHints() {
ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() {
@Override
public List<String> argNames() {
public List<String> shortcutFieldOrder() {
return Arrays.asList("bean", "arg1");
}
};
Map<String, String> args = new HashMap<>();
args.put("bean", "#{@foo}");
args.put("arg1", "val1");
Tuple tuple = RouteDefinitionRouteLocator.getTuple(argumentHints, args, parser, this.beanFactory);
Tuple tuple = RouteDefinitionRouteLocator.getTuple(shortcutConfigurable, args, parser, this.beanFactory);
assertThat(tuple).isNotNull();
assertThat(tuple.getValue("bean", Integer.class)).isEqualTo(42);
assertThat(tuple.getString("arg1")).isEqualTo("val1");

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2013-2018 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
*
* http://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.support;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType;
import org.springframework.context.annotation.Bean;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ShortcutConfigurableTests {
private SpelExpressionParser parser;
@Autowired
BeanFactory beanFactory;
@Test
public void testNormalizeDefaultTypeWithSpel() {
parser = new SpelExpressionParser();
ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() {
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("bean", "arg1");
}
};
Map<String, String> args = new HashMap<>();
args.put("bean", "#{@foo}");
args.put("arg1", "val1");
Map<String, Object> map = ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory);
assertThat(map).isNotNull()
.containsEntry("bean", 42)
.containsEntry("arg1", "val1");
}
@Test
@SuppressWarnings("unchecked")
public void testNormalizeGatherListTypeWithSpel() {
parser = new SpelExpressionParser();
ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() {
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("values");
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST;
}
};
Map<String, String> args = new HashMap<>();
args.put("1", "#{@foo}");
args.put("2", "val1");
args.put("3", "val2");
Map<String, Object> map = ShortcutType.GATHER_LIST.normalize(args, shortcutConfigurable, parser, this.beanFactory);
assertThat(map).isNotNull().containsKey("values");
assertThat((List)map.get("values"))
.containsExactly(42, "val1", "val2");
}
@SpringBootConfiguration
protected static class TestConfig {
@Bean
public Integer foo() {
return 42;
}
}
}

View File

@@ -38,7 +38,7 @@ import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilter
import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactoryTests;
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactoryIntegrationTests;
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactoryTests;
import org.springframework.cloud.gateway.filter.factory.SetResponseGatewayFilterFactoryTests;
import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactoryTests;
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactoryTests;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilterTests;
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilterTests;
@@ -88,7 +88,7 @@ import static org.junit.Assume.assumeThat;
RequestRateLimiterGatewayFilterFactoryTests.class,
SetPathGatewayFilterFactoryIntegrationTests.class,
AddRequestParameterGatewayFilterFactoryTests.class,
SetResponseGatewayFilterFactoryTests.class,
SetResponseHeaderGatewayFilterFactoryTests.class,
PrincipalNameKeyResolverIntegrationTests.class,
RedisRateLimiterTests.class,
RouteDefinitionRouteLocatorTests.class,

View File

@@ -104,10 +104,6 @@ public class BaseWebClientTests {
@Import(DefaultTestConfig.class)
public static class MainConfig { }
public static void main(String[] args) {
new SpringApplication(MainConfig.class).run(args);
}
protected static class TestRibbonConfig {
@LocalServerPort

View File

@@ -0,0 +1,20 @@
test:
uri: lb://myservice
spring:
cloud:
gateway:
default-filters:
routes:
# =====================================
- id: redis_rate_limiter_config_test
uri: ${test.uri}
predicates:
- Path=/
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter:
replenish-rate: 10
burst-capacity: 20

View File

@@ -11,4 +11,4 @@ spring:
predicates:
- Path=/headers
filters:
- AddRequestHeader=X-Request-Foo, Bar
- AddRequestHeader=X-Request-Example, ValueA

View File

@@ -12,4 +12,4 @@ spring:
- Host=**.addrequestparameter.org
- Path=/get
filters:
- AddRequestParameter=foo, bar
- AddRequestParameter=example, ValueA

View File

@@ -229,11 +229,6 @@ spring:
args:
pattern: /**
testservice:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
listOfServers: ${test.hostport}
hystrix.command.successcmd.execution.isolation.thread.timeoutInMilliseconds: 5000
logging:

View File

@@ -41,7 +41,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
public class GatewaySampleApplication {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) {
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
//@formatter:off
return builder.routes()
.route(r -> r.host("**.abc.org").and().path("/image/png")
@@ -56,21 +56,17 @@ public class GatewaySampleApplication {
)
.route(r -> r.order(-1)
.host("**.throttle.org").and().path("/get")
.filters(f -> f.filter(throttle.apply(1,
1,
10,
TimeUnit.SECONDS)))
.filters(f -> f.filter(new ThrottleGatewayFilter()
.setCapacity(1)
.setRefillTokens(1)
.setRefillPeriod(10)
.setRefillUnit(TimeUnit.SECONDS)))
.uri("http://httpbin.org:80")
)
.build();
//@formatter:on
}
@Bean
public ThrottleGatewayFilterFactory throttleWebFilterFactory() {
return new ThrottleGatewayFilterFactory();
}
@Bean
public RouterFunction<ServerResponse> testFunRouterFunction() {
RouterFunction<ServerResponse> route = RouterFunctions.route(

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2013-2017 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
*
* http://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.sample;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.isomorphism.util.TokenBucket;
import org.isomorphism.util.TokenBuckets;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Sample throttling filter.
* See https://github.com/bbeck/token-bucket
*/
public class ThrottleGatewayFilter implements GatewayFilter {
private static final Log log = LogFactory.getLog(ThrottleGatewayFilter.class);
int capacity;
int refillTokens;
int refillPeriod;
TimeUnit refillUnit;
public int getCapacity() {
return capacity;
}
public ThrottleGatewayFilter setCapacity(int capacity) {
this.capacity = capacity;
return this;
}
public int getRefillTokens() {
return refillTokens;
}
public ThrottleGatewayFilter setRefillTokens(int refillTokens) {
this.refillTokens = refillTokens;
return this;
}
public int getRefillPeriod() {
return refillPeriod;
}
public ThrottleGatewayFilter setRefillPeriod(int refillPeriod) {
this.refillPeriod = refillPeriod;
return this;
}
public TimeUnit getRefillUnit() {
return refillUnit;
}
public ThrottleGatewayFilter setRefillUnit(TimeUnit refillUnit) {
this.refillUnit = refillUnit;
return this;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
TokenBucket tokenBucket = TokenBuckets.builder()
.withCapacity(capacity)
.withFixedIntervalRefillStrategy(refillTokens, refillPeriod, refillUnit)
.build();
//TODO: get a token bucket for a key
log.debug("TokenBucket capacity: " + tokenBucket.getCapacity());
boolean consumed = tokenBucket.tryConsume();
if (consumed) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2013-2017 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
*
* http://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.sample;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.isomorphism.util.TokenBucket;
import org.isomorphism.util.TokenBuckets;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.tuple.Tuple;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import java.util.concurrent.TimeUnit;
/**
* Sample throttling filter.
* See https://github.com/bbeck/token-bucket
*/
public class ThrottleGatewayFilterFactory implements GatewayFilterFactory {
private Log log = LogFactory.getLog(getClass());
@Override
public GatewayFilter apply(Tuple args) {
int capacity = args.getInt("capacity");
int refillTokens = args.getInt("refillTokens");
int refillPeriod = args.getInt("refillPeriod");
TimeUnit refillUnit = TimeUnit.valueOf(args.getString("refillUnit"));
return apply(capacity, refillTokens, refillPeriod, refillUnit);
}
public GatewayFilter apply(int capacity, int refillTokens, int refillPeriod, TimeUnit refillUnit) {
final TokenBucket tokenBucket = TokenBuckets.builder()
.withCapacity(capacity)
.withFixedIntervalRefillStrategy(refillTokens, refillPeriod, refillUnit)
.build();
return (exchange, chain) -> {
//TODO: get a token bucket for a key
log.debug("TokenBucket capacity: " + tokenBucket.getCapacity());
boolean consumed = tokenBucket.tryConsume();
if (consumed) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
};
}
}

View File

@@ -28,6 +28,8 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.SocketUtils;
import java.time.Duration;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
@@ -59,7 +61,7 @@ public class GatewaySampleApplicationTests {
@Before
public void setup() {
baseUri = "http://localhost:" + port;
this.webClient = WebTestClient.bindToServer().baseUrl(baseUri).build();
this.webClient = WebTestClient.bindToServer().responseTimeout(Duration.ofSeconds(10)).baseUrl(baseUri).build();
}
@Test