All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter Maven / Gradle / Ivy

There is a newer version: 3.0.0-milestone2
Show newest version
/**
 * 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.cxf.rs.security.cors;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;

import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;

import org.apache.cxf.common.util.ReflectionUtil;
import org.apache.cxf.jaxrs.JAXRSServiceImpl;
import org.apache.cxf.jaxrs.ext.RequestHandler;
import org.apache.cxf.jaxrs.ext.ResponseHandler;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.apache.cxf.jaxrs.model.ClassResourceInfo;
import org.apache.cxf.jaxrs.model.OperationResourceInfo;
import org.apache.cxf.jaxrs.model.URITemplate;
import org.apache.cxf.jaxrs.utils.HttpUtils;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.service.Service;

/**
 * A single class that provides both an input and an output filter for CORS, following
 * http://www.w3.org/TR/cors/. The input filter examines the input headers. If the request is valid, it stores the
 * information in the Exchange to allow the response handler to add the appropriate headers to the response.
 * If you need complex or subtle control of the behavior here (e.g. clearing the prefight cache) you might be
 * better off reading the source of this class and implementing this inside your service.
 * 
 * This class will perform preflight processing even if there is a resource method annotated 
 * to handle @OPTIONS,
 * unless that method is annotated as follows:
 * 
 *   @LocalPreflight
 * 
* or unless the defaultOptionsMethodsHandlePreflight property of this class is set to true. */ public class CrossOriginResourceSharingFilter implements RequestHandler, ResponseHandler { private static final Pattern SPACE_PATTERN = Pattern.compile(" "); private static final Pattern FIELD_COMMA_PATTERN = Pattern.compile(","); private static final String LOCAL_PREFLIGHT = "local_preflight"; private static final String LOCAL_PREFLIGHT_ORIGIN = "local_preflight.origin"; private static final String LOCAL_PREFLIGHT_METHOD = "local_preflight.method"; private static final String PREFLIGHT_PASSED = "preflight_passed"; private static final String PREFLIGHT_FAILED = "preflight_failed"; private static final String SIMPLE_REQUEST = "simple_request"; @Context private HttpHeaders headers; /** * This would be a rather painful list to maintain for real, since it's entirely dependent on the * deployment. */ private List allowOrigins = Collections.emptyList(); private List allowHeaders = Collections.emptyList(); private boolean allowCredentials; private List exposeHeaders = Collections.emptyList(); private Integer maxAge; private Integer preflightFailStatus = 200; private boolean defaultOptionsMethodsHandlePreflight; private T getAnnotation(Method m, Class annClass) { if (m == null) { return null; } return ReflectionUtil.getAnnotationForMethodOrContainingClass( m, annClass); } public Response handleRequest(Message m, ClassResourceInfo resourceClass) { OperationResourceInfo opResInfo = m.getExchange().get(OperationResourceInfo.class); CrossOriginResourceSharing annotation = opResInfo == null ? null : getAnnotation(opResInfo.getAnnotatedMethod(), CrossOriginResourceSharing.class); if ("OPTIONS".equals(m.get(Message.HTTP_REQUEST_METHOD))) { return preflightRequest(m, annotation, opResInfo, resourceClass); } return simpleRequest(m, annotation); } private Response simpleRequest(Message m, CrossOriginResourceSharing ann) { List values = getHeaderValues(CorsHeaderConstants.HEADER_ORIGIN, true); // 5.1.1 there has to be an origin if (values == null || values.size() == 0) { return null; } // 5.1.2 check all the origins if (!effectiveAllowOrigins(ann, values)) { return null; } String originResponse; // 5.1.3 credentials lives in the output filter // in any case if (effectiveAllowAllOrigins(ann)) { originResponse = "*"; } else { originResponse = concatValues(values, true); } // handle 5.1.3 commonRequestProcessing(m, ann, originResponse); // 5.1.4 List effectiveExposeHeaders = effectiveExposeHeaders(ann); if (effectiveExposeHeaders != null && effectiveExposeHeaders.size() != 0) { m.getExchange().put(CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS, effectiveExposeHeaders); } // note what kind of processing we're doing. m.getExchange().put(CrossOriginResourceSharingFilter.class.getName(), SIMPLE_REQUEST); return null; } /** * handle preflight. * * Note that preflight is a bit of a parasite on OPTIONS. The class may still have an options method, * and, if it does, it will be invoked, and it will respond however it likes. The response will * have additional headers based on what happens here. * * @param m the incoming message. * @param opResInfo * @param ann the annotation, if any, derived from a method that matched the OPTIONS request for the * preflight. probably completely useless. * @param resourceClass the resource class passed into the filter. * @return */ //CHECKSTYLE:OFF private Response preflightRequest(Message m, CrossOriginResourceSharing corsAnn, OperationResourceInfo opResInfo, ClassResourceInfo resourceClass) { // Validate main CORS preflight properties (origin, method) // even if Local preflight is requested // 5.2.1 -- must have origin, must have one origin. List headerOriginValues = getHeaderValues(CorsHeaderConstants.HEADER_ORIGIN, true); if (headerOriginValues == null || headerOriginValues.size() != 1) { return null; } String origin = headerOriginValues.get(0); // 5.2.3 must have access-control-request-method, must be single-valued // we should reject parse errors but we cannot. List requestMethodValues = getHeaderValues(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD, false); if (requestMethodValues == null || requestMethodValues.size() != 1) { return createPreflightResponse(m, false); } String requestMethod = requestMethodValues.get(0); /* * Ask JAX-RS runtime to validate that the matching resource method actually exists. */ Method method = getPreflightMethod(m, requestMethod); if (method == null) { return null; } /* * What to do if the resource class indeed has a method annotated with @OPTIONS * that is matched by this request? We go ahead and do this job unless the request * has one of our annotations on it (or its parent class) indicating 'localPreflight' -- * or the defaultOptionsMethodsHandlePreflight flag is true. */ LocalPreflight preflightAnnotation = opResInfo == null ? null : getAnnotation(opResInfo.getAnnotatedMethod(), LocalPreflight.class); if (preflightAnnotation != null || defaultOptionsMethodsHandlePreflight) { m.put(LOCAL_PREFLIGHT, "true"); m.put(LOCAL_PREFLIGHT_ORIGIN, origin); m.put(LOCAL_PREFLIGHT_METHOD, method); return null; // let the resource method take all responsibility. } CrossOriginResourceSharing ann = getAnnotation(method, CrossOriginResourceSharing.class); ann = ann == null ? corsAnn : ann; /* We aren't required to have any annotation at all. If no annotation, * the properties of this filter make all the decisions. */ // 5.2.2 must be on the list or we must be matching *. if (!effectiveAllowOrigins(ann, Collections.singletonList(origin))) { return createPreflightResponse(m, false); } // 5.2.4 get list of request headers. we should reject parse errors but we cannot. List requestHeaders = getHeaderValues(CorsHeaderConstants.HEADER_AC_REQUEST_HEADERS, false); // 5.2.5 reject if the method is not on the list. // This was indirectly enforced by getCorsMethod() // 5.2.6 reject if the header is not listed. if (!effectiveAllowHeaders(ann, requestHeaders)) { return createPreflightResponse(m, false); } // 5.2.7: add allow credentials and allow-origin as required: this lives in the Output filter String originResponse; if (effectiveAllowAllOrigins(ann)) { originResponse = "*"; } else { originResponse = origin; } // 5.2.9 add allow-methods; we pass them from here to the output filter which actually adds them. m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_METHODS, Arrays.asList(requestMethod)); // 5.2.10 add allow-headers; we pass them from here to the output filter which actually adds them. m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, requestHeaders); // 5.2.8 max-age lives in the output filter. if (effectiveMaxAge(ann) != null) { m.getExchange().put(CorsHeaderConstants.HEADER_AC_MAX_AGE,effectiveMaxAge(ann).toString()); } // 5.2.7 is in here. commonRequestProcessing(m, ann, originResponse); return createPreflightResponse(m, true); } //CHECKSTYLE:ON private Response createPreflightResponse(Message m, boolean passed) { m.getExchange().put(CrossOriginResourceSharingFilter.class.getName(), passed ? PREFLIGHT_PASSED : PREFLIGHT_FAILED); int status = passed ? 200 : preflightFailStatus; return Response.status(status).build(); } private Method getPreflightMethod(Message m, String httpMethod) { String requestUri = HttpUtils.getPathToMatch(m, true); Service service = m.getExchange().get(Service.class); List resources = ((JAXRSServiceImpl)service).getClassResourceInfos(); MultivaluedMap values = new MetadataMap(); ClassResourceInfo resource = JAXRSUtils.selectResourceClass(resources, requestUri, values, m); if (resource == null) { return null; } OperationResourceInfo ori = findPreflightMethod(resource, requestUri, httpMethod, values, m); return ori == null ? null : ori.getAnnotatedMethod(); } private OperationResourceInfo findPreflightMethod(ClassResourceInfo resource, String requestUri, String httpMethod, MultivaluedMap values, Message m) { final String contentType = MediaType.WILDCARD; final MediaType acceptType = MediaType.WILDCARD_TYPE; OperationResourceInfo ori = JAXRSUtils.findTargetMethod(resource, m, httpMethod, values, contentType, Collections.singletonList(acceptType), true); if (ori == null) { return null; } if (ori.isSubResourceLocator()) { Class cls = ori.getMethodToInvoke().getReturnType(); ClassResourceInfo subcri = resource.getSubResource(cls, cls); if (subcri == null) { return null; } else { MultivaluedMap newValues = new MetadataMap(); newValues.putAll(values); return findPreflightMethod(subcri, values.getFirst(URITemplate.FINAL_MATCH_GROUP), httpMethod, newValues, m); } } else { return ori; } } private void commonRequestProcessing(Message m, CrossOriginResourceSharing ann, String origin) { m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, origin); m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS, effectiveAllowCredentials(ann)); } public Response handleResponse(Message m, OperationResourceInfo ori, Response response) { String op = (String)m.getExchange().get(CrossOriginResourceSharingFilter.class.getName()); if (op == null || op == PREFLIGHT_FAILED) { return response; } ResponseBuilder rbuilder = Response.fromResponse(response); /* Common to simple and preflight */ rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, m.getExchange().get(CorsHeaderConstants.HEADER_ORIGIN)); rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS, m.getExchange().get(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS)); if (SIMPLE_REQUEST.equals(op)) { /* 5.1.4 expose headers */ List effectiveExposeHeaders = getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS); if (effectiveExposeHeaders != null) { addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS, effectiveExposeHeaders, false); } // if someone wants to clear the cache, we can't help them. return rbuilder.build(); } else { // 5.2.8 max-age String maValue = (String)m.getExchange().get(CorsHeaderConstants.HEADER_AC_MAX_AGE); if (maValue != null) { rbuilder.header(CorsHeaderConstants.HEADER_AC_MAX_AGE, maValue); } // 5.2.9 add allowed methods /* * Currently, input side just lists the one requested method, and spec endorses that. */ addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS, getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS), false); // 5.2.10 add allowed headers List rqAllowedHeaders = getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS); if (rqAllowedHeaders != null) { addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, rqAllowedHeaders, false); } return rbuilder.build(); } } private boolean effectiveAllowAllOrigins(CrossOriginResourceSharing ann) { if (ann != null) { return ann.allowAllOrigins(); } else { return allowOrigins.isEmpty(); } } private boolean effectiveAllowCredentials(CrossOriginResourceSharing ann) { if (ann != null) { return ann.allowCredentials(); } else { return allowCredentials; } } private boolean effectiveAllowOrigins(CrossOriginResourceSharing ann, List origins) { if (effectiveAllowAllOrigins(ann)) { return true; } List actualOrigins = Collections.emptyList(); if (ann != null) { actualOrigins = Arrays.asList(ann.allowOrigins()); } if (actualOrigins.isEmpty()) { actualOrigins = allowOrigins; } return actualOrigins.containsAll(origins); } private boolean effectiveAllowAnyHeaders(CrossOriginResourceSharing ann) { if (ann != null) { return ann.allowHeaders().length == 0; } else { return allowHeaders.isEmpty(); } } private boolean effectiveAllowHeaders(CrossOriginResourceSharing ann, List aHeaders) { if (effectiveAllowAnyHeaders(ann)) { return true; } List actualHeaders = null; if (ann != null) { actualHeaders = Arrays.asList(ann.allowHeaders()); } else { actualHeaders = allowHeaders; } Set actualHeadersSet = new TreeSet(String.CASE_INSENSITIVE_ORDER); actualHeadersSet.addAll(actualHeaders); return actualHeadersSet.containsAll(aHeaders); } private List effectiveExposeHeaders(CrossOriginResourceSharing ann) { List actualExposeHeaders = null; if (ann != null) { actualExposeHeaders = Arrays.asList(ann.exposeHeaders()); } else { actualExposeHeaders = exposeHeaders; } return actualExposeHeaders; } private Integer effectiveMaxAge(CrossOriginResourceSharing ann) { if (ann != null) { int ma = ann.maxAge(); if (ma < 0) { return null; } else { return Integer.valueOf(ma); } } else { return maxAge; } } /** * Function called to grab a list of strings left behind by the input side. * @param m * @param key * @return */ @SuppressWarnings("unchecked") private List getHeadersFromInput(Message m, String key) { Object obj = m.getExchange().get(key); if (obj instanceof List) { return (List)obj; } return null; } /** * CORS uses one header containing space-separated values (Origin) and then * a raft of #field-name productions, which parse on commas and optional spaces. * @param m * @param key * @return */ private List getHeaderValues(String key, boolean spaceSeparated) { List values = headers.getRequestHeader(key); Pattern splitPattern; if (spaceSeparated) { splitPattern = SPACE_PATTERN; } else { splitPattern = FIELD_COMMA_PATTERN; } List results = new ArrayList(); if (values != null) { for (String value : values) { String[] items = splitPattern.split(value); for (String item : items) { results.add(item.trim()); } } } return results; } private void addHeaders(ResponseBuilder rb, String key, List values, boolean spaceSeparated) { String sb = concatValues(values, spaceSeparated); rb.header(key, sb); } private String concatValues(List values, boolean spaceSeparated) { StringBuffer sb = new StringBuffer(); for (int x = 0; x < values.size(); x++) { sb.append(values.get(x)); if (x != values.size() - 1) { if (spaceSeparated) { sb.append(" "); } else { sb.append(", "); } } } return sb.toString(); } /** * The origin strings to allow. An empty list allows all origins. * * @param allowedOrigins a list of case-sensitive origin strings. */ public void setAllowOrigins(List allowedOrigins) { this.allowOrigins = allowedOrigins; } /** @return the list of allowed origins. */ public List getAllowOrigins() { return allowOrigins; } public List getAllowHeaders() { return allowHeaders; } /** * The list of allowed headers for preflight checks. Section 5.2.6 * * @param allowedHeaders a list of permitted headers. */ public void setAllowHeaders(List allowedHeaders) { this.allowHeaders = allowedHeaders; } public List getExposeHeaders() { return exposeHeaders; } public Integer getMaxAge() { return maxAge; } public boolean isAllowCredentials() { return allowCredentials; } /** * The value for the Access-Control-Allow-Credentials header. If false, no header is added. If true, the * header is added with the value 'true'. * * @param allowCredentials */ public void setAllowCredentials(boolean allowCredentials) { this.allowCredentials = allowCredentials; } /** * A list of non-simple headers to be exposed via Access-Control-Expose-Headers. * * @param exposeHeaders the list of (case-sensitive) header names. */ public void setExposeHeaders(List exposeHeaders) { this.exposeHeaders = exposeHeaders; } /** * The value for Access-Control-Max-Age. * * @param maxAge An integer 'delta-seconds' or null. If null, no header is added. */ public void setMaxAge(Integer maxAge) { this.maxAge = maxAge; } /** * Preflight error response status, default is 200. * * @param status HTTP status code. */ public void setPreflightErrorStatus(Integer status) { this.preflightFailStatus = status; } public boolean isDefaultOptionsMethodsHandlePreflight() { return defaultOptionsMethodsHandlePreflight; } /** * What to do when a preflight request comes along for a resource that has a handler method for * \@OPTIONS and there is no @{@link CrossResourceSharing}(localPreflight = val) * annotation on the method. If this is true, then the filter * defers to the resource class method. * If this is false, then this filter performs preflight processing. * @param defaultOptionsMethodsHandlePreflight true to defer to resource methods. */ public void setDefaultOptionsMethodsHandlePreflight(boolean defaultOptionsMethodsHandlePreflight) { this.defaultOptionsMethodsHandlePreflight = defaultOptionsMethodsHandlePreflight; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy