[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:
Rawven 2024-05-23 11:05:17 +08:00 committed by GitHub
parent 8368262406
commit 1f6d441de6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1301 additions and 25 deletions

View File

@ -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'

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -20,6 +20,7 @@ public enum HttpStatus {
OK(200),
CREATED(201),
ACCEPTED(202),
NO_CONTENT(204),
FOUND(302),
BAD_REQUEST(400),
UNAUTHORIZED(401),

View File

@ -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);
}

View File

@ -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() {}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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));
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1 @@
rest-cors=org.apache.dubbo.rpc.protocol.tri.rest.cors.CorsHeaderFilter

View File

@ -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));
}
}

View File

@ -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>