[Feature 3.3] Triple Rest Cors Support (#14073)
* feat(): add cors key * feat(): add base cors class * feat(): add cors class in rpc-triple and rest-spring * feat(): add cors class in rpc-triple and rest-spring * test(): add rpc-triple cors test * fix(): fix CorsUtil bug * fix(): fix CorsUtil bug * fix(): fix objectUtil bug * fix(): fix corsmeta set bug * fix(): fix config load fail bug * fix(): option method can not be look fail * fix(): CorsMeta method will null * fix(): request-header not set will fail * refactor(): improve CorsMeta CorsProcess some code * fix(): coreMeta combine priority * test(): remove rest-spring cors test to sample * docs(): add docs * revert(): test version * fix(): getCorsMeta can be null * fix(): combine can be null * fix(): save option and vary bug * fix(): pom version * fix(): spring version will cause allowPrivateWork resolve error * fix(): ci * refactor(): delete useless code * refactor(): accept some sonarcloud issue * refactor(): add @Nullable to point the CorsMeta Attributes * refactor(): style * fix(): fix prelight logic * fix(): remove credential & privateNetWork report * refactor(): Move globalMetaMerge in RequestMappingResolver * refactor(): use array replace corsConfig string * refactor(): move CorsProcessor to CorsHeaderFilterAdapter * fix(): fix unit test * fix(): fix test failure * fix(): delete useless param * fix(): fix sonarcloud * fix(): fix wrong class place & naming * fix(): fix wrong static global corsMeta * fix(): refactor CorsUtil from sonar issue * feat(rest): refine cors support * feat(rest): refine cors support * feat(rest): refine cors support bugfix * fix(): getBoolean will throw exception when null * fix(rest-spring): fix crossOrigin allowCredentials is string * fix(): fix globalCorsMeta load null * fix(): fix vary header bug * fix(): fix unit test && Fix cors specification * fix(): fix pom * fix(): fix combine bug * fix(): fix some sonar issue * fix(): fix style * feat(rest): refine cors support * fix(): fix style * fix(): fix needed sonar issue * refactor(): refactor CorsMeta.combine() and add comment * fix(): Replenish license * fix(): update test * test(): Refactor the test class and add credential test cases * test(): Refactor the test class and add credential test cases * fix(rest): revert api HeaderFilter * fix(): accept sonar issue --------- Co-authored-by: Sean Yang <oxsean@gmail.com> Co-authored-by: earthchen <earthchen1996@gmail.com>
This commit is contained in:
parent
8368262406
commit
1f6d441de6
|
|
@ -79,6 +79,7 @@ header:
|
|||
- 'dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/EmbeddedZooKeeper.java'
|
||||
- 'dubbo-test/dubbo-test-common/src/main/java/org/apache/dubbo/test/common/utils/TestSocketUtils.java'
|
||||
- 'dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java'
|
||||
- 'dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilter.java'
|
||||
- 'dubbo-common/src/main/java/org/apache/dubbo/common/threadpool/serial/SerializingExecutor.java'
|
||||
- 'dubbo-maven-plugin/src/main/java/org/apache/dubbo/maven/plugin/aot/AbstractAotMojo.java'
|
||||
- 'dubbo-maven-plugin/src/main/java/org/apache/dubbo/maven/plugin/aot/AbstractDependencyFilterMojo.java'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.apache.dubbo.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class CorsConfig implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* A list of origins for which cross-origin requests are allowed. Values may be a specific domain, e.g.
|
||||
* {@code "https://domain1.com"}, or the CORS defined special value {@code "*"} for all origins.
|
||||
* <p>By default this is not set which means that no origins are allowed.
|
||||
* However, an instance of this class is often initialized further, e.g. for {@code @CrossOrigin}, via
|
||||
* {@code org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta.Builder#applyDefault()}.
|
||||
*/
|
||||
private String[] allowedOrigins;
|
||||
|
||||
/**
|
||||
* Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"},
|
||||
* {@code "PUT"}, etc. The special value {@code "*"} allows all methods.
|
||||
* <p>If not set, only {@code "GET"} and {@code "HEAD"} are allowed.
|
||||
* <p>By default this is not set.
|
||||
*/
|
||||
private String[] allowedMethods;
|
||||
|
||||
/**
|
||||
* /**
|
||||
* Set the list of headers that a pre-flight request can list as allowed
|
||||
* for use during an actual request. The special value {@code "*"} allows
|
||||
* actual requests to send any header.
|
||||
* <p>By default this is not set.
|
||||
*/
|
||||
private String[] allowedHeaders;
|
||||
|
||||
/**
|
||||
* Set the list of response headers that an actual response might have
|
||||
* and can be exposed to the client. The special value {@code "*"}
|
||||
* allows all headers to be exposed.
|
||||
* <p>By default this is not set.
|
||||
*/
|
||||
private String[] exposedHeaders;
|
||||
|
||||
/**
|
||||
* Whether user credentials are supported.
|
||||
* <p>By default this is not set (i.e. user credentials are not supported).
|
||||
*/
|
||||
private Boolean allowCredentials;
|
||||
|
||||
/**
|
||||
* Configure how long, as a duration, the response from a pre-flight request
|
||||
* can be cached by clients.
|
||||
*/
|
||||
private Long maxAge;
|
||||
|
||||
public String[] getAllowedOrigins() {
|
||||
return allowedOrigins;
|
||||
}
|
||||
|
||||
public void setAllowedOrigins(String[] allowedOrigins) {
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
}
|
||||
|
||||
public String[] getAllowedMethods() {
|
||||
return allowedMethods;
|
||||
}
|
||||
|
||||
public void setAllowedMethods(String[] allowedMethods) {
|
||||
this.allowedMethods = allowedMethods;
|
||||
}
|
||||
|
||||
public String[] getAllowedHeaders() {
|
||||
return allowedHeaders;
|
||||
}
|
||||
|
||||
public void setAllowedHeaders(String[] allowedHeaders) {
|
||||
this.allowedHeaders = allowedHeaders;
|
||||
}
|
||||
|
||||
public String[] getExposedHeaders() {
|
||||
return exposedHeaders;
|
||||
}
|
||||
|
||||
public void setExposedHeaders(String[] exposedHeaders) {
|
||||
this.exposedHeaders = exposedHeaders;
|
||||
}
|
||||
|
||||
public Boolean getAllowCredentials() {
|
||||
return allowCredentials;
|
||||
}
|
||||
|
||||
public void setAllowCredentials(Boolean allowCredentials) {
|
||||
this.allowCredentials = allowCredentials;
|
||||
}
|
||||
|
||||
public Long getMaxAge() {
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
public void setMaxAge(Long maxAge) {
|
||||
this.maxAge = maxAge;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.apache.dubbo.config;
|
||||
|
||||
import org.apache.dubbo.config.support.Nested;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
|
|
@ -68,6 +70,12 @@ public class RestConfig implements Serializable {
|
|||
*/
|
||||
private String formatParameterName;
|
||||
|
||||
/**
|
||||
* The config is used to set the Global CORS configuration properties.
|
||||
*/
|
||||
@Nested
|
||||
private CorsConfig cors;
|
||||
|
||||
public Integer getMaxBodySize() {
|
||||
return maxBodySize;
|
||||
}
|
||||
|
|
@ -115,4 +123,12 @@ public class RestConfig implements Serializable {
|
|||
public void setFormatParameterName(String formatParameterName) {
|
||||
this.formatParameterName = formatParameterName;
|
||||
}
|
||||
|
||||
public CorsConfig getCors() {
|
||||
return cors;
|
||||
}
|
||||
|
||||
public void setCors(CorsConfig cors) {
|
||||
this.cors = cors;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ package org.apache.dubbo.rpc.protocol.tri.rest.support.jaxrs;
|
|||
|
||||
import org.apache.dubbo.common.extension.Activate;
|
||||
import org.apache.dubbo.rpc.model.FrameworkModel;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.cors.CorsUtils;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping.Builder;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMappingResolver;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.condition.ServiceVersionCondition;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationSupport;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ServiceMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.util.RestToolKit;
|
||||
|
|
@ -31,9 +33,12 @@ import org.apache.dubbo.rpc.protocol.tri.rest.util.RestToolKit;
|
|||
@Activate(onClass = "javax.ws.rs.Path")
|
||||
public class JaxrsRequestMappingResolver implements RequestMappingResolver {
|
||||
|
||||
private final FrameworkModel frameworkModel;
|
||||
private final RestToolKit toolKit;
|
||||
private CorsMeta globalCorsMeta;
|
||||
|
||||
public JaxrsRequestMappingResolver(FrameworkModel frameworkModel) {
|
||||
this.frameworkModel = frameworkModel;
|
||||
toolKit = new JaxrsRestToolKit(frameworkModel);
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +70,14 @@ public class JaxrsRequestMappingResolver implements RequestMappingResolver {
|
|||
return null;
|
||||
}
|
||||
ServiceMeta serviceMeta = methodMeta.getServiceMeta();
|
||||
if (globalCorsMeta == null) {
|
||||
globalCorsMeta = CorsUtils.getGlobalCorsMeta(frameworkModel);
|
||||
}
|
||||
return builder(methodMeta, path, httpMethod)
|
||||
.name(methodMeta.getMethod().getName())
|
||||
.contextPath(methodMeta.getServiceMeta().getContextPath())
|
||||
.custom(new ServiceVersionCondition(serviceMeta.getServiceGroup(), serviceMeta.getServiceVersion()))
|
||||
.cors(globalCorsMeta)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,17 @@
|
|||
*/
|
||||
package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
|
||||
|
||||
import org.apache.dubbo.common.constants.CommonConstants;
|
||||
import org.apache.dubbo.common.extension.Activate;
|
||||
import org.apache.dubbo.common.utils.StringUtils;
|
||||
import org.apache.dubbo.rpc.model.FrameworkModel;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.cors.CorsUtils;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping.Builder;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMappingResolver;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.condition.ServiceVersionCondition;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ServiceMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.util.RestToolKit;
|
||||
|
|
@ -35,6 +38,7 @@ public class SpringMvcRequestMappingResolver implements RequestMappingResolver {
|
|||
|
||||
private final FrameworkModel frameworkModel;
|
||||
private volatile RestToolKit toolKit;
|
||||
private CorsMeta globalCorsMeta;
|
||||
|
||||
public SpringMvcRequestMappingResolver(FrameworkModel frameworkModel) {
|
||||
this.frameworkModel = frameworkModel;
|
||||
|
|
@ -62,9 +66,13 @@ public class SpringMvcRequestMappingResolver implements RequestMappingResolver {
|
|||
return null;
|
||||
}
|
||||
AnnotationMeta<?> responseStatus = serviceMeta.findMergedAnnotation(Annotations.ResponseStatus);
|
||||
AnnotationMeta<?> crossOrigin = serviceMeta.findMergedAnnotation(Annotations.CrossOrigin);
|
||||
String[] methods = requestMapping.getStringArray("method");
|
||||
return builder(requestMapping, responseStatus)
|
||||
.method(methods)
|
||||
.name(serviceMeta.getType().getSimpleName())
|
||||
.contextPath(serviceMeta.getContextPath())
|
||||
.cors(buildCorsMeta(crossOrigin, methods))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
@ -80,10 +88,14 @@ public class SpringMvcRequestMappingResolver implements RequestMappingResolver {
|
|||
}
|
||||
ServiceMeta serviceMeta = methodMeta.getServiceMeta();
|
||||
AnnotationMeta<?> responseStatus = methodMeta.findMergedAnnotation(Annotations.ResponseStatus);
|
||||
AnnotationMeta<?> crossOrigin = methodMeta.findMergedAnnotation(Annotations.CrossOrigin);
|
||||
String[] methods = requestMapping.getStringArray("method");
|
||||
return builder(requestMapping, responseStatus)
|
||||
.method(methods)
|
||||
.name(methodMeta.getMethod().getName())
|
||||
.contextPath(serviceMeta.getContextPath())
|
||||
.custom(new ServiceVersionCondition(serviceMeta.getServiceGroup(), serviceMeta.getServiceVersion()))
|
||||
.cors(buildCorsMeta(crossOrigin, methods))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
@ -98,10 +110,34 @@ public class SpringMvcRequestMappingResolver implements RequestMappingResolver {
|
|||
}
|
||||
}
|
||||
return builder.path(requestMapping.getValueArray())
|
||||
.method(requestMapping.getStringArray("method"))
|
||||
.param(requestMapping.getStringArray("params"))
|
||||
.header(requestMapping.getStringArray("headers"))
|
||||
.consume(requestMapping.getStringArray("consumes"))
|
||||
.produce(requestMapping.getStringArray("produces"));
|
||||
}
|
||||
|
||||
private CorsMeta buildCorsMeta(AnnotationMeta<?> crossOrigin, String[] methods) {
|
||||
if (globalCorsMeta == null) {
|
||||
globalCorsMeta = CorsUtils.getGlobalCorsMeta(frameworkModel);
|
||||
}
|
||||
if (crossOrigin == null) {
|
||||
return globalCorsMeta;
|
||||
}
|
||||
String[] allowedMethods = crossOrigin.getStringArray("methods");
|
||||
if (allowedMethods.length == 0) {
|
||||
allowedMethods = methods;
|
||||
if (allowedMethods.length == 0) {
|
||||
allowedMethods = new String[] {CommonConstants.ANY_VALUE};
|
||||
}
|
||||
}
|
||||
CorsMeta corsMeta = CorsMeta.builder()
|
||||
.allowedOrigins(crossOrigin.getStringArray("origins"))
|
||||
.allowedMethods(allowedMethods)
|
||||
.allowedHeaders(crossOrigin.getStringArray("allowedHeaders"))
|
||||
.exposedHeaders(crossOrigin.getStringArray("exposedHeaders"))
|
||||
.allowCredentials(crossOrigin.getString("allowCredentials"))
|
||||
.maxAge(crossOrigin.getNumber("maxAge"))
|
||||
.build();
|
||||
return globalCorsMeta.combine(corsMeta);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@CrossOrigin
|
||||
public class SpringDemoServiceImpl implements SpringRestDemoService {
|
||||
private static Map<String, Object> context;
|
||||
private boolean called;
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ public abstract class AbstractServerHttpChannelObserver implements CustomizableH
|
|||
public final void onError(Throwable throwable) {
|
||||
if (throwable instanceof HttpResultPayloadException) {
|
||||
onNext(((HttpResultPayloadException) throwable).getResult());
|
||||
doOnCompleted(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public enum HttpStatus {
|
|||
OK(200),
|
||||
CREATED(201),
|
||||
ACCEPTED(202),
|
||||
NO_CONTENT(204),
|
||||
FOUND(302),
|
||||
BAD_REQUEST(400),
|
||||
UNAUTHORIZED(401),
|
||||
|
|
|
|||
|
|
@ -162,10 +162,10 @@ public abstract class AbstractServerTransportListener<HEADER extends RequestMeta
|
|||
protected void logError(Throwable t) {
|
||||
if (t instanceof HttpStatusException) {
|
||||
HttpStatusException e = (HttpStatusException) t;
|
||||
if (e.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR.getCode()) {
|
||||
if (e.getStatusCode() >= HttpStatus.BAD_REQUEST.getCode()) {
|
||||
LOGGER.debug("http status exception", e);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOGGER.error(INTERNAL_ERROR, "", "", "server internal error", t);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,5 +57,14 @@ public final class RestConstants {
|
|||
public static final String CASE_SENSITIVE_MATCH_KEY = CONFIG_PREFIX + "case-sensitive-match";
|
||||
public static final String FORMAT_PARAMETER_NAME_KEY = CONFIG_PREFIX + "format-parameter-name";
|
||||
|
||||
/* Cors Configuration Key */
|
||||
public static final String CORS_CONFIG_PREFIX = CONFIG_PREFIX + "cors.";
|
||||
public static final String ALLOWED_ORIGINS = CORS_CONFIG_PREFIX + "allowed-origins";
|
||||
public static final String ALLOWED_METHODS = CORS_CONFIG_PREFIX + "allowed-methods";
|
||||
public static final String ALLOWED_HEADERS = CORS_CONFIG_PREFIX + "allowed-headers";
|
||||
public static final String ALLOW_CREDENTIALS = CORS_CONFIG_PREFIX + "allow-credentials";
|
||||
public static final String EXPOSED_HEADERS = CORS_CONFIG_PREFIX + "exposed-headers";
|
||||
public static final String MAX_AGE = CORS_CONFIG_PREFIX + "max-age";
|
||||
|
||||
private RestConstants() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.dubbo.rpc.protocol.tri.rest.cors;
|
||||
|
||||
import org.apache.dubbo.common.constants.CommonConstants;
|
||||
import org.apache.dubbo.common.extension.Activate;
|
||||
import org.apache.dubbo.common.utils.ArrayUtils;
|
||||
import org.apache.dubbo.common.utils.StringUtils;
|
||||
import org.apache.dubbo.remoting.http12.HttpMethods;
|
||||
import org.apache.dubbo.remoting.http12.HttpRequest;
|
||||
import org.apache.dubbo.remoting.http12.HttpResponse;
|
||||
import org.apache.dubbo.remoting.http12.HttpResult;
|
||||
import org.apache.dubbo.remoting.http12.HttpStatus;
|
||||
import org.apache.dubbo.remoting.http12.exception.HttpResultPayloadException;
|
||||
import org.apache.dubbo.rpc.Invoker;
|
||||
import org.apache.dubbo.rpc.RpcException;
|
||||
import org.apache.dubbo.rpc.RpcInvocation;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestHeaderFilterAdapter;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.ANY_VALUE;
|
||||
|
||||
/**
|
||||
* See: <a href="https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java">DefaultCorsProcessor</a>
|
||||
*/
|
||||
@Activate(group = CommonConstants.PROVIDER, order = 1000)
|
||||
public class CorsHeaderFilter extends RestHeaderFilterAdapter {
|
||||
|
||||
public static final String VARY = "Vary";
|
||||
public static final String ORIGIN = "Origin";
|
||||
public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
|
||||
public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
|
||||
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
|
||||
public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
|
||||
public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
|
||||
public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
|
||||
public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
|
||||
public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
|
||||
public static final String SEP = ", ";
|
||||
|
||||
@Override
|
||||
protected void invoke(Invoker<?> invoker, RpcInvocation invocation, HttpRequest request, HttpResponse response)
|
||||
throws RpcException {
|
||||
RequestMapping mapping = request.attribute(RestConstants.MAPPING_ATTRIBUTE);
|
||||
CorsMeta cors = mapping.getCors();
|
||||
String origin = request.header(ORIGIN);
|
||||
if (cors == null) {
|
||||
if (isPreFlightRequest(request, origin)) {
|
||||
throw new HttpResultPayloadException(HttpResult.builder()
|
||||
.status(HttpStatus.FORBIDDEN)
|
||||
.body("Invalid CORS request")
|
||||
.build());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process(cors, request, response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new HttpResultPayloadException(HttpResult.builder()
|
||||
.status(HttpStatus.FORBIDDEN)
|
||||
.body("Invalid CORS request")
|
||||
.headers(response.headers())
|
||||
.build());
|
||||
}
|
||||
|
||||
private boolean process(CorsMeta cors, HttpRequest request, HttpResponse response) {
|
||||
setVaryHeader(response);
|
||||
|
||||
String origin = request.header(ORIGIN);
|
||||
if (isNotCorsRequest(request, origin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response.header(ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String allowOrigin = checkOrigin(cors, origin);
|
||||
if (allowOrigin == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean preFlight = isPreFlightRequest(request, origin);
|
||||
|
||||
List<String> allowMethods =
|
||||
checkMethods(cors, preFlight ? request.header(ACCESS_CONTROL_REQUEST_METHOD) : request.method());
|
||||
if (allowMethods == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> allowHeaders = null;
|
||||
if (preFlight) {
|
||||
allowHeaders = checkHeaders(cors, request.headerValues(ACCESS_CONTROL_REQUEST_HEADERS));
|
||||
if (allowHeaders == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigin);
|
||||
|
||||
if (ArrayUtils.isNotEmpty(cors.getExposedHeaders())) {
|
||||
response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, StringUtils.join(cors.getExposedHeaders(), SEP));
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(cors.getAllowCredentials())) {
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString());
|
||||
}
|
||||
|
||||
if (preFlight) {
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.join(allowMethods, SEP));
|
||||
|
||||
if (!allowHeaders.isEmpty()) {
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, StringUtils.join(allowHeaders, SEP));
|
||||
}
|
||||
if (cors.getMaxAge() != null) {
|
||||
response.setHeader(ACCESS_CONTROL_MAX_AGE, cors.getMaxAge().toString());
|
||||
}
|
||||
throw new HttpResultPayloadException(HttpResult.builder()
|
||||
.status(HttpStatus.NO_CONTENT)
|
||||
.headers(response.headers())
|
||||
.build());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void setVaryHeader(HttpResponse response) {
|
||||
List<String> varyHeaders = response.headerValues(VARY);
|
||||
String varyValue;
|
||||
if (varyHeaders == null) {
|
||||
varyValue = ORIGIN + SEP + ACCESS_CONTROL_REQUEST_METHOD + SEP + ACCESS_CONTROL_REQUEST_HEADERS;
|
||||
} else {
|
||||
Set<String> varHeadersSet = new LinkedHashSet<>(varyHeaders);
|
||||
varHeadersSet.add(ORIGIN);
|
||||
varHeadersSet.add(ACCESS_CONTROL_REQUEST_METHOD);
|
||||
varHeadersSet.add(ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
varyValue = StringUtils.join(varHeadersSet, SEP);
|
||||
}
|
||||
response.setHeader(VARY, varyValue);
|
||||
}
|
||||
|
||||
private static String checkOrigin(CorsMeta cors, String origin) {
|
||||
if (StringUtils.isBlank(origin)) {
|
||||
return null;
|
||||
}
|
||||
origin = CorsUtils.formatOrigin(origin);
|
||||
String[] allowedOrigins = cors.getAllowedOrigins();
|
||||
if (ArrayUtils.isNotEmpty(allowedOrigins)) {
|
||||
if (ArrayUtils.contains(allowedOrigins, ANY_VALUE)) {
|
||||
if (Boolean.TRUE.equals(cors.getAllowCredentials())) {
|
||||
throw new IllegalArgumentException(
|
||||
"When allowCredentials is true, allowedOrigins cannot contain the special value \"*\"");
|
||||
}
|
||||
return ANY_VALUE;
|
||||
}
|
||||
for (String allowedOrigin : allowedOrigins) {
|
||||
if (origin.equalsIgnoreCase(allowedOrigin)) {
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ArrayUtils.isNotEmpty(cors.getAllowedOriginsPatterns())) {
|
||||
for (Pattern pattern : cors.getAllowedOriginsPatterns()) {
|
||||
if (pattern.matcher(origin).matches()) {
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<String> checkMethods(CorsMeta cors, String method) {
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
String[] allowedMethods = cors.getAllowedMethods();
|
||||
if (ArrayUtils.contains(allowedMethods, ANY_VALUE)) {
|
||||
return Collections.singletonList(method);
|
||||
}
|
||||
for (String allowedMethod : allowedMethods) {
|
||||
if (method.equalsIgnoreCase(allowedMethod)) {
|
||||
return Arrays.asList(allowedMethods);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<String> checkHeaders(CorsMeta cors, Collection<String> headers) {
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String[] allowedHeaders = cors.getAllowedHeaders();
|
||||
if (ArrayUtils.isEmpty(allowedHeaders)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean allowAny = ArrayUtils.contains(allowedHeaders, ANY_VALUE);
|
||||
List<String> result = new ArrayList<>(headers.size());
|
||||
for (String header : headers) {
|
||||
if (allowAny) {
|
||||
result.add(header);
|
||||
continue;
|
||||
}
|
||||
for (String allowedHeader : allowedHeaders) {
|
||||
if (header.equalsIgnoreCase(allowedHeader)) {
|
||||
result.add(header);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.isEmpty() ? null : result;
|
||||
}
|
||||
|
||||
private static boolean isNotCorsRequest(HttpRequest request, String origin) {
|
||||
if (origin == null) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(origin);
|
||||
return request.scheme().equals(uri.getScheme())
|
||||
&& request.serverName().equals(uri.getHost())
|
||||
&& getPort(request.scheme(), request.serverPort()) == getPort(uri.getScheme(), uri.getPort());
|
||||
} catch (URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPreFlightRequest(HttpRequest request, String origin) {
|
||||
return request.method().equals(HttpMethods.OPTIONS.name())
|
||||
&& origin != null
|
||||
&& request.hasHeader(ACCESS_CONTROL_REQUEST_METHOD);
|
||||
}
|
||||
|
||||
private static int getPort(String scheme, int port) {
|
||||
if (port == -1) {
|
||||
if ("http".equals(scheme)) {
|
||||
return 80;
|
||||
}
|
||||
if ("https".equals(scheme)) {
|
||||
return 443;
|
||||
}
|
||||
}
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.apache.dubbo.rpc.protocol.tri.rest.cors;
|
||||
|
||||
import org.apache.dubbo.common.config.Configuration;
|
||||
import org.apache.dubbo.common.config.ConfigurationUtils;
|
||||
import org.apache.dubbo.common.utils.StringUtils;
|
||||
import org.apache.dubbo.rpc.model.FrameworkModel;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta;
|
||||
|
||||
public class CorsUtils {
|
||||
|
||||
private CorsUtils() {}
|
||||
|
||||
public static CorsMeta getGlobalCorsMeta(FrameworkModel frameworkModel) {
|
||||
Configuration config = ConfigurationUtils.getGlobalConfiguration(frameworkModel.defaultApplication());
|
||||
|
||||
String maxAge = config.getString(RestConstants.MAX_AGE);
|
||||
return CorsMeta.builder()
|
||||
.allowedOrigins(getValues(config, RestConstants.ALLOWED_ORIGINS))
|
||||
.allowedMethods(getValues(config, RestConstants.ALLOWED_METHODS))
|
||||
.allowedHeaders(getValues(config, RestConstants.ALLOWED_HEADERS))
|
||||
.allowCredentials(config.getString(RestConstants.ALLOW_CREDENTIALS))
|
||||
.exposedHeaders(getValues(config, RestConstants.EXPOSED_HEADERS))
|
||||
.maxAge(maxAge == null ? null : Long.valueOf(maxAge))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String[] getValues(Configuration config, String key) {
|
||||
return StringUtils.tokenize(config.getString(key), ',');
|
||||
}
|
||||
|
||||
public static String formatOrigin(String value) {
|
||||
value = value.trim();
|
||||
int last = value.length() - 1;
|
||||
return last > -1 && value.charAt(last) == '/' ? value.substring(0, last) : value;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ package org.apache.dubbo.rpc.protocol.tri.rest.filter;
|
|||
import org.apache.dubbo.remoting.http12.HttpRequest;
|
||||
import org.apache.dubbo.remoting.http12.HttpResponse;
|
||||
import org.apache.dubbo.rpc.HeaderFilter;
|
||||
import org.apache.dubbo.rpc.Invocation;
|
||||
import org.apache.dubbo.rpc.Invoker;
|
||||
import org.apache.dubbo.rpc.RpcException;
|
||||
import org.apache.dubbo.rpc.RpcInvocation;
|
||||
|
|
@ -32,11 +31,12 @@ public abstract class RestHeaderFilterAdapter implements HeaderFilter {
|
|||
if (TripleConstant.TRIPLE_HANDLER_TYPE_REST.equals(invocation.get(TripleConstant.HANDLER_TYPE_KEY))) {
|
||||
HttpRequest request = (HttpRequest) invocation.get(TripleConstant.HTTP_REQUEST_KEY);
|
||||
HttpResponse response = (HttpResponse) invocation.get(TripleConstant.HTTP_RESPONSE_KEY);
|
||||
return invoke(invoker, invocation, request, response);
|
||||
invoke(invoker, invocation, request, response);
|
||||
}
|
||||
return invocation;
|
||||
}
|
||||
|
||||
protected abstract RpcInvocation invoke(
|
||||
Invoker<?> invoker, Invocation invocation, HttpRequest request, HttpResponse response) throws RpcException;
|
||||
protected abstract void invoke(
|
||||
Invoker<?> invoker, RpcInvocation invocation, HttpRequest request, HttpResponse response)
|
||||
throws RpcException;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import org.apache.dubbo.rpc.protocol.tri.rest.mapping.condition.ParamsCondition;
|
|||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.condition.PathCondition;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.condition.PathExpression;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.condition.ProducesCondition;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ResponseMeta;
|
||||
|
||||
import java.util.Objects;
|
||||
|
|
@ -43,6 +44,7 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
private final ConsumesCondition consumesCondition;
|
||||
private final ProducesCondition producesCondition;
|
||||
private final ConditionWrapper customCondition;
|
||||
private final CorsMeta cors;
|
||||
private final ResponseMeta response;
|
||||
|
||||
private int hashCode;
|
||||
|
|
@ -56,6 +58,7 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
ConsumesCondition consumesCondition,
|
||||
ProducesCondition producesCondition,
|
||||
Condition<?, HttpRequest> customCondition,
|
||||
CorsMeta cors,
|
||||
ResponseMeta response) {
|
||||
this.name = name;
|
||||
this.pathCondition = pathCondition;
|
||||
|
|
@ -65,6 +68,7 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
this.consumesCondition = consumesCondition;
|
||||
this.producesCondition = producesCondition;
|
||||
this.customCondition = customCondition == null ? null : new ConditionWrapper(customCondition);
|
||||
this.cors = cors;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
|
|
@ -82,12 +86,20 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
ConsumesCondition consumes = combine(consumesCondition, other.consumesCondition);
|
||||
ProducesCondition produces = combine(producesCondition, other.producesCondition);
|
||||
ConditionWrapper custom = combine(customCondition, other.customCondition);
|
||||
CorsMeta corsMeta = combine(this.cors, other.cors);
|
||||
ResponseMeta response = ResponseMeta.combine(this.response, other.response);
|
||||
return new RequestMapping(name, paths, methods, params, headers, consumes, produces, custom, response);
|
||||
return new RequestMapping(
|
||||
name, paths, methods, params, headers, consumes, produces, custom, corsMeta, response);
|
||||
}
|
||||
|
||||
private <T extends Condition<T, HttpRequest>> T combine(T value, T other) {
|
||||
return value == null ? other : other == null ? value : value.combine(other);
|
||||
private <T extends Condition<T, HttpRequest>> T combine(T source, T other) {
|
||||
return source == null ? other : other == null ? source : source.combine(other);
|
||||
}
|
||||
|
||||
private CorsMeta combine(CorsMeta source, CorsMeta other) {
|
||||
return source == null || source.isEmpty()
|
||||
? other == null || other.isEmpty() ? null : other.applyDefault()
|
||||
: source.combine(other).applyDefault();
|
||||
}
|
||||
|
||||
public RequestMapping match(HttpRequest request, PathExpression path) {
|
||||
|
|
@ -156,7 +168,7 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
}
|
||||
}
|
||||
|
||||
return new RequestMapping(name, paths, methods, params, headers, consumes, produces, custom, response);
|
||||
return new RequestMapping(name, paths, methods, params, headers, consumes, produces, custom, cors, response);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
|
@ -171,6 +183,10 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
return producesCondition;
|
||||
}
|
||||
|
||||
public CorsMeta getCors() {
|
||||
return cors;
|
||||
}
|
||||
|
||||
public ResponseMeta getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
|
@ -290,6 +306,9 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
if (response != null) {
|
||||
sb.append(", response=").append(response);
|
||||
}
|
||||
if (cors != null) {
|
||||
sb.append(", cors=").append(cors);
|
||||
}
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
|
@ -304,6 +323,7 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
private String[] consumes;
|
||||
private String[] produces;
|
||||
private Condition<?, HttpRequest> customCondition;
|
||||
private CorsMeta corsMeta;
|
||||
private Integer responseStatus;
|
||||
private String responseReason;
|
||||
|
||||
|
|
@ -352,6 +372,11 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder cors(CorsMeta corsMeta) {
|
||||
this.corsMeta = corsMeta;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder responseStatus(int status) {
|
||||
responseStatus = status;
|
||||
return this;
|
||||
|
|
@ -363,23 +388,17 @@ public final class RequestMapping implements Condition<RequestMapping, HttpReque
|
|||
}
|
||||
|
||||
public RequestMapping build() {
|
||||
PathCondition pathCondition = isEmpty(paths) ? null : new PathCondition(contextPath, paths);
|
||||
MethodsCondition methodsCondition = isEmpty(methods) ? null : new MethodsCondition(methods);
|
||||
ParamsCondition paramsCondition = isEmpty(params) ? null : new ParamsCondition(params);
|
||||
HeadersCondition headersCondition = isEmpty(headers) ? null : new HeadersCondition(headers);
|
||||
ConsumesCondition consumesCondition = isEmpty(consumes) ? null : new ConsumesCondition(consumes);
|
||||
ProducesCondition producesCondition = isEmpty(produces) ? null : new ProducesCondition(produces);
|
||||
ResponseMeta response = responseStatus == null ? null : new ResponseMeta(responseStatus, responseReason);
|
||||
return new RequestMapping(
|
||||
name,
|
||||
pathCondition,
|
||||
methodsCondition,
|
||||
paramsCondition,
|
||||
headersCondition,
|
||||
consumesCondition,
|
||||
producesCondition,
|
||||
isEmpty(paths) ? null : new PathCondition(contextPath, paths),
|
||||
isEmpty(methods) ? null : new MethodsCondition(methods),
|
||||
isEmpty(params) ? null : new ParamsCondition(params),
|
||||
isEmpty(headers) ? null : new HeadersCondition(headers),
|
||||
isEmpty(consumes) ? null : new ConsumesCondition(consumes),
|
||||
isEmpty(produces) ? null : new ProducesCondition(produces),
|
||||
customCondition,
|
||||
response);
|
||||
corsMeta == null ? null : corsMeta,
|
||||
responseStatus == null ? null : new ResponseMeta(responseStatus, responseReason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import java.util.Set;
|
|||
|
||||
import static org.apache.dubbo.remoting.http12.HttpMethods.GET;
|
||||
import static org.apache.dubbo.remoting.http12.HttpMethods.HEAD;
|
||||
import static org.apache.dubbo.remoting.http12.HttpMethods.OPTIONS;
|
||||
|
||||
public final class MethodsCondition implements Condition<MethodsCondition, HttpRequest> {
|
||||
|
||||
|
|
@ -53,6 +54,12 @@ public final class MethodsCondition implements Condition<MethodsCondition, HttpR
|
|||
if (HEAD.name().equals(method) && methods.contains(GET.name())) {
|
||||
return new MethodsCondition(GET.name());
|
||||
}
|
||||
if (OPTIONS.name().equals(method)
|
||||
&& request.hasHeader("origin")
|
||||
&& request.hasHeader("access-control-request-method")) {
|
||||
return new MethodsCondition(OPTIONS.name());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.apache.dubbo.rpc.protocol.tri.rest.mapping.meta;
|
||||
|
||||
import org.apache.dubbo.common.utils.CollectionUtils;
|
||||
import org.apache.dubbo.common.utils.StringUtils;
|
||||
import org.apache.dubbo.remoting.http12.HttpMethods;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.cors.CorsUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.ANY_VALUE;
|
||||
import static org.apache.dubbo.common.utils.StringUtils.EMPTY_STRING_ARRAY;
|
||||
|
||||
public class CorsMeta {
|
||||
|
||||
private final String[] allowedOrigins;
|
||||
private final Pattern[] allowedOriginsPatterns;
|
||||
private final String[] allowedMethods;
|
||||
private final String[] allowedHeaders;
|
||||
private final String[] exposedHeaders;
|
||||
private final Boolean allowCredentials;
|
||||
private final Long maxAge;
|
||||
|
||||
private CorsMeta(
|
||||
String[] allowedOrigins,
|
||||
Pattern[] allowedOriginsPatterns,
|
||||
String[] allowedMethods,
|
||||
String[] allowedHeaders,
|
||||
String[] exposedHeaders,
|
||||
Boolean allowCredentials,
|
||||
Long maxAge) {
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
this.allowedOriginsPatterns = allowedOriginsPatterns;
|
||||
this.allowedMethods = allowedMethods;
|
||||
this.allowedHeaders = allowedHeaders;
|
||||
this.exposedHeaders = exposedHeaders;
|
||||
this.allowCredentials = allowCredentials;
|
||||
this.maxAge = maxAge;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public String[] getAllowedOrigins() {
|
||||
return allowedOrigins;
|
||||
}
|
||||
|
||||
public Pattern[] getAllowedOriginsPatterns() {
|
||||
return allowedOriginsPatterns;
|
||||
}
|
||||
|
||||
public String[] getAllowedMethods() {
|
||||
return allowedMethods;
|
||||
}
|
||||
|
||||
public String[] getAllowedHeaders() {
|
||||
return allowedHeaders;
|
||||
}
|
||||
|
||||
public String[] getExposedHeaders() {
|
||||
return exposedHeaders;
|
||||
}
|
||||
|
||||
public Boolean getAllowCredentials() {
|
||||
return allowCredentials;
|
||||
}
|
||||
|
||||
public Long getMaxAge() {
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return allowedOrigins.length == 0
|
||||
&& allowedMethods.length == 0
|
||||
&& allowedHeaders.length == 0
|
||||
&& exposedHeaders.length == 0
|
||||
&& allowCredentials == null
|
||||
&& maxAge == null;
|
||||
}
|
||||
|
||||
public CorsMeta applyDefault() {
|
||||
String[] allowedOriginArray = null;
|
||||
Pattern[] allowedOriginPatternArray = null;
|
||||
if (this.allowedOrigins.length == 0) {
|
||||
allowedOriginArray = new String[] {ANY_VALUE};
|
||||
allowedOriginPatternArray = new Pattern[] {null};
|
||||
}
|
||||
|
||||
String[] allowedMethodArray = null;
|
||||
if (this.allowedMethods.length == 0) {
|
||||
allowedMethodArray =
|
||||
new String[] {HttpMethods.GET.name(), HttpMethods.HEAD.name(), HttpMethods.POST.name()};
|
||||
}
|
||||
|
||||
String[] allowedHeaderArray = null;
|
||||
if (this.allowedHeaders.length == 0) {
|
||||
allowedHeaderArray = new String[] {ANY_VALUE};
|
||||
}
|
||||
|
||||
Long maxAgeValue = null;
|
||||
if (this.maxAge == null) {
|
||||
maxAgeValue = 1800L;
|
||||
}
|
||||
|
||||
if (allowedOriginArray == null
|
||||
&& allowedMethodArray == null
|
||||
&& allowedHeaderArray == null
|
||||
&& maxAgeValue == null) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new CorsMeta(
|
||||
allowedOriginArray == null ? this.allowedOrigins : allowedOriginArray,
|
||||
allowedOriginPatternArray == null ? this.allowedOriginsPatterns : allowedOriginPatternArray,
|
||||
allowedMethodArray == null ? this.allowedMethods : allowedMethodArray,
|
||||
allowedHeaderArray == null ? this.allowedHeaders : allowedHeaderArray,
|
||||
exposedHeaders,
|
||||
allowCredentials,
|
||||
maxAgeValue);
|
||||
}
|
||||
|
||||
public CorsMeta combine(CorsMeta other) {
|
||||
if (other == null || other.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
return new CorsMeta(
|
||||
combine(allowedOrigins, other.allowedOrigins),
|
||||
merge(allowedOriginsPatterns, other.allowedOriginsPatterns).toArray(new Pattern[0]),
|
||||
combine(allowedMethods, other.allowedMethods),
|
||||
combine(allowedHeaders, other.allowedHeaders),
|
||||
combine(exposedHeaders, other.exposedHeaders),
|
||||
other.allowCredentials == null ? allowCredentials : other.allowCredentials,
|
||||
other.maxAge == null ? maxAge : other.maxAge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two arrays of CORS config values, with the other array having higher priority.
|
||||
*/
|
||||
private static String[] combine(String[] source, String[] other) {
|
||||
if (other.length == 0) {
|
||||
return source;
|
||||
}
|
||||
if (source.length == 0 || source[0].equals(ANY_VALUE) || other[0].equals(ANY_VALUE)) {
|
||||
return other;
|
||||
}
|
||||
return merge(source, other).toArray(EMPTY_STRING_ARRAY);
|
||||
}
|
||||
|
||||
private static <T> Set<T> merge(T[] source, T[] other) {
|
||||
int size = source.length + other.length;
|
||||
if (size == 0) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<T> merged = CollectionUtils.newLinkedHashSet(size);
|
||||
Collections.addAll(merged, source);
|
||||
Collections.addAll(merged, other);
|
||||
return merged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CorsMeta{"
|
||||
+ "allowedOrigins=" + Arrays.toString(allowedOrigins)
|
||||
+ ", allowedOriginsPatterns=" + Arrays.toString(allowedOriginsPatterns)
|
||||
+ ", allowedMethods=" + Arrays.toString(allowedMethods)
|
||||
+ ", allowedHeaders=" + Arrays.toString(allowedHeaders)
|
||||
+ ", exposedHeaders=" + Arrays.toString(exposedHeaders)
|
||||
+ ", allowCredentials=" + allowCredentials
|
||||
+ ", maxAge=" + maxAge
|
||||
+ '}';
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private static final Pattern PORTS_PATTERN = Pattern.compile("(.*):\\[(\\*|\\d+(,\\d+)*)]");
|
||||
|
||||
private final Set<String> allowedOrigins = new LinkedHashSet<>();
|
||||
private final Set<String> allowedMethods = new LinkedHashSet<>();
|
||||
private final Set<String> allowedHeaders = new LinkedHashSet<>();
|
||||
private final Set<String> exposedHeaders = new LinkedHashSet<>();
|
||||
private Boolean allowCredentials;
|
||||
private Long maxAge;
|
||||
|
||||
public Builder allowedOrigins(String... origins) {
|
||||
addValues(allowedOrigins, CorsUtils::formatOrigin, origins);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder allowedMethods(String... methods) {
|
||||
addValues(allowedMethods, v -> v.trim().toUpperCase(), methods);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder allowedHeaders(String... headers) {
|
||||
addValues(allowedHeaders, String::trim, headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder exposedHeaders(String... headers) {
|
||||
addValues(exposedHeaders, String::trim, headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
private static void addValues(Set<String> set, Function<String, String> fn, String... values) {
|
||||
if (values == null || set.contains(ANY_VALUE)) {
|
||||
return;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (StringUtils.isNotEmpty(value)) {
|
||||
value = fn.apply(value);
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (ANY_VALUE.equals(value)) {
|
||||
set.clear();
|
||||
set.add(ANY_VALUE);
|
||||
return;
|
||||
}
|
||||
set.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Pattern initPattern(String patternValue) {
|
||||
String ports = null;
|
||||
Matcher matcher = PORTS_PATTERN.matcher(patternValue);
|
||||
if (matcher.matches()) {
|
||||
patternValue = matcher.group(1);
|
||||
ports = matcher.group(2);
|
||||
}
|
||||
patternValue = "\\Q" + patternValue + "\\E";
|
||||
patternValue = patternValue.replace("*", "\\E.*\\Q");
|
||||
if (ports != null) {
|
||||
patternValue += (ANY_VALUE.equals(ports) ? "(:\\d+)?" : ":(" + ports.replace(',', '|') + ")");
|
||||
}
|
||||
return Pattern.compile(patternValue);
|
||||
}
|
||||
|
||||
public Builder allowCredentials(Boolean allowCredentials) {
|
||||
this.allowCredentials = allowCredentials;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder allowCredentials(String allowCredentials) {
|
||||
if ("true".equals(allowCredentials)) {
|
||||
this.allowCredentials = true;
|
||||
} else if ("false".equals(allowCredentials)) {
|
||||
this.allowCredentials = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxAge(Long maxAge) {
|
||||
if (maxAge != null && maxAge > -1) {
|
||||
this.maxAge = maxAge;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsMeta build() {
|
||||
int len = allowedOrigins.size();
|
||||
String[] origins = new String[len];
|
||||
List<Pattern> originsPatterns = new ArrayList<>(len);
|
||||
int i = 0;
|
||||
for (String origin : allowedOrigins) {
|
||||
origins[i++] = origin;
|
||||
if (ANY_VALUE.equals(origin)) {
|
||||
continue;
|
||||
}
|
||||
originsPatterns.add(initPattern(origin));
|
||||
}
|
||||
return new CorsMeta(
|
||||
origins,
|
||||
originsPatterns.toArray(new Pattern[0]),
|
||||
allowedMethods.toArray(EMPTY_STRING_ARRAY),
|
||||
allowedHeaders.toArray(EMPTY_STRING_ARRAY),
|
||||
exposedHeaders.toArray(EMPTY_STRING_ARRAY),
|
||||
allowCredentials,
|
||||
maxAge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
rest-cors=org.apache.dubbo.rpc.protocol.tri.rest.cors.CorsHeaderFilter
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.apache.dubbo.rpc.protocol.tri.rest.cors;
|
||||
|
||||
import org.apache.dubbo.remoting.http12.HttpMethods;
|
||||
import org.apache.dubbo.remoting.http12.HttpRequest;
|
||||
import org.apache.dubbo.remoting.http12.HttpResponse;
|
||||
import org.apache.dubbo.remoting.http12.HttpStatus;
|
||||
import org.apache.dubbo.remoting.http12.exception.HttpResultPayloadException;
|
||||
import org.apache.dubbo.remoting.http12.message.DefaultHttpResponse;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping;
|
||||
import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class CorsHeaderFilterTest {
|
||||
|
||||
private HttpRequest request;
|
||||
|
||||
private HttpResponse response;
|
||||
|
||||
private MockCorsHeaderFilter processor;
|
||||
|
||||
private RequestMapping build;
|
||||
|
||||
static class MockCorsHeaderFilter extends CorsHeaderFilter {
|
||||
public void process(HttpRequest request, HttpResponse response) {
|
||||
invoke(null, null, request, response);
|
||||
}
|
||||
|
||||
public void preLightProcess(HttpRequest request, HttpResponse response, int code) {
|
||||
try {
|
||||
process(request, response);
|
||||
Assertions.fail();
|
||||
} catch (HttpResultPayloadException e) {
|
||||
Assertions.assertEquals(code, e.getStatusCode());
|
||||
} catch (Exception e) {
|
||||
Assertions.fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CorsMeta defaultCorsMeta() {
|
||||
return CorsMeta.builder().maxAge(1000L).build();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
build = Mockito.mock(RequestMapping.class);
|
||||
request = Mockito.mock(HttpRequest.class);
|
||||
Mockito.when(request.attribute(RestConstants.MAPPING_ATTRIBUTE)).thenReturn(build);
|
||||
Mockito.when(request.uri()).thenReturn("/test.html");
|
||||
Mockito.when(request.serverName()).thenReturn("domain1.example");
|
||||
Mockito.when(request.scheme()).thenReturn("http");
|
||||
Mockito.when(request.serverPort()).thenReturn(80);
|
||||
Mockito.when(request.remoteHost()).thenReturn("127.0.0.1");
|
||||
response = new DefaultHttpResponse();
|
||||
response.setStatus(HttpStatus.OK.getCode());
|
||||
processor = new MockCorsHeaderFilter();
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestWithoutOriginHeader() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(build.getCors()).thenReturn(CorsMeta.builder().build());
|
||||
Mockito.when(build.getCors()).thenReturn(defaultCorsMeta());
|
||||
processor.process(request, response);
|
||||
Assertions.assertFalse(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertTrue(response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ORIGIN));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS));
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameOriginRequest() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("http://domain1.example");
|
||||
Mockito.when(build.getCors()).thenReturn(defaultCorsMeta());
|
||||
processor.process(request, response);
|
||||
Assertions.assertFalse(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertTrue(response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ORIGIN));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS));
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestWithOriginHeader() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(build.getCors()).thenReturn(defaultCorsMeta());
|
||||
Assertions.assertThrows(HttpResultPayloadException.class, () -> processor.process(request, response));
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestWithOriginHeaderAndNullConfig() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(build.getCors()).thenReturn(null);
|
||||
processor.process(request, response);
|
||||
Assertions.assertFalse(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestWithOriginHeaderAndAllowedOrigin() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(build.getCors()).thenReturn(CorsMeta.builder().build().applyDefault());
|
||||
processor.process(request, response);
|
||||
Assertions.assertTrue(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertEquals("*", response.header(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertFalse(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_MAX_AGE));
|
||||
Assertions.assertFalse(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_EXPOSE_HEADERS));
|
||||
Assertions.assertTrue(response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ORIGIN));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS));
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestCaseInsensitiveOriginMatch() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(build.getCors())
|
||||
.thenReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://DOMAIN2.com")
|
||||
.build()
|
||||
.applyDefault());
|
||||
processor.process(request, response);
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
Assertions.assertTrue(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestTrailingSlashOriginMatch() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(build.getCors())
|
||||
.thenReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://domain2.com/")
|
||||
.build()
|
||||
.applyDefault());
|
||||
processor.process(request, response);
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
Assertions.assertTrue(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestExposedHeaders() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.doReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.exposedHeaders("header1", "header2")
|
||||
.build()
|
||||
.applyDefault())
|
||||
.when(build)
|
||||
.getCors();
|
||||
processor.process(request, response);
|
||||
Assertions.assertTrue(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertEquals("https://domain2.com", response.header(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertTrue(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_EXPOSE_HEADERS));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.ACCESS_CONTROL_EXPOSE_HEADERS).contains("header1"));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.ACCESS_CONTROL_EXPOSE_HEADERS).contains("header2"));
|
||||
Assertions.assertTrue(response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ORIGIN));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS));
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestCredentials() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.doReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://domain1.com", "https://domain2.com")
|
||||
.allowCredentials(true)
|
||||
.build()
|
||||
.applyDefault())
|
||||
.when(build)
|
||||
.getCors();
|
||||
processor.process(request, response);
|
||||
Assertions.assertTrue(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertEquals("https://domain2.com", response.header(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||
Assertions.assertTrue(response.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS));
|
||||
Assertions.assertEquals("true", response.header(CorsHeaderFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS));
|
||||
Assertions.assertTrue(response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ORIGIN));
|
||||
Assertions.assertEquals(HttpStatus.OK.getCode(), response.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualRequestCredentialsWithWildcardOrigin() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.doReturn(CorsMeta.builder()
|
||||
.allowedOrigins("*")
|
||||
.allowCredentials(true)
|
||||
.build()
|
||||
.applyDefault())
|
||||
.when(build)
|
||||
.getCors();
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> processor.process(request, response));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestWrongAllowedMethod() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("DELETE");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(build.getCors())
|
||||
.thenReturn(CorsMeta.builder().allowedOrigins("*").build());
|
||||
processor.preLightProcess(request, response, HttpStatus.FORBIDDEN.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestMatchedAllowedMethod() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(build.getCors()).thenReturn(CorsMeta.builder().build().applyDefault());
|
||||
processor.preLightProcess(request, response, HttpStatus.NO_CONTENT.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestTestWithOriginButWithoutOtherHeaders() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(build.getCors()).thenReturn(defaultCorsMeta());
|
||||
processor.preLightProcess(request, response, HttpStatus.FORBIDDEN.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestWithoutRequestMethod() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||
.thenReturn("Header1");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(build.getCors()).thenReturn(defaultCorsMeta());
|
||||
processor.preLightProcess(request, response, HttpStatus.FORBIDDEN.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestWithRequestAndMethodHeaderButNoConfig() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||
.thenReturn("Header1");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(build.getCors()).thenReturn(defaultCorsMeta());
|
||||
processor.preLightProcess(request, response, HttpStatus.FORBIDDEN.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestValidRequestAndConfig() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||
.thenReturn("Header1");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(build.getCors())
|
||||
.thenReturn(CorsMeta.builder()
|
||||
.allowedOrigins("*")
|
||||
.allowedMethods("GET", "PUT")
|
||||
.allowedHeaders("Header1", "Header2")
|
||||
.build());
|
||||
processor.preLightProcess(request, response, HttpStatus.NO_CONTENT.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestAllowedHeaders() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.doReturn(Arrays.asList("Header1", "Header2"))
|
||||
.when(request)
|
||||
.headerValues(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
Mockito.doReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.allowedHeaders("Header1", "Header2")
|
||||
.build()
|
||||
.applyDefault())
|
||||
.when(build)
|
||||
.getCors();
|
||||
processor.preLightProcess(request, response, HttpStatus.NO_CONTENT.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestAllowsAllHeaders() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(request.headerValues(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||
.thenReturn(Arrays.asList("Header1", "Header2"));
|
||||
Mockito.when(build.getCors())
|
||||
.thenReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.allowedHeaders("*")
|
||||
.build()
|
||||
.applyDefault());
|
||||
processor.preLightProcess(request, response, HttpStatus.NO_CONTENT.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestWithEmptyHeaders() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
Mockito.when(request.hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn(true);
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||
.thenReturn("");
|
||||
Mockito.when(build.getCors())
|
||||
.thenReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.allowedHeaders("*")
|
||||
.build()
|
||||
.applyDefault());
|
||||
processor.preLightProcess(request, response, HttpStatus.NO_CONTENT.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestWithNullConfig() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
Mockito.when(build.getCors())
|
||||
.thenReturn(CorsMeta.builder().allowedOrigins("*").build());
|
||||
processor.preLightProcess(request, response, HttpStatus.FORBIDDEN.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preflightRequestCredentials() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.OPTIONS.name());
|
||||
Mockito.when(request.header(CorsHeaderFilter.ORIGIN)).thenReturn("https://domain2.com");
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD))
|
||||
.thenReturn("GET");
|
||||
Mockito.doReturn(true).when(request).hasHeader(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD);
|
||||
Mockito.when(request.header(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||
.thenReturn("Header1");
|
||||
Mockito.doReturn(CorsMeta.builder()
|
||||
.allowedOrigins("https://domain1.com", "https://domain2.com", "http://domain3.example")
|
||||
.allowedHeaders("Header1")
|
||||
.allowCredentials(true)
|
||||
.build()
|
||||
.applyDefault())
|
||||
.when(build)
|
||||
.getCors();
|
||||
processor.preLightProcess(request, response, HttpStatus.NO_CONTENT.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preventDuplicatedVaryHeaders() {
|
||||
Mockito.when(request.method()).thenReturn(HttpMethods.GET.name());
|
||||
response.setHeader(
|
||||
CorsHeaderFilter.VARY,
|
||||
CorsHeaderFilter.ORIGIN + "," + CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD + ","
|
||||
+ CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
processor.process(request, response);
|
||||
Assertions.assertTrue(response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ORIGIN));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_METHOD));
|
||||
Assertions.assertTrue(
|
||||
response.header(CorsHeaderFilter.VARY).contains(CorsHeaderFilter.ACCESS_CONTROL_REQUEST_HEADERS));
|
||||
}
|
||||
}
|
||||
2
pom.xml
2
pom.xml
|
|
@ -571,6 +571,7 @@
|
|||
**/org/apache/dubbo/test/common/utils/TestSocketUtils.java,
|
||||
**/org/apache/dubbo/triple/TripleWrapper.java,
|
||||
**/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java,
|
||||
**/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilter.java,
|
||||
**/org/apache/dubbo/metrics/aggregate/DubboMergingDigest.java,
|
||||
**/org/apache/dubbo/metrics/aggregate/DubboAbstractTDigest.java,
|
||||
**/org/apache/dubbo/common/logger/helpers/FormattingTuple.java,
|
||||
|
|
@ -842,6 +843,7 @@
|
|||
<exclude>src/test/java/org/apache/dubbo/config/spring/EmbeddedZooKeeper.java</exclude>
|
||||
<exclude>src/main/java/org/apache/dubbo/test/common/utils/TestSocketUtils.java</exclude>
|
||||
<exclude>src/main/java/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java</exclude>
|
||||
<exclude>src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilter.java</exclude>
|
||||
<exclude>src/main/java/org/apache/dubbo/common/threadpool/serial/SerializingExecutor.java</exclude>
|
||||
<exclude>src/main/java/org/apache/dubbo/maven/plugin/aot/AbstractAotMojo.java</exclude>
|
||||
<exclude>src/main/java/org/apache/dubbo/maven/plugin/aot/AbstractDependencyFilterMojo.java</exclude>
|
||||
|
|
|
|||
Loading…
Reference in New Issue