Signed-off-by: Eric Zhao <sczyh16@gmail.com>master
@@ -23,6 +23,7 @@ | |||
<module>sentinel-reactor-adapter</module> | |||
<module>sentinel-spring-webflux-adapter</module> | |||
<module>sentinel-api-gateway-adapter-common</module> | |||
<module>sentinel-spring-cloud-gateway-adapter</module> | |||
</modules> | |||
<dependencyManagement> | |||
@@ -0,0 +1,54 @@ | |||
# Sentinel Spring Cloud Gateway Adapter | |||
> Note: this module requires Java 8 or later version. | |||
Sentinel provides integration module with Spring Cloud Gateway. | |||
The integration module is based on the Sentinel Reactor Adapter. | |||
Add the following dependency in `pom.xml` (if you are using Maven): | |||
```xml | |||
<dependency> | |||
<groupId>com.alibaba.csp</groupId> | |||
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> | |||
<version>x.y.z</version> | |||
</dependency> | |||
``` | |||
Then you only need to inject the corresponding `SentinelGatewayFilter` and `SentinelGatewayBlockExceptionHandler` instance | |||
in Spring configuration. For example: | |||
```java | |||
@Configuration | |||
public class GatewayConfiguration { | |||
private final List<ViewResolver> viewResolvers; | |||
private final ServerCodecConfigurer serverCodecConfigurer; | |||
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, | |||
ServerCodecConfigurer serverCodecConfigurer) { | |||
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); | |||
this.serverCodecConfigurer = serverCodecConfigurer; | |||
} | |||
@Bean | |||
@Order(-1) | |||
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { | |||
// Register the block exception handler for Spring Cloud Gateway. | |||
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); | |||
} | |||
@Bean | |||
@Order(-1) | |||
public GlobalFilter sentinelGatewayFilter() { | |||
return new SentinelGatewayFilter(); | |||
} | |||
} | |||
``` | |||
The gateway adapter will regard all `routeId` (defined in Spring properties) and all customized API definitions | |||
(defined in `GatewayApiDefinitionManager` of `sentinel-api-gateway-adapter-common` module) as resources. | |||
You can register various customized callback in `GatewayCallbackManager`: | |||
- `setBlockHandler`: register a customized `BlockRequestHandler` to handle the blocked request. The default implementation is `DefaultBlockRequestHandler`, which returns default message like `Blocked by Sentinel: FlowException`. |
@@ -0,0 +1,82 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project xmlns="http://maven.apache.org/POM/4.0.0" | |||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||
<parent> | |||
<artifactId>sentinel-adapter</artifactId> | |||
<groupId>com.alibaba.csp</groupId> | |||
<version>1.6.0-SNAPSHOT</version> | |||
</parent> | |||
<modelVersion>4.0.0</modelVersion> | |||
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> | |||
<packaging>jar</packaging> | |||
<properties> | |||
<java.source.version>1.8</java.source.version> | |||
<java.target.version>1.8</java.target.version> | |||
<spring.cloud.gateway.version>2.1.1.RELEASE</spring.cloud.gateway.version> | |||
<spring.boot.version>2.1.4.RELEASE</spring.boot.version> | |||
<spring.version>5.1.5.RELEASE</spring.version> | |||
</properties> | |||
<dependencies> | |||
<dependency> | |||
<groupId>com.alibaba.csp</groupId> | |||
<artifactId>sentinel-api-gateway-adapter-common</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.alibaba.csp</groupId> | |||
<artifactId>sentinel-reactor-adapter</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.cloud</groupId> | |||
<artifactId>spring-cloud-gateway-core</artifactId> | |||
<version>${spring.cloud.gateway.version}</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework</groupId> | |||
<artifactId>spring-webflux</artifactId> | |||
<version>${spring.version}</version> | |||
<scope>provided</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.cloud</groupId> | |||
<artifactId>spring-cloud-starter-gateway</artifactId> | |||
<version>${spring.cloud.gateway.version}</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-webflux</artifactId> | |||
<version>${spring.boot.version}</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-test</artifactId> | |||
<version>${spring.boot.version}</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.assertj</groupId> | |||
<artifactId>assertj-core</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.mockito</groupId> | |||
<artifactId>mockito-core</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
</dependencies> | |||
</project> |
@@ -0,0 +1,90 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* 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 com.alibaba.csp.sentinel.adapter.gateway.sc; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import com.alibaba.csp.sentinel.EntryType; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayParamParser; | |||
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; | |||
import com.alibaba.csp.sentinel.adapter.reactor.ContextConfig; | |||
import com.alibaba.csp.sentinel.adapter.reactor.EntryConfig; | |||
import com.alibaba.csp.sentinel.adapter.reactor.SentinelReactorTransformer; | |||
import com.alibaba.csp.sentinel.adapter.gateway.sc.api.GatewayApiMatcherManager; | |||
import com.alibaba.csp.sentinel.adapter.gateway.sc.api.matcher.WebExchangeApiMatcher; | |||
import org.springframework.cloud.gateway.filter.GatewayFilter; | |||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; | |||
import org.springframework.cloud.gateway.filter.GlobalFilter; | |||
import org.springframework.cloud.gateway.route.Route; | |||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; | |||
import org.springframework.web.server.ServerWebExchange; | |||
import reactor.core.publisher.Mono; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class SentinelGatewayFilter implements GatewayFilter, GlobalFilter { | |||
private final GatewayParamParser<ServerWebExchange> paramParser = new GatewayParamParser<>( | |||
new ServerWebExchangeItemParser()); | |||
@Override | |||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { | |||
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); | |||
Mono<Void> asyncResult = chain.filter(exchange); | |||
if (route != null) { | |||
String routeId = route.getId(); | |||
Object[] params = paramParser.parseParameterFor(routeId, exchange, | |||
r -> r.getResourceMode() == SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID); | |||
String origin = Optional.ofNullable(GatewayCallbackManager.getRequestOriginParser()) | |||
.map(f -> f.apply(exchange)) | |||
.orElse(""); | |||
asyncResult = asyncResult.transform( | |||
new SentinelReactorTransformer<>(new EntryConfig(routeId, EntryType.IN, | |||
1, params, new ContextConfig(contextName(routeId), origin))) | |||
); | |||
} | |||
Set<String> matchingApis = pickMatchingApiDefinitions(exchange); | |||
for (String apiName : matchingApis) { | |||
Object[] params = paramParser.parseParameterFor(apiName, exchange, | |||
r -> r.getResourceMode() == SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME); | |||
asyncResult = asyncResult.transform( | |||
new SentinelReactorTransformer<>(new EntryConfig(apiName, EntryType.IN, 1, params)) | |||
); | |||
} | |||
return asyncResult; | |||
} | |||
private String contextName(String route) { | |||
return SentinelGatewayConstants.GATEWAY_CONTEXT_ROUTE_PREFIX + route; | |||
} | |||
Set<String> pickMatchingApiDefinitions(ServerWebExchange exchange) { | |||
return GatewayApiMatcherManager.getApiMatcherMap().values() | |||
.stream() | |||
.filter(m -> m.test(exchange)) | |||
.map(WebExchangeApiMatcher::getApiName) | |||
.collect(Collectors.toSet()); | |||
} | |||
} |
@@ -0,0 +1,53 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* https://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc; | |||
import java.net.InetSocketAddress; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.param.RequestItemParser; | |||
import org.springframework.web.server.ServerWebExchange; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class ServerWebExchangeItemParser implements RequestItemParser<ServerWebExchange> { | |||
@Override | |||
public String getPath(ServerWebExchange exchange) { | |||
return exchange.getRequest().getPath().value(); | |||
} | |||
@Override | |||
public String getRemoteAddress(ServerWebExchange exchange) { | |||
InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress(); | |||
if (remoteAddress == null) { | |||
return null; | |||
} | |||
return remoteAddress.getAddress().getHostAddress(); | |||
} | |||
@Override | |||
public String getHeader(ServerWebExchange exchange, String key) { | |||
return exchange.getRequest().getHeaders().getFirst(key); | |||
} | |||
@Override | |||
public String getUrlParam(ServerWebExchange exchange, String paramName) { | |||
return exchange.getRequest().getQueryParams().getFirst(paramName); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* https://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc.api; | |||
import java.util.Collections; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import java.util.stream.Collectors; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||
import com.alibaba.csp.sentinel.adapter.gateway.sc.api.matcher.WebExchangeApiMatcher; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public final class GatewayApiMatcherManager { | |||
private static final Map<String, WebExchangeApiMatcher> API_MATCHER_MAP = new ConcurrentHashMap<>(); | |||
public static Map<String, WebExchangeApiMatcher> getApiMatcherMap() { | |||
return Collections.unmodifiableMap(API_MATCHER_MAP); | |||
} | |||
public static Optional<WebExchangeApiMatcher> getMatcher(final String apiName) { | |||
return Optional.ofNullable(apiName) | |||
.map(e -> API_MATCHER_MAP.get(apiName)); | |||
} | |||
public static Set<ApiDefinition> getApiDefinitionSet() { | |||
return API_MATCHER_MAP.values() | |||
.stream() | |||
.map(WebExchangeApiMatcher::getApiDefinition) | |||
.collect(Collectors.toSet()); | |||
} | |||
static synchronized void loadApiDefinitions(/*@Valid*/ Set<ApiDefinition> definitions) { | |||
if (definitions == null || definitions.isEmpty()) { | |||
API_MATCHER_MAP.clear(); | |||
return; | |||
} | |||
definitions.forEach(GatewayApiMatcherManager::addApiDefinition); | |||
} | |||
static void addApiDefinition(ApiDefinition definition) { | |||
API_MATCHER_MAP.put(definition.getApiName(), new WebExchangeApiMatcher(definition)); | |||
} | |||
private GatewayApiMatcherManager() {} | |||
} |
@@ -0,0 +1,33 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* https://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc.api; | |||
import java.util.Set; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinitionChangeObserver; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class SpringCloudGatewayApiDefinitionChangeObserver implements ApiDefinitionChangeObserver { | |||
@Override | |||
public void onChange(Set<ApiDefinition> apiDefinitions) { | |||
GatewayApiMatcherManager.loadApiDefinitions(apiDefinitions); | |||
} | |||
} |
@@ -0,0 +1,70 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* https://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc.api.matcher; | |||
import java.util.Optional; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.matcher.AbstractApiMatcher; | |||
import com.alibaba.csp.sentinel.adapter.gateway.sc.route.RouteMatchers; | |||
import com.alibaba.csp.sentinel.util.StringUtil; | |||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||
import org.springframework.web.server.ServerWebExchange; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class WebExchangeApiMatcher extends AbstractApiMatcher<ServerWebExchange> { | |||
public WebExchangeApiMatcher(ApiDefinition apiDefinition) { | |||
super(apiDefinition); | |||
} | |||
@Override | |||
protected void initializeMatchers() { | |||
if (apiDefinition.getPredicateItems() != null) { | |||
apiDefinition.getPredicateItems().forEach(item -> | |||
fromApiPredicate(item).ifPresent(matchers::add)); | |||
} | |||
} | |||
private Optional<Predicate<ServerWebExchange>> fromApiPredicate(/*@NonNull*/ ApiPredicateItem item) { | |||
if (item instanceof ApiPathPredicateItem) { | |||
return fromApiPathPredicate((ApiPathPredicateItem)item); | |||
} | |||
return Optional.empty(); | |||
} | |||
private Optional<Predicate<ServerWebExchange>> fromApiPathPredicate(/*@Valid*/ ApiPathPredicateItem item) { | |||
String pattern = item.getPattern(); | |||
if (StringUtil.isBlank(pattern)) { | |||
return Optional.empty(); | |||
} | |||
switch (item.getMatchStrategy()) { | |||
case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX: | |||
return Optional.of(RouteMatchers.regexPath(pattern)); | |||
case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX: | |||
return Optional.of(RouteMatchers.antPath(pattern)); | |||
default: | |||
return Optional.of(RouteMatchers.exactPath(pattern)); | |||
} | |||
} | |||
} |
@@ -0,0 +1,38 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* 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 com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||
import org.springframework.web.reactive.function.server.ServerResponse; | |||
import org.springframework.web.server.ServerWebExchange; | |||
import reactor.core.publisher.Mono; | |||
/** | |||
* Reactive handler for the blocked request. | |||
* | |||
* @author Eric Zhao | |||
*/ | |||
@FunctionalInterface | |||
public interface BlockRequestHandler { | |||
/** | |||
* Handle the blocked request. | |||
* | |||
* @param exchange server exchange object | |||
* @param t block exception | |||
* @return server response to return | |||
*/ | |||
Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t); | |||
} |
@@ -0,0 +1,93 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* 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 com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||
import java.util.List; | |||
import org.springframework.http.HttpStatus; | |||
import org.springframework.http.InvalidMediaTypeException; | |||
import org.springframework.http.MediaType; | |||
import org.springframework.web.reactive.function.server.ServerResponse; | |||
import org.springframework.web.server.ServerWebExchange; | |||
import reactor.core.publisher.Mono; | |||
import static org.springframework.web.reactive.function.BodyInserters.fromObject; | |||
/** | |||
* The default implementation of {@link BlockRequestHandler}. | |||
* Compatible with Spring WebFlux and Spring Cloud Gateway. | |||
* | |||
* @author Eric Zhao | |||
*/ | |||
public class DefaultBlockRequestHandler implements BlockRequestHandler { | |||
private static final String DEFAULT_BLOCK_MSG_PREFIX = "Blocked by Sentinel: "; | |||
@Override | |||
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable ex) { | |||
if (acceptsHtml(exchange)) { | |||
return htmlErrorResponse(ex); | |||
} | |||
// JSON result by default. | |||
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) | |||
.contentType(MediaType.APPLICATION_JSON_UTF8) | |||
.body(fromObject(buildErrorResult(ex))); | |||
} | |||
private Mono<ServerResponse> htmlErrorResponse(Throwable ex) { | |||
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) | |||
.contentType(MediaType.TEXT_PLAIN) | |||
.syncBody(DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName()); | |||
} | |||
private ErrorResult buildErrorResult(Throwable ex) { | |||
return new ErrorResult(HttpStatus.TOO_MANY_REQUESTS.value(), | |||
DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName()); | |||
} | |||
/** | |||
* Reference from {@code DefaultErrorWebExceptionHandler} of Spring Boot. | |||
*/ | |||
private boolean acceptsHtml(ServerWebExchange exchange) { | |||
try { | |||
List<MediaType> acceptedMediaTypes = exchange.getRequest().getHeaders().getAccept(); | |||
acceptedMediaTypes.remove(MediaType.ALL); | |||
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes); | |||
return acceptedMediaTypes.stream() | |||
.anyMatch(MediaType.TEXT_HTML::isCompatibleWith); | |||
} catch (InvalidMediaTypeException ex) { | |||
return false; | |||
} | |||
} | |||
private static class ErrorResult { | |||
private final int code; | |||
private final String message; | |||
ErrorResult(int code, String message) { | |||
this.code = code; | |||
this.message = message; | |||
} | |||
public int getCode() { | |||
return code; | |||
} | |||
public String getMessage() { | |||
return message; | |||
} | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* https://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||
import java.util.function.Function; | |||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||
import org.springframework.web.server.ServerWebExchange; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public final class GatewayCallbackManager { | |||
private static final Function<ServerWebExchange, String> DEFAULT_ORIGIN_PARSER = (w) -> ""; | |||
/** | |||
* BlockRequestHandler: (serverExchange, exception) -> response | |||
*/ | |||
private static volatile BlockRequestHandler blockHandler = new DefaultBlockRequestHandler(); | |||
/** | |||
* RequestOriginParser: (serverExchange) -> origin | |||
*/ | |||
private static volatile Function<ServerWebExchange, String> requestOriginParser = DEFAULT_ORIGIN_PARSER; | |||
public static /*@NonNull*/ BlockRequestHandler getBlockHandler() { | |||
return blockHandler; | |||
} | |||
public static void resetBlockHandler() { | |||
GatewayCallbackManager.blockHandler = new DefaultBlockRequestHandler(); | |||
} | |||
public static void setBlockHandler(BlockRequestHandler blockHandler) { | |||
AssertUtil.notNull(blockHandler, "blockHandler cannot be null"); | |||
GatewayCallbackManager.blockHandler = blockHandler; | |||
} | |||
public static /*@NonNull*/ Function<ServerWebExchange, String> getRequestOriginParser() { | |||
return requestOriginParser; | |||
} | |||
public static void resetRequestOriginParser() { | |||
GatewayCallbackManager.requestOriginParser = DEFAULT_ORIGIN_PARSER; | |||
} | |||
public static void setRequestOriginParser(Function<ServerWebExchange, String> requestOriginParser) { | |||
AssertUtil.notNull(requestOriginParser, "requestOriginParser cannot be null"); | |||
GatewayCallbackManager.requestOriginParser = requestOriginParser; | |||
} | |||
private GatewayCallbackManager() {} | |||
} |
@@ -0,0 +1,43 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* https://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||
import java.net.URI; | |||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||
import org.springframework.web.reactive.function.server.ServerResponse; | |||
import org.springframework.web.server.ServerWebExchange; | |||
import reactor.core.publisher.Mono; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class RedirectBlockRequestHandler implements BlockRequestHandler { | |||
private final URI uri; | |||
public RedirectBlockRequestHandler(String url) { | |||
AssertUtil.assertNotBlank(url, "url cannot be blank"); | |||
this.uri = URI.create(url); | |||
} | |||
@Override | |||
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) { | |||
return ServerResponse.temporaryRedirect(uri).build(); | |||
} | |||
} |
@@ -0,0 +1,78 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* 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 com.alibaba.csp.sentinel.adapter.gateway.sc.exception; | |||
import java.util.List; | |||
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; | |||
import com.alibaba.csp.sentinel.slots.block.BlockException; | |||
import com.alibaba.csp.sentinel.util.function.Supplier; | |||
import org.springframework.http.codec.HttpMessageWriter; | |||
import org.springframework.http.codec.ServerCodecConfigurer; | |||
import org.springframework.web.reactive.function.server.ServerResponse; | |||
import org.springframework.web.reactive.result.view.ViewResolver; | |||
import org.springframework.web.server.ServerWebExchange; | |||
import org.springframework.web.server.WebExceptionHandler; | |||
import reactor.core.publisher.Mono; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class SentinelGatewayBlockExceptionHandler implements WebExceptionHandler { | |||
private List<ViewResolver> viewResolvers; | |||
private List<HttpMessageWriter<?>> messageWriters; | |||
public SentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { | |||
this.viewResolvers = viewResolvers; | |||
this.messageWriters = serverCodecConfigurer.getWriters(); | |||
} | |||
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) { | |||
return response.writeTo(exchange, contextSupplier.get()); | |||
} | |||
@Override | |||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { | |||
if (exchange.getResponse().isCommitted()) { | |||
return Mono.error(ex); | |||
} | |||
// This exception handler only handles rejection by Sentinel. | |||
if (!BlockException.isBlockException(ex)) { | |||
return Mono.error(ex); | |||
} | |||
return handleBlockedRequest(exchange, ex) | |||
.flatMap(response -> writeResponse(response, exchange)); | |||
} | |||
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { | |||
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); | |||
} | |||
private final Supplier<ServerResponse.Context> contextSupplier = () -> new ServerResponse.Context() { | |||
@Override | |||
public List<HttpMessageWriter<?>> messageWriters() { | |||
return SentinelGatewayBlockExceptionHandler.this.messageWriters; | |||
} | |||
@Override | |||
public List<ViewResolver> viewResolvers() { | |||
return SentinelGatewayBlockExceptionHandler.this.viewResolvers; | |||
} | |||
}; | |||
} |
@@ -0,0 +1,55 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* 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 com.alibaba.csp.sentinel.adapter.gateway.sc.route; | |||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||
import org.springframework.util.AntPathMatcher; | |||
import org.springframework.util.PathMatcher; | |||
import org.springframework.web.server.ServerWebExchange; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class AntRoutePathMatcher implements Predicate<ServerWebExchange> { | |||
private final String pattern; | |||
private final PathMatcher pathMatcher; | |||
private final boolean canMatch; | |||
public AntRoutePathMatcher(String pattern) { | |||
AssertUtil.assertNotBlank(pattern, "pattern cannot be blank"); | |||
this.pattern = pattern; | |||
this.pathMatcher = new AntPathMatcher(); | |||
this.canMatch = pathMatcher.isPattern(pattern); | |||
} | |||
@Override | |||
public boolean test(ServerWebExchange exchange) { | |||
String path = exchange.getRequest().getPath().value(); | |||
if (canMatch) { | |||
return pathMatcher.match(pattern, path); | |||
} | |||
return false; | |||
} | |||
public String getPattern() { | |||
return pattern; | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* 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 com.alibaba.csp.sentinel.adapter.gateway.sc.route; | |||
import java.util.regex.Pattern; | |||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||
import org.springframework.web.server.ServerWebExchange; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public class RegexRoutePathMatcher implements Predicate<ServerWebExchange> { | |||
private final String pattern; | |||
private final Pattern regex; | |||
public RegexRoutePathMatcher(String pattern) { | |||
AssertUtil.assertNotBlank(pattern, "pattern cannot be blank"); | |||
this.pattern = pattern; | |||
this.regex = Pattern.compile(pattern); | |||
} | |||
@Override | |||
public boolean test(ServerWebExchange exchange) { | |||
String path = exchange.getRequest().getPath().value(); | |||
return regex.matcher(path).matches(); | |||
} | |||
public String getPattern() { | |||
return pattern; | |||
} | |||
} |
@@ -0,0 +1,46 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* 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 com.alibaba.csp.sentinel.adapter.gateway.sc.route; | |||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||
import org.springframework.web.server.ServerWebExchange; | |||
/** | |||
* @author Eric Zhao | |||
* @since 1.6.0 | |||
*/ | |||
public final class RouteMatchers { | |||
public static Predicate<ServerWebExchange> all() { | |||
return exchange -> true; | |||
} | |||
public static Predicate<ServerWebExchange> antPath(String pathPattern) { | |||
return new AntRoutePathMatcher(pathPattern); | |||
} | |||
public static Predicate<ServerWebExchange> exactPath(final String path) { | |||
return exchange -> exchange.getRequest().getPath().value().equals(path); | |||
} | |||
public static Predicate<ServerWebExchange> regexPath(String pathPattern) { | |||
return new RegexRoutePathMatcher(pathPattern); | |||
} | |||
private RouteMatchers() {} | |||
} |
@@ -0,0 +1 @@ | |||
com.alibaba.csp.sentinel.adapter.gateway.sc.api.SpringCloudGatewayApiDefinitionChangeObserver |
@@ -0,0 +1,91 @@ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc; | |||
import java.util.Collections; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; | |||
import org.junit.After; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.springframework.http.server.RequestPath; | |||
import org.springframework.http.server.reactive.ServerHttpRequest; | |||
import org.springframework.web.server.ServerWebExchange; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
/** | |||
* Test cases for {@link SentinelGatewayFilter}. | |||
* | |||
* @author Eric Zhao | |||
*/ | |||
public class SentinelGatewayFilterTest { | |||
@Test | |||
public void testPickMatchingApiDefinitions() { | |||
// Mock a request. | |||
ServerWebExchange exchange = mock(ServerWebExchange.class); | |||
ServerHttpRequest request = mock(ServerHttpRequest.class); | |||
when(exchange.getRequest()).thenReturn(request); | |||
RequestPath requestPath = mock(RequestPath.class); | |||
when(request.getPath()).thenReturn(requestPath); | |||
// Prepare API definitions. | |||
Set<ApiDefinition> apiDefinitions = new HashSet<>(); | |||
String apiName1 = "some_customized_api"; | |||
ApiDefinition api1 = new ApiDefinition(apiName1) | |||
.setPredicateItems(Collections.singleton( | |||
new ApiPathPredicateItem().setPattern("/product/**") | |||
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX) | |||
)); | |||
String apiName2 = "another_customized_api"; | |||
ApiDefinition api2 = new ApiDefinition(apiName2) | |||
.setPredicateItems(new HashSet<ApiPredicateItem>() {{ | |||
add(new ApiPathPredicateItem().setPattern("/something")); | |||
add(new ApiPathPredicateItem().setPattern("/other/**") | |||
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX)); | |||
}}); | |||
apiDefinitions.add(api1); | |||
apiDefinitions.add(api2); | |||
GatewayApiDefinitionManager.loadApiDefinitions(apiDefinitions); | |||
SentinelGatewayFilter filter = new SentinelGatewayFilter(); | |||
when(requestPath.value()).thenReturn("/product/123"); | |||
Set<String> matchingApis = filter.pickMatchingApiDefinitions(exchange); | |||
assertThat(matchingApis.size()).isEqualTo(1); | |||
assertThat(matchingApis.contains(apiName1)).isTrue(); | |||
when(requestPath.value()).thenReturn("/products"); | |||
assertThat(filter.pickMatchingApiDefinitions(exchange).size()).isZero(); | |||
when(requestPath.value()).thenReturn("/something"); | |||
matchingApis = filter.pickMatchingApiDefinitions(exchange); | |||
assertThat(matchingApis.size()).isEqualTo(1); | |||
assertThat(matchingApis.contains(apiName2)).isTrue(); | |||
when(requestPath.value()).thenReturn("/other/foo/3"); | |||
matchingApis = filter.pickMatchingApiDefinitions(exchange); | |||
assertThat(matchingApis.size()).isEqualTo(1); | |||
assertThat(matchingApis.contains(apiName2)).isTrue(); | |||
} | |||
@Before | |||
public void setUp() { | |||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||
GatewayRuleManager.loadRules(new HashSet<>()); | |||
} | |||
@After | |||
public void tearDown() { | |||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||
GatewayRuleManager.loadRules(new HashSet<>()); | |||
} | |||
} |
@@ -0,0 +1,209 @@ | |||
/* | |||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* https://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.alibaba.csp.sentinel.adapter.gateway.sc; | |||
import java.net.InetAddress; | |||
import java.net.InetSocketAddress; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayParamParser; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; | |||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; | |||
import com.alibaba.csp.sentinel.slots.block.RuleConstant; | |||
import org.junit.After; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.springframework.http.HttpHeaders; | |||
import org.springframework.http.server.RequestPath; | |||
import org.springframework.http.server.reactive.ServerHttpRequest; | |||
import org.springframework.util.MultiValueMap; | |||
import org.springframework.web.server.ServerWebExchange; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
/** | |||
* @author Eric Zhao | |||
*/ | |||
public class SpringCloudGatewayParamParserTest { | |||
private final GatewayParamParser<ServerWebExchange> paramParser = new GatewayParamParser<>( | |||
new ServerWebExchangeItemParser() | |||
); | |||
@Test | |||
public void testParseParametersNoParamItem() { | |||
// Mock a request. | |||
ServerWebExchange exchange = mock(ServerWebExchange.class); | |||
// Prepare gateway rules. | |||
Set<GatewayFlowRule> rules = new HashSet<>(); | |||
String routeId1 = "my_test_route_A"; | |||
rules.add(new GatewayFlowRule(routeId1) | |||
.setCount(5) | |||
.setIntervalSec(1) | |||
); | |||
GatewayRuleManager.loadRules(rules); | |||
Object[] params = paramParser.parseParameterFor(routeId1, exchange, | |||
e -> e.getResourceMode() == 0); | |||
assertThat(params.length).isZero(); | |||
} | |||
@Test | |||
public void testParseParametersWithItems() { | |||
// Mock a request. | |||
ServerWebExchange exchange = mock(ServerWebExchange.class); | |||
ServerHttpRequest request = mock(ServerHttpRequest.class); | |||
when(exchange.getRequest()).thenReturn(request); | |||
RequestPath requestPath = mock(RequestPath.class); | |||
when(request.getPath()).thenReturn(requestPath); | |||
// Prepare gateway rules. | |||
Set<GatewayFlowRule> rules = new HashSet<>(); | |||
String routeId1 = "my_test_route_A"; | |||
String api1 = "my_test_route_B"; | |||
String headerName = "X-Sentinel-Flag"; | |||
String paramName = "p"; | |||
GatewayFlowRule routeRule1 = new GatewayFlowRule(routeId1) | |||
.setCount(2) | |||
.setIntervalSec(2) | |||
.setBurst(2) | |||
.setParamItem(new GatewayParamFlowItem() | |||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP) | |||
); | |||
GatewayFlowRule routeRule2 = new GatewayFlowRule(routeId1) | |||
.setCount(10) | |||
.setIntervalSec(1) | |||
.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) | |||
.setMaxQueueingTimeoutMs(600) | |||
.setParamItem(new GatewayParamFlowItem() | |||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) | |||
.setFieldName(headerName) | |||
); | |||
GatewayFlowRule routeRule3 = new GatewayFlowRule(routeId1) | |||
.setCount(20) | |||
.setIntervalSec(1) | |||
.setBurst(5) | |||
.setParamItem(new GatewayParamFlowItem() | |||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) | |||
.setFieldName(paramName) | |||
); | |||
GatewayFlowRule routeRule4 = new GatewayFlowRule(routeId1) | |||
.setCount(120) | |||
.setIntervalSec(10) | |||
.setBurst(30) | |||
.setParamItem(new GatewayParamFlowItem() | |||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HOST) | |||
); | |||
GatewayFlowRule apiRule1 = new GatewayFlowRule(api1) | |||
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) | |||
.setCount(5) | |||
.setIntervalSec(1) | |||
.setParamItem(new GatewayParamFlowItem() | |||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) | |||
.setFieldName(paramName) | |||
); | |||
rules.add(routeRule1); | |||
rules.add(routeRule2); | |||
rules.add(routeRule3); | |||
rules.add(routeRule4); | |||
rules.add(apiRule1); | |||
GatewayRuleManager.loadRules(rules); | |||
String expectedHost = "hello.test.sentinel"; | |||
String expectedAddress = "66.77.88.99"; | |||
String expectedHeaderValue1 = "Sentinel"; | |||
String expectedUrlParamValue1 = "17"; | |||
mockClientHostAddress(request, expectedAddress); | |||
Map<String, String> expectedHeaders = new HashMap<String, String>() {{ | |||
put(headerName, expectedHeaderValue1); put("Host", expectedHost); | |||
}}; | |||
mockHeaders(request, expectedHeaders); | |||
mockSingleUrlParam(request, paramName, expectedUrlParamValue1); | |||
Object[] params = paramParser.parseParameterFor(routeId1, exchange, e -> e.getResourceMode() == 0); | |||
assertThat(params.length).isEqualTo(4); | |||
assertThat(params[routeRule1.getParamItem().getIndex()]).isEqualTo(expectedAddress); | |||
assertThat(params[routeRule2.getParamItem().getIndex()]).isEqualTo(expectedHeaderValue1); | |||
assertThat(params[routeRule3.getParamItem().getIndex()]).isEqualTo(expectedUrlParamValue1); | |||
assertThat(params[routeRule4.getParamItem().getIndex()]).isEqualTo(expectedHost); | |||
assertThat(paramParser.parseParameterFor(api1, exchange, e -> e.getResourceMode() == 0).length).isZero(); | |||
String expectedUrlParamValue2 = "fs"; | |||
mockSingleUrlParam(request, paramName, expectedUrlParamValue2); | |||
params = paramParser.parseParameterFor(api1, exchange, e -> e.getResourceMode() == 1); | |||
assertThat(params.length).isEqualTo(1); | |||
assertThat(params[apiRule1.getParamItem().getIndex()]).isEqualTo(expectedUrlParamValue2); | |||
} | |||
private void mockClientHostAddress(/*@Mock*/ ServerHttpRequest request, String address) { | |||
InetSocketAddress socketAddress = mock(InetSocketAddress.class); | |||
when(request.getRemoteAddress()).thenReturn(socketAddress); | |||
InetAddress inetAddress = mock(InetAddress.class); | |||
when(inetAddress.getHostAddress()).thenReturn(address); | |||
when(socketAddress.getAddress()).thenReturn(inetAddress); | |||
} | |||
private void mockHeaders(/*@Mock*/ ServerHttpRequest request, Map<String, String> headerMap) { | |||
HttpHeaders headers = mock(HttpHeaders.class); | |||
when(request.getHeaders()).thenReturn(headers); | |||
for (Map.Entry<String, String> e : headerMap.entrySet()) { | |||
when(headers.getFirst(e.getKey())).thenReturn(e.getValue()); | |||
} | |||
} | |||
@SuppressWarnings("unchecked") | |||
private void mockUrlParams(/*@Mock*/ ServerHttpRequest request, Map<String, String> paramMap) { | |||
MultiValueMap<String, String> urlParams = mock(MultiValueMap.class); | |||
when(request.getQueryParams()).thenReturn(urlParams); | |||
for (Map.Entry<String, String> e : paramMap.entrySet()) { | |||
when(urlParams.getFirst(e.getKey())).thenReturn(e.getValue()); | |||
} | |||
} | |||
@SuppressWarnings("unchecked") | |||
private void mockSingleUrlParam(/*@Mock*/ ServerHttpRequest request, String key, String value) { | |||
MultiValueMap<String, String> urlParams = mock(MultiValueMap.class); | |||
when(request.getQueryParams()).thenReturn(urlParams); | |||
when(urlParams.getFirst(key)).thenReturn(value); | |||
} | |||
private void mockSingleHeader(/*@Mock*/ ServerHttpRequest request, String key, String value) { | |||
HttpHeaders headers = mock(HttpHeaders.class); | |||
when(request.getHeaders()).thenReturn(headers); | |||
when(headers.getFirst(key)).thenReturn(value); | |||
} | |||
@Before | |||
public void setUp() { | |||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||
GatewayRuleManager.loadRules(new HashSet<>()); | |||
} | |||
@After | |||
public void tearDown() { | |||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||
GatewayRuleManager.loadRules(new HashSet<>()); | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
mock-maker-inline |