From b25fca48913e5201819bcc92686c4cc13dd748ae Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 13 Jan 2017 19:04:49 -0700 Subject: [PATCH] initial per Route filter mechanism --- .../gateway/config/FilterDefinition.java | 90 +++++++++++++++++++ .../config/GatewayAutoConfiguration.java | 23 ++++- .../cloud/gateway/config/Route.java | 12 +++ .../definition/AppendRequestHeaderFilter.java | 31 +++++++ .../AppendResponseHeaderFilter.java | 29 ++++++ .../definition/GatewayFilterDefinition.java | 13 +++ .../handler/GatewayFilteringWebHandler.java | 66 ++++++++++++-- .../gateway/test/GatewayIntegrationTests.java | 21 +++++ src/test/resources/application.yml | 14 ++- 9 files changed, 288 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/springframework/cloud/gateway/config/FilterDefinition.java create mode 100644 src/main/java/org/springframework/cloud/gateway/filter/definition/AppendRequestHeaderFilter.java create mode 100644 src/main/java/org/springframework/cloud/gateway/filter/definition/AppendResponseHeaderFilter.java create mode 100644 src/main/java/org/springframework/cloud/gateway/filter/definition/GatewayFilterDefinition.java diff --git a/src/main/java/org/springframework/cloud/gateway/config/FilterDefinition.java b/src/main/java/org/springframework/cloud/gateway/config/FilterDefinition.java new file mode 100644 index 00000000..6b804b3d --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/config/FilterDefinition.java @@ -0,0 +1,90 @@ +package org.springframework.cloud.gateway.config; + +import java.util.Arrays; +import java.util.Objects; + +import javax.validation.ValidationException; +import javax.validation.constraints.NotNull; + +import static org.springframework.util.StringUtils.tokenizeToStringArray; + +/** + * @author Spencer Gibb + */ +public class FilterDefinition { + @NotNull + private String name; + @NotNull + private String value; + + private String[] args; + + public FilterDefinition() { + } + + public FilterDefinition(String text) { + int eqIdx = text.indexOf("="); + if (eqIdx <= 0) { + throw new ValidationException("Unable to parse FilterDefinition text '" + text + "'" + + ", must be of the form name=value"); + } + setName(text.substring(0, eqIdx)); + + String[] args = tokenizeToStringArray(text.substring(eqIdx+1), ","); + + setValue(args[0]); + + if (args.length > 1) { + setArgs(Arrays.copyOfRange(args, 1, args.length)); + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String[] getArgs() { + return args; + } + + public void setArgs(String[] args) { + this.args = args; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FilterDefinition that = (FilterDefinition) o; + return Objects.equals(name, that.name) && + Objects.equals(value, that.value) && + Arrays.equals(args, that.args); + } + + @Override + public int hashCode() { + return Objects.hash(name, value, args); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("FilterDefinition{"); + sb.append("name='").append(name).append('\''); + sb.append(", value='").append(value).append('\''); + sb.append(", args=").append(Arrays.toString(args)); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index e61cabf6..3e5273e4 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -9,6 +9,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.cloud.gateway.actuate.GatewayEndpoint; import org.springframework.cloud.gateway.api.RouteReader; import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.definition.AppendRequestHeaderFilter; +import org.springframework.cloud.gateway.filter.definition.AppendResponseHeaderFilter; +import org.springframework.cloud.gateway.filter.definition.GatewayFilterDefinition; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.handler.GatewayFilteringWebHandler; import org.springframework.cloud.gateway.handler.GatewayPredicateHandlerMapping; @@ -60,10 +63,14 @@ public class GatewayAutoConfiguration { } @Bean - public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler, GatewayFilter[] filters) { - return new GatewayFilteringWebHandler(gatewayWebHandler, filters); + public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler, + List filters, + List filterDefinitions) { + return new GatewayFilteringWebHandler(gatewayWebHandler, filters, filterDefinitions); } + // Predicate beans + @Bean public GatewayPredicateHandlerMapping gatewayPredicateHandlerMapping(GatewayFilteringWebHandler webHandler, List predicates, @@ -101,6 +108,18 @@ public class GatewayAutoConfiguration { return new UrlPredicate(); } + // Filter beans + + @Bean + public AppendRequestHeaderFilter appendRequestHeaderFilter() { + return new AppendRequestHeaderFilter(); + } + + @Bean + public AppendResponseHeaderFilter appendResponseHeaderFilter() { + return new AppendResponseHeaderFilter(); + } + @Configuration @ConditionalOnClass(Endpoint.class) protected static class GatewayActuatorConfiguration { diff --git a/src/main/java/org/springframework/cloud/gateway/config/Route.java b/src/main/java/org/springframework/cloud/gateway/config/Route.java index 860f5aa9..42749335 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/Route.java +++ b/src/main/java/org/springframework/cloud/gateway/config/Route.java @@ -24,6 +24,9 @@ public class Route { @Valid private List predicates = new ArrayList<>(); + @Valid + private List filters = new ArrayList<>(); + @NotNull private URI uri; @@ -63,6 +66,14 @@ public class Route { this.predicates = predicates; } + public List getFilters() { + return filters; + } + + public void setFilters(List filters) { + this.filters = filters; + } + public URI getUri() { return uri; } @@ -91,6 +102,7 @@ public class Route { return "Route{" + "id='" + id + '\'' + ", predicates=" + predicates + + ", filters=" + filters + ", uri=" + uri + '}'; } diff --git a/src/main/java/org/springframework/cloud/gateway/filter/definition/AppendRequestHeaderFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/definition/AppendRequestHeaderFilter.java new file mode 100644 index 00000000..1f9a644d --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/definition/AppendRequestHeaderFilter.java @@ -0,0 +1,31 @@ +package org.springframework.cloud.gateway.filter.definition; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.Assert; + +/** + * @author Spencer Gibb + */ +public class AppendRequestHeaderFilter implements GatewayFilterDefinition { + + @Override + public String getName() { + return "AppendRequestHeader"; + } + + @Override + public GatewayFilter apply(String header, String[] args) { + Assert.isTrue(args != null && args.length == 1, + "args must have one entry"); + + //TODO: caching can happen here + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest().mutate() + .header(header, args[0]) + .build(); + + return chain.filter(exchange.mutate().request(request).build()); + }; + } +} diff --git a/src/main/java/org/springframework/cloud/gateway/filter/definition/AppendResponseHeaderFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/definition/AppendResponseHeaderFilter.java new file mode 100644 index 00000000..be82000e --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/definition/AppendResponseHeaderFilter.java @@ -0,0 +1,29 @@ +package org.springframework.cloud.gateway.filter.definition; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.Assert; + +/** + * @author Spencer Gibb + */ +public class AppendResponseHeaderFilter implements GatewayFilterDefinition { + + @Override + public String getName() { + return "AppendResponseHeader"; + } + + @Override + public GatewayFilter apply(String header, String[] args) { + Assert.isTrue(args != null && args.length == 1, + "args must have one entry"); + + //TODO: caching can happen here + return (exchange, chain) -> { + exchange.getResponse().getHeaders().add(header, args[0]); + + return chain.filter(exchange); + }; + } +} diff --git a/src/main/java/org/springframework/cloud/gateway/filter/definition/GatewayFilterDefinition.java b/src/main/java/org/springframework/cloud/gateway/filter/definition/GatewayFilterDefinition.java new file mode 100644 index 00000000..fdd1c76a --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/definition/GatewayFilterDefinition.java @@ -0,0 +1,13 @@ +package org.springframework.cloud.gateway.filter.definition; + +import org.springframework.cloud.gateway.filter.GatewayFilter; + +/** + * @author Spencer Gibb + */ +public interface GatewayFilterDefinition { + + String getName(); + + GatewayFilter apply(String value, String[] args); +} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/GatewayFilteringWebHandler.java b/src/main/java/org/springframework/cloud/gateway/handler/GatewayFilteringWebHandler.java index efa5237f..73d9b664 100644 --- a/src/main/java/org/springframework/cloud/gateway/handler/GatewayFilteringWebHandler.java +++ b/src/main/java/org/springframework/cloud/gateway/handler/GatewayFilteringWebHandler.java @@ -16,11 +16,21 @@ package org.springframework.cloud.gateway.handler; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.gateway.config.Route; import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.definition.GatewayFilterDefinition; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; @@ -28,6 +38,9 @@ import org.springframework.web.server.handler.WebHandlerDecorator; import reactor.core.publisher.Mono; +import static java.util.Collections.emptyList; +import static org.springframework.cloud.gateway.filter.GatewayFilter.GATEWAY_ROUTE_ATTR; + /** * WebHandler that delegates to a chain of {@link GatewayFilter} instances and then * to the target {@link WebHandler}. @@ -36,20 +49,22 @@ import reactor.core.publisher.Mono; * @since 5.0 */ public class GatewayFilteringWebHandler extends WebHandlerDecorator { + protected final Log logger = LogFactory.getLog(getClass()); private final List filters; + private final Map filterDefinitions = new HashMap<>(); - - public GatewayFilteringWebHandler(WebHandler targetHandler, GatewayFilter... filters) { + public GatewayFilteringWebHandler(WebHandler targetHandler, List filters, + List filterDefinitions) { super(targetHandler); this.filters = initList(filters); + initList(filterDefinitions).forEach(def -> this.filterDefinitions.put(def.getName(), def)); } - private static List initList(GatewayFilter[] list) { - return (list != null ? Collections.unmodifiableList(Arrays.asList(list)) : Collections.emptyList()); + private static List initList(List list) { + return (list != null ? list : emptyList()); } - /** * Return a read-only list of the configured filters. */ @@ -59,13 +74,48 @@ public class GatewayFilteringWebHandler extends WebHandlerDecorator { @Override public Mono handle(ServerWebExchange exchange) { - return new DefaultWebFilterChain().filter(exchange); + //TODO: probably a java 8 stream way of doing this + ArrayList routeFilters = new ArrayList<>(this.filters); + Optional route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); + if (route.isPresent() && !route.get().getFilters().isEmpty()) { + routeFilters.addAll(loadFilters(route.get())); + } + return new DefaultWebFilterChain(routeFilters, getDelegate()).filter(exchange); + } + + private Collection loadFilters(Route route) { + return route.getFilters().stream() + .map(definition -> { + GatewayFilterDefinition filter = this.filterDefinitions.get(definition.getName()); + if (filter == null) { + throw new IllegalArgumentException("Unable to find GatewayFilterDefinition with name " + definition.getName()); + } + if (logger.isDebugEnabled()) { + List args; + if (definition.getArgs() != null) { + args = Arrays.asList(definition.getArgs()); + } else { + args = Collections.emptyList(); + } + logger.debug("Route " + route.getId() + " applying filter "+ definition.getValue() + + ", " + args + " to " + definition.getName()); + } + return filter.apply(definition.getValue(), definition.getArgs()); + }) + .collect(Collectors.toList()); } - private class DefaultWebFilterChain implements WebFilterChain { + private static class DefaultWebFilterChain implements WebFilterChain { private int index; + private final List filters; + private final WebHandler delegate; + + public DefaultWebFilterChain(List filters, WebHandler delegate) { + this.filters = filters; + this.delegate = delegate; + } @Override public Mono filter(ServerWebExchange exchange) { @@ -74,7 +124,7 @@ public class GatewayFilteringWebHandler extends WebHandlerDecorator { return filter.filter(exchange, this); } else { - return getDelegate().handle(exchange); + return this.delegate.handle(exchange); } } } diff --git a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index 07494efa..de30d019 100644 --- a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -116,6 +116,25 @@ public class GatewayIntegrationTests { .verify(Duration.ofSeconds(3)); } + @Test + public void appendRequestHeaderFilterWorks() { + Mono result = webClient.exchange( + GET("http://localhost:" + port + "/headers") + .header("Host", "www.bar.org") + .build() + ).then(response -> response.body(toMono(Map.class))); + + StepVerifier.create(result) + .consumeNextWith( + response -> { + assertThat(response).containsKey("headers").isInstanceOf(Map.class); + Map headers = (Map) response.get("headers"); + assertThat(headers).containsEntry("X-Request-Foo", "Bar"); + }) + .expectComplete() + .verify(Duration.ofSeconds(3)); + } + @Test public void postWorks() { ClientRequest> request = POST("http://localhost:" + port + "/post") @@ -162,6 +181,8 @@ public class GatewayIntegrationTests { .isEqualTo(GatewayPredicateHandlerMapping.class.getSimpleName()); assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)) .isEqualTo("host_foo_path_headers_to_httpbin"); + assertThat(httpHeaders.getFirst("X-Response-Foo")) + .isEqualTo("Bar"); assertThat(statusCode).isEqualTo(HttpStatus.OK); }) .expectComplete() diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 72cc330c..45f9a9fd 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -17,6 +17,18 @@ spring: - Query=foo, ba. - Query=baz - Cookie=chocolate, ch.p + filters: + - AppendRequestHeader=X-Request-Foo, Bar + - AppendResponseHeader=X-Response-Foo, Bar + + # ===================================== + - id: host_bar_path_headers_to_httpbin + uri: http://httpbin.org:80 + predicates: + - Host=**.bar.org + - Url=/headers + filters: + - AppendRequestHeader=X-Request-Foo, Bar # ===================================== - id: default_path_to_httpbin @@ -30,4 +42,4 @@ logging: management: context-path: /admin -# port: 8081 + port: 8081