dubbo icon indicating copy to clipboard operation
dubbo copied to clipboard

[Feature][3.3] CORS support for triple rest protocol

Open oxsean opened this issue 11 months ago • 2 comments

Pre-check

  • [X] I am sure that all the content I provide is in English.

Search before asking

  • [X] I had searched in the issues and found no similar feature requirement.

Apache Dubbo Component

Java SDK (apache/dubbo)

Descriptions

Supports CORS negotiation in triple rest and supports options configuration similar to Spring MVC.

Related issues

No response

Are you willing to submit a pull request to fix on your own?

  • [ ] Yes I am willing to submit a pull request on my own!

Code of Conduct

oxsean avatar Mar 20 '24 04:03 oxsean

Can it be assigned to me ,i can complete it

Rawven avatar Apr 01 '24 03:04 Rawven

Triple Rest Cors Plan

1. Cors related implementation

Under the rest/cors package of dubbo-rpc-triple module

CorsMeta configuration class currently supports the following modules and methods related to inspection and processing of some attributes.


public class CorsMeta {
.........
    private List<String> allowedOrigins;
    private List<OriginPattern> allowedOriginPatterns;
    private List<String> allowedMethods;
    private List<HttpMethods> resolvedMethods = DEFAULT_METHODS;
    private List<String> allowedHeaders;
    private List<String> exposedHeaders;
    private Boolean allowCredentials;
    private Boolean allowPrivateNetwork;
    private Long maxAge;
 .........

CorsProcessor, used to process requests. The following are the main methods of DefaultCorsProcesso


public class DefaultCorsProcessor implements CorsProcessor {
    public boolean process(CorsMeta config, HttpRequest request, HttpResponse response) {
        // set vary header
        setVaryHeaders(response);
        // skip if is not a cors request
        if (!isCorsRequest(request)) {
            return true;
        }
        // skip if origin already contains in Access-Control-Allow-Origin header
        if (response.header(RestConstants.ACCESS_CONTROL_ALLOW_ORIGIN) != null)...

        if (config == null) {
            // if no cors config and is a preflight request
            if (isPreFlight(request)) {
                reject(response);
                return false;
            }
            return true;
        }
        // handle cors request
        return handleInternal(request, response, config, isPreFlight(request));
    }

    private boolean isPreFlight(HttpRequest request) {
        // preflight request is a OPTIONS request with 
        // Access-Control-Request-Method header
        return request.method().equals(HttpMethods.OPTIONS.name())
                && request.header(RestConstants.ACCESS_CONTROL_REQUEST_METHOD) != null;
    }

    private boolean isCorsRequest(HttpRequest request) {
        // skip if request has no origin header
        String origin = request.header(RestConstants.ORIGIN);
        if (origin == null) {
            return false;
        }
        try {
            URI uri = new URI(origin);
            // return true if origin is not the same as request's scheme, host and port
            return !(Objects.equals(uri.getScheme(), request.scheme())
                    && uri.getHost().equals(request.serverName())
                    //getPortByScheme
                    && getPort(uri.getScheme(), uri.getPort()) == getPort(request.scheme(), request.serverPort()));
        } catch (URISyntaxException e) {
            // skip if origin is not a valid URI
            return false;
        }
    }

    protected boolean handleInternal(HttpRequest request, HttpResponse response, CorsMeta config, boolean isPreLight) {
        //check origin,method,header are allowed
        String allowOrigin = config.checkOrigin(request.header(RestConstants.ORIGIN));
        ........
        List<HttpMethods> allowHttpMethods = config.checkHttpMethods(getHttpMethods(request, isPreLight));
        .......
        List<String> allowHeaders = config.checkHeaders(getHttpHeaders(request, isPreLight));
        ......
        response.setHeader(RestConstants.ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigin);
        //set allow method if is prelight
        if (isPreLight) {
            response.setHeader(
                    RestConstants.ACCESS_CONTROL_ALLOW_METHODS,
                   ......   
        if (isPreLight && !allowHeaders.isEmpty()) ......
          if (isPreLight && config.getMaxAge() != null)....
        // set related config
        if (!CollectionUtils.isEmpty(config.getExposedHeaders()))...
        if (Boolean.TRUE.equals(config.getAllowCredentials())) .....
        if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) .....
        return true;
    }
}

2.Initialize Cors configuration

Global configuration

Add RestCorsConfig under org.apache.dubbo.config.RestConfig

Add the CorsMeta attribute under org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping

public class RestConfig implements Serializable {
.......
    /**
     *  The config is used to set the Global CORS configuration properties.
     */
    private CorsConfig corsConfig;
.......    
}
public final class RequestMapping implements Condition<RequestMapping, HttpRequest> {
    ......... 
    private final CorsMeta corsMeta;
}

Configuration parsing will combine global, class, and method configurations in org.apache.dubbo.rpc.protocol.tri.rest.mapping.DefaultRequestMappingRegistry

public final class DefaultRequestMappingRegistry implements RequestMappingRegistry {
    private CorsMeta globalCorsMeta;
    .....
   
    @Override
    public void register(Invoker<?> invoker) {
        new MethodWalker().walk(service.getClass(), (classes, consumer) -> {
            for (RequestMappingResolver resolver : resolvers) {
                .....
                // combine gloabl config in class 
                classMapping.setCorsMeta(classMapping.getCorsMeta().combine(getGlobalCorsMeta()));
                consumer.accept((methods) -> {
                    .....
                    //combine class in method
                    methodMapping = classMapping.combine(methodMapping);
                    ....
                });
            }
        });
    }
    
    private CorsMeta getGlobalCorsMeta() {
        if (globalCorsMeta == null) {
            Configuration globalConfiguration =
                    ConfigurationUtils.getGlobalConfiguration(ApplicationModel.defaultModel());
            globalCorsMeta = CorsUtil.resolveGlobalMeta(globalConfiguration);
        }
        return globalCorsMeta;
    }
    

spring@CrossOrigin annotation configuration


@Activate(onClass = "org.springframework.web.bind.annotation.RequestMapping")
public class SpringMvcRequestMappingResolver implements RequestMappingResolver {
    public RequestMapping resolve(ServiceMeta serviceMeta) {
    ......
        AnnotationMeta<?> crossOrigin = serviceMeta.findMergedAnnotation(Annotations.CrossOrigin);
        return builder(requestMapping, responseStatus)
                 ....
                .cors(createCorsMeta(crossOrigin))
                .build();
    }

    public RequestMapping resolve(MethodMeta methodMeta) 
        AnnotationMeta<?> crossOrigin = methodMeta.findMergedAnnotation(Annotations.CrossOrigin);
        return builder(requestMapping, responseStatus)
                 ......
                .cors(createCorsMeta(crossOrigin))
                .build();
    }
    
    private CorsMeta createCorsMeta(AnnotationMeta<?> crossOrigin) {
        CorsMeta meta = new CorsMeta();
        if (crossOrigin == null) {
            return meta;
        }
        meta.setAllowCredentials(Boolean.valueOf(crossOrigin.getString("allowCredentials")));
        meta.setAllowedHeaders(Arrays.asList(crossOrigin.getStringArray("allowedHeaders")));
        .....
        return meta;
    }
}

3.Perform request interception processing

Add the CorsProcessor member variable under org.apache.dubbo.rpc.protocol.tri.rest.mapping.DefaultRequestMappingRegistry, and add the following processing code in the lookup method


public final class DefaultRequestMappingRegistry implements RequestMappingRegistry {
    .......
    private CorsMeta globalCorsMeta;
    private final CorsProcessor corsProcessor;

    public DefaultRequestMappingRegistry(FrameworkModel frameworkModel){
        corsProcessor = frameworkModel.getBeanFactory().getOrRegisterBean(CorsProcessor.class);
    }
    public HandlerMeta lookup(HttpRequest request, HttpResponse response) {
        .......
        List<Candidate> candidates = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            Match<Registration> match = matches.get(i);
            RequestMapping mapping = match.getValue().mapping.match(request, match.getExpression());
            if (mapping != null) {
                Candidate candidate = new Candidate();
                candidate.mapping = mapping;
                candidate.meta = match.getValue().meta;
                candidate.expression = match.getExpression();
                candidate.variableMap = match.getVariableMap();
                candidates.add(candidate);
            }
        }
      
        RequestMapping mapping = winner.mapping;
        
        //Handle request
        processCors(method, mapping, request, response);
        ....
    }

    private void processCors(RequestMapping mapping, HttpRequest request, HttpResponse response) {
        if (!corsProcessor.process(mapping.getCorsMeta(), request, response)) {
            throw new HttpResultPayloadException(HttpResult.builder()
                    .......
        }
    }
    ............

Rawven avatar Apr 11 '24 09:04 Rawven

Thanks for you contribution @Rawven

oxsean avatar May 23 '24 03:05 oxsean