diff --git a/.licenserc.yaml b/.licenserc.yaml
index e497f4b10e..98957a25f7 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -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'
diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/CorsConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/CorsConfig.java
new file mode 100644
index 0000000000..657f20d039
--- /dev/null
+++ b/dubbo-common/src/main/java/org/apache/dubbo/config/CorsConfig.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.
+ *
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.
+ *
If not set, only {@code "GET"} and {@code "HEAD"} are allowed.
+ *
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.
+ *
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.
+ *
By default this is not set.
+ */
+ private String[] exposedHeaders;
+
+ /**
+ * Whether user credentials are supported.
+ *
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;
+ }
+}
diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/RestConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/RestConfig.java
index 6a3674e119..dc7e988928 100644
--- a/dubbo-common/src/main/java/org/apache/dubbo/config/RestConfig.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/config/RestConfig.java
@@ -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;
+ }
}
diff --git a/dubbo-plugin/dubbo-rest-jaxrs/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/jaxrs/JaxrsRequestMappingResolver.java b/dubbo-plugin/dubbo-rest-jaxrs/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/jaxrs/JaxrsRequestMappingResolver.java
index 9d7dc5a143..72cb325351 100644
--- a/dubbo-plugin/dubbo-rest-jaxrs/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/jaxrs/JaxrsRequestMappingResolver.java
+++ b/dubbo-plugin/dubbo-rest-jaxrs/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/jaxrs/JaxrsRequestMappingResolver.java
@@ -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();
}
diff --git a/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMvcRequestMappingResolver.java b/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMvcRequestMappingResolver.java
index ca4dd68a3b..e6de4f568b 100644
--- a/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMvcRequestMappingResolver.java
+++ b/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMvcRequestMappingResolver.java
@@ -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);
+ }
}
diff --git a/dubbo-plugin/dubbo-rest-spring/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/compatible/SpringDemoServiceImpl.java b/dubbo-plugin/dubbo-rest-spring/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/compatible/SpringDemoServiceImpl.java
index bf235fed57..46498e5162 100644
--- a/dubbo-plugin/dubbo-rest-spring/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/compatible/SpringDemoServiceImpl.java
+++ b/dubbo-plugin/dubbo-rest-spring/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/compatible/SpringDemoServiceImpl.java
@@ -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 context;
private boolean called;
diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/AbstractServerHttpChannelObserver.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/AbstractServerHttpChannelObserver.java
index 9883a8072e..26ce8b705e 100644
--- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/AbstractServerHttpChannelObserver.java
+++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/AbstractServerHttpChannelObserver.java
@@ -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 {
diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/HttpStatus.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/HttpStatus.java
index f91b628028..f28eb9422f 100644
--- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/HttpStatus.java
+++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/HttpStatus.java
@@ -20,6 +20,7 @@ public enum HttpStatus {
OK(200),
CREATED(201),
ACCEPTED(202),
+ NO_CONTENT(204),
FOUND(302),
BAD_REQUEST(400),
UNAUTHORIZED(401),
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java
index 970aa88fa7..fc0961fdc7 100644
--- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java
@@ -162,10 +162,10 @@ public abstract class AbstractServerTransportListener= HttpStatus.BAD_REQUEST.getCode()) {
LOGGER.debug("http status exception", e);
- return;
}
+ return;
}
LOGGER.error(INTERNAL_ERROR, "", "", "server internal error", t);
}
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/RestConstants.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/RestConstants.java
index 7a431bd02a..7c512278bb 100644
--- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/RestConstants.java
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/RestConstants.java
@@ -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() {}
}
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilter.java
new file mode 100644
index 0000000000..f3dbe3eb62
--- /dev/null
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilter.java
@@ -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: DefaultCorsProcessor
+ */
+@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 allowMethods =
+ checkMethods(cors, preFlight ? request.header(ACCESS_CONTROL_REQUEST_METHOD) : request.method());
+ if (allowMethods == null) {
+ return false;
+ }
+
+ List 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 varyHeaders = response.headerValues(VARY);
+ String varyValue;
+ if (varyHeaders == null) {
+ varyValue = ORIGIN + SEP + ACCESS_CONTROL_REQUEST_METHOD + SEP + ACCESS_CONTROL_REQUEST_HEADERS;
+ } else {
+ Set 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 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 checkHeaders(CorsMeta cors, Collection 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 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;
+ }
+}
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsUtils.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsUtils.java
new file mode 100644
index 0000000000..a9aab0bd91
--- /dev/null
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsUtils.java
@@ -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;
+ }
+}
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/filter/RestHeaderFilterAdapter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/filter/RestHeaderFilterAdapter.java
index abbf4a7a5a..bb6f0927fb 100644
--- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/filter/RestHeaderFilterAdapter.java
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/filter/RestHeaderFilterAdapter.java
@@ -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;
}
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/mapping/RequestMapping.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/mapping/RequestMapping.java
index 2c38367c71..5d00ad8eac 100644
--- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/mapping/RequestMapping.java
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/mapping/RequestMapping.java
@@ -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 customCondition,
+ CorsMeta cors,
ResponseMeta response) {
this.name = name;
this.pathCondition = pathCondition;
@@ -65,6 +68,7 @@ public final class RequestMapping implements Condition> T combine(T value, T other) {
- return value == null ? other : other == null ? value : value.combine(other);
+ private > 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 Set merge(T[] source, T[] other) {
+ int size = source.length + other.length;
+ if (size == 0) {
+ return Collections.emptySet();
+ }
+ Set 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 allowedOrigins = new LinkedHashSet<>();
+ private final Set allowedMethods = new LinkedHashSet<>();
+ private final Set allowedHeaders = new LinkedHashSet<>();
+ private final Set 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 set, Function 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 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);
+ }
+ }
+}
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.HeaderFilter b/dubbo-rpc/dubbo-rpc-triple/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.HeaderFilter
new file mode 100644
index 0000000000..fa26599c25
--- /dev/null
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.HeaderFilter
@@ -0,0 +1 @@
+rest-cors=org.apache.dubbo.rpc.protocol.tri.rest.cors.CorsHeaderFilter
\ No newline at end of file
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilterTest.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilterTest.java
new file mode 100644
index 0000000000..70ef114483
--- /dev/null
+++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilterTest.java
@@ -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));
+ }
+}
diff --git a/pom.xml b/pom.xml
index b93069ee27..a8a3b1d694 100644
--- a/pom.xml
+++ b/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 @@
src/test/java/org/apache/dubbo/config/spring/EmbeddedZooKeeper.java
src/main/java/org/apache/dubbo/test/common/utils/TestSocketUtils.java
src/main/java/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java
+ src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/cors/CorsHeaderFilter.java
src/main/java/org/apache/dubbo/common/threadpool/serial/SerializingExecutor.java
src/main/java/org/apache/dubbo/maven/plugin/aot/AbstractAotMojo.java
src/main/java/org/apache/dubbo/maven/plugin/aot/AbstractDependencyFilterMojo.java