Author: bimargulies Date: Fri Dec 2 13:25:10 2011 New Revision: 1209463 URL: http://svn.apache.org/viewvc?rev=1209463&view=rev Log: CXF-3493: fix up much confusion, more tests pass.
Added: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java (with props) cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java (contents, props changed) - copied, changed from r1209425, cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java Removed: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java Modified: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml Modified: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java?rev=1209463&r1=1209462&r2=1209463&view=diff ============================================================================== --- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java (original) +++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java Fri Dec 2 13:25:10 2011 @@ -53,17 +53,17 @@ public @interface CrossOriginResourceSha * A list of permitted origins. This is ignored * if {@link #allowAllOrigins()} is true. */ - String[] allowOrigins(); + String[] allowOrigins() default { }; /** * A list of HTTP methods. This is used only for preflight, * and is only valid on a class. */ - String[] allowMethods(); + String[] allowMethods() default { }; /** * A list of headers that the client may include * in an actual request. */ - String[] allowHeaders(); + String[] allowHeaders() default { }; /** * If true, this resource will return * <pre>Access-Control-Allow-Credentials: true</pre> @@ -73,7 +73,7 @@ public @interface CrossOriginResourceSha * A list of headers to return in <tt> * Access-Control-Expose-Headers</tt>. */ - String[] exposeHeaders(); + String[] exposeHeaders() default { }; /** * The value to return in <tt>Access-Control-Max-Age</tt>. * If this is negative, then no header is returned. The default @@ -89,4 +89,10 @@ public @interface CrossOriginResourceSha * performs preflight processing. */ boolean localPreflight() default false; + + /** + * For use inside @{@link CrossOriginResourceSharingPaths}. The path to apply the + * policies to. + */ + String path() default ""; } Modified: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java?rev=1209463&r1=1209462&r2=1209463&view=diff ============================================================================== --- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java (original) +++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java Fri Dec 2 13:25:10 2011 @@ -19,9 +19,11 @@ package org.apache.cxf.jaxrs.cors; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; @@ -33,6 +35,7 @@ import org.apache.cxf.jaxrs.ext.RequestH import org.apache.cxf.jaxrs.ext.ResponseHandler; import org.apache.cxf.jaxrs.model.ClassResourceInfo; import org.apache.cxf.jaxrs.model.OperationResourceInfo; +import org.apache.cxf.jaxrs.utils.HttpUtils; import org.apache.cxf.message.Message; /** @@ -41,9 +44,19 @@ import org.apache.cxf.message.Message; * 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 and implementing this inside your service. + * + * This class will perform preflight processing even if there is a resource method annotated + * to handle @OPTIONS, + * <em>unless</em> that method is annotated as follows: + * <pre> + * @CrossOriginResourceSharing(localPreflight = true) + * </pre> + * or unless the <tt>defaultOptionsMethodsHandlePreflight</tt> property of this class is set to <tt>true</tt>. */ public class CrossOriginResourceSharingFilter implements RequestHandler, ResponseHandler { - + private static final Pattern SPACE_PATTERN = Pattern.compile(" "); + private static final Pattern FIELD_COMMA_PATTERN = Pattern.compile(",\\w*"); + @Context private HttpHeaders headers; @@ -58,8 +71,12 @@ public class CrossOriginResourceSharingF private boolean allowCredentials; private List<String> exposeHeaders = Collections.emptyList(); private Integer maxAge; + private boolean defaultOptionsMethodsHandlePreflight; private CrossOriginResourceSharing getAnnotation(OperationResourceInfo ori) { + if (ori == null) { + return null; + } return ReflectionUtil.getAnnotationForMethodOrContainingClass(ori.getAnnotatedMethod(), CrossOriginResourceSharing.class); } @@ -69,37 +86,41 @@ public class CrossOriginResourceSharingF CrossOriginResourceSharing annotation = getAnnotation(opResInfo); if ("OPTIONS".equals(m.get(Message.HTTP_REQUEST_METHOD))) { - // what if someone wants to use options for something else, and also for preflight? - // in that case, they set the localPreflight flag, and we bow out. - if (opResInfo != null && (annotation == null || annotation.localPreflight())) { - return null; // continue handling - } - return preflightRequest(m, annotation, resourceClass); + + return preflightRequest(m, annotation, opResInfo, resourceClass); } return simpleRequest(m, annotation); } private Response simpleRequest(Message m, CrossOriginResourceSharing ann) { - List<String> values = headers.getRequestHeader(CorsHeaderConstants.HEADER_ORIGIN); + List<String> 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 (!effectiveAllowAllOrigins(ann) && !effectiveAllowOrigins(ann).containsAll(values)) { return null; } + + String originResponse; // 5.1.3 credentials lives in the output filter // in any case if (effectiveAllowAllOrigins(ann)) { - m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, Arrays.asList(new String[] { - "*" - })); + originResponse = "*"; } else { - m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, values); + originResponse = concatValues(values, true); } - // 5.1.4 expose headers lives on the output side. + // handle 5.1.3 + commonRequestProcessing(m, ann, originResponse); + + // 5.1.4 + List<String> effectiveExposeHeaders = effectiveExposeHeaders(ann); + if (effectiveExposeHeaders != null && effectiveExposeHeaders.size() != 0) { + m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, effectiveExposeHeaders); + } // note what kind of processing we're doing. m.getExchange().put(CrossOriginResourceSharingFilter.class.getName(), "simple"); @@ -109,47 +130,84 @@ public class CrossOriginResourceSharingF /** * 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 optionAnn, - ClassResourceInfo resourceClass) { + OperationResourceInfo opResInfo, ClassResourceInfo resourceClass) { + /* - * CORS doesn't send enough information with a preflight to accurately identity the single method - * that will handle the request. So the code uses annotations from the containing class, - * only. + * 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. */ - CrossOriginResourceSharing ann - = resourceClass.getResourceClass().getAnnotation(CrossOriginResourceSharing.class); + if (opResInfo != null && ((optionAnn == null && defaultOptionsMethodsHandlePreflight) + || (optionAnn != null && optionAnn.localPreflight()))) { + return null; // let the resource method take all responsibility. + } - List<String> values = headers.getRequestHeader(CorsHeaderConstants.HEADER_ORIGIN); + List<String> headerOriginValues = getHeaderValues(CorsHeaderConstants.HEADER_ORIGIN, true); String origin; // 5.2.1 -- must have origin, must have one origin. - if (values == null || values.size() != 1) { - return null; - } - origin = values.get(0); - // 5.2.2 must be on the list or we must be matching *. - boolean effectiveAllowAllOrigins = effectiveAllowAllOrigins(ann); - if (!effectiveAllowAllOrigins && !effectiveAllowOrigins(ann).contains(origin)) { + if (headerOriginValues == null || headerOriginValues.size() != 1) { return null; } + origin = headerOriginValues.get(0); - values = headers.getRequestHeader(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD); + List<String> requestMethodValues = getHeaderValues(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD, false); // 5.2.3 must have access-control-request-method, must be single-valued // we should reject parse errors but we cannot. - if (values == null || values.size() != 1) { + if (requestMethodValues == null || requestMethodValues.size() != 1) { return null; } + String requestMethod = requestMethodValues.get(0); - String requestMethod = values.get(0); + /* + * CORS doesn't send enough information with a preflight to accurately identity the single method + * that will handle the request. CrossOriginResourceSharingPaths provides annotations by path/method + * for this case. If none of those apply, a plain class level CrossOrginResourceSharing is the + * best we can do. + */ + String requestUri = HttpUtils.getPathToMatch(m, true); + CrossOriginResourceSharing ann = null; + CrossOriginResourceSharingPaths classPathsAnn = + resourceClass.getResourceClass().getAnnotation(CrossOriginResourceSharingPaths.class); + if (classPathsAnn != null) { + /* search the path/method pair. */ + for (CrossOriginResourceSharing pathAnn : classPathsAnn.value()) { + /* A very simple path policy! If someone wants to turn this into + * searching up the tree, they are welcome. + */ + if (pathAnn.path() != null && pathAnn.path().equals(requestUri) + && Arrays.asList(pathAnn.allowMethods()).contains(requestMethod)) { + ann = pathAnn; + break; + } + } + } + if (ann == null) { + ann = resourceClass.getResourceClass().getAnnotation(CrossOriginResourceSharing.class); + } + + // 5.2.2 must be on the list or we must be matching *. + boolean effectiveAllowAllOrigins = effectiveAllowAllOrigins(ann); + if (!effectiveAllowAllOrigins && !effectiveAllowOrigins(ann).contains(origin)) { + return null; + } // 5.2.4 get list of request headers. we should reject parse errors but we cannot. - List<String> requestHeaders = headers.getRequestHeader(CorsHeaderConstants.HEADER_AC_REQUEST_HEADERS); + List<String> requestHeaders = getHeaderValues(CorsHeaderConstants.HEADER_AC_REQUEST_HEADERS, false); // 5.2.5 reject if the method is not on the list. List<String> effectiveAllowMethods = effectiveAllowMethods(ann); @@ -164,77 +222,82 @@ public class CrossOriginResourceSharingF } // 5.2.7: add allow credentials and allow-origin as required: this lives in the Output filter + String originResponse; if (effectiveAllowAllOrigins(ann)) { - m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, Arrays.asList(new String[] { - "*" - })); + originResponse = "*"; } else { - m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, origin); + originResponse = origin; } - // 5.2.8 max-age lives in the output filter. // 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(new String[] { - requestMethod - })); + 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); + m.getExchange().put(CrossOriginResourceSharingFilter.class.getName(), "preflight"); // and allow things to proceed to the output filter. return Response.ok().build(); } + //CHECKSTYLE:ON + + 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) { return response; // we're not here. } - CrossOriginResourceSharing annotation; - List<String> originHeader = getHeadersFromInput(m, CorsHeaderConstants.HEADER_ORIGIN); ResponseBuilder rbuilder = Response.fromResponse(response); + + /* Common to simple and preflight */ + rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, + (String)m.getExchange().get(CorsHeaderConstants.HEADER_ORIGIN)); + rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS, + Boolean.toString(allowCredentials)); + if ("simple".equals(op)) { - annotation = getAnnotation(ori); - // 5.1.3: add Allow-Origin supplied from the input side, plus allow-credentials as requested - addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, originHeader); - rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS, - Boolean.toString(effectiveAllowCredentials(annotation))); - // 5.1.4 add allowed headers - List<String> rqAllowedHeaders = getHeadersFromInput(m, - CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS); - if (rqAllowedHeaders != null) { - addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS, rqAllowedHeaders); - } - - List<String> effectiveExposeHeaders = effectiveExposeHeaders(annotation); - if (effectiveExposeHeaders.size() > 0) { - addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS, effectiveExposeHeaders); + /* 5.1.4 expose headers */ + List<String> 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 { - annotation = ori.getAnnotatedMethod().getDeclaringClass() - .getAnnotation(CrossOriginResourceSharing.class); - // preflight - // 5.2.7 add Allow-Origin supplied from the input side, plus allow-credentials as requested - addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, originHeader); - rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS, - Boolean.toString(allowCredentials)); // 5.2.8 max-age - if (effectiveMaxAge(annotation) != null) { - rbuilder.header(CorsHeaderConstants.HEADER_AC_MAX_AGE, - effectiveMaxAge(annotation).toString()); + 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)); + getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS), false); // 5.2.10 add allowed headers List<String> rqAllowedHeaders = getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS); if (rqAllowedHeaders != null) { - addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, rqAllowedHeaders); + addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, rqAllowedHeaders, false); } return rbuilder.build(); @@ -313,6 +376,66 @@ public class CrossOriginResourceSharingF 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<String> getHeadersFromInput(Message m, String key) { + Object obj = m.getExchange().get(key); + if (obj instanceof List<?>) { + return (List<String>)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<String> getHeaderValues(String key, boolean spaceSeparated) { + List<String> values = headers.getRequestHeader(key); + Pattern splitPattern; + if (spaceSeparated) { + splitPattern = SPACE_PATTERN; + } else { + splitPattern = FIELD_COMMA_PATTERN; + } + List<String> results = new ArrayList<String>(); + for (String value : values) { + String[] items = splitPattern.split(value); + for (String item : items) { + results.add(item); + } + } + return results; + } + + private void addHeaders(ResponseBuilder rb, String key, List<String> values, boolean spaceSeparated) { + String sb = concatValues(values, spaceSeparated); + rb.header(key, sb); + } + + private String concatValues(List<String> 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. Call {@link #setAllowAllOrigins(boolean)} to enable '*'. @@ -330,8 +453,9 @@ public class CrossOriginResourceSharingF /** * Whether to implement Access-Control-Allow-Origin: * * - * @param allowAllOrigins if true, all origins are accepted and * is returned in the header. Sections - * 5.1.1 and 5.1.2, and 5.2.1 and 5.2.2. If false, then the list of allowed origins must be + * @param allowAllOrigins if true, all origins are accepted and + * "*" is returned in the header. Sections + * 5.1.1 and 5.1.2, and 5.2.1 and 5.2.2. If false, then the list of allowed origins must be */ public void setAllowAllOrigins(boolean allowAllOrigins) { this.allowAllOrigins = allowAllOrigins; @@ -403,19 +527,21 @@ public class CrossOriginResourceSharingF this.maxAge = maxAge; } - @SuppressWarnings("unchecked") - List<String> getHeadersFromInput(Message m, String key) { - Object obj = m.getExchange().get(key); - if (obj instanceof List<?>) { - return (List<String>)obj; - } - return null; + + public boolean isDefaultOptionsMethodsHandlePreflight() { + return defaultOptionsMethodsHandlePreflight; } - private void addHeaders(ResponseBuilder rb, String key, List<String> vals) { - for (String v : vals) { - rb.header(key, v); - } + /** + * What to do when a preflight request comes along for a resource that has a handler method for + * \@OPTIONS and there is no <tt>@{@link CrossResourceSharing}(localPreflight = val)</tt> + * annotation on the method. If this is <tt>true</tt>, 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; } } Added: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java?rev=1209463&view=auto ============================================================================== --- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java (added) +++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java Fri Dec 2 13:25:10 2011 @@ -0,0 +1,42 @@ +/** + * 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.jaxrs.cors; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate a JAX-RS class to provide pre-flight access control options + * based on the CORS standard's definition of a resource for access + * control purposes: a URL + method. Each @CrossScriptOrignResourceSharing + * annotation in here should contain a <tt>path</tt> attribute to define the + * path that it applies to. The <tt>allowedMethods</tt> attribute defines + * the method or methods that the policy options apply to. + */ +@Target({ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CrossOriginResourceSharingPaths { + /** + * The individual annotations. + */ + CrossOriginResourceSharing[] value(); +} Propchange: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Modified: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java URL: http://svn.apache.org/viewvc/cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java?rev=1209463&r1=1209462&r2=1209463&view=diff ============================================================================== --- cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java (original) +++ cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java Fri Dec 2 13:25:10 2011 @@ -21,6 +21,7 @@ package org.apache.cxf.systest.jaxrs.cor import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.apache.cxf.helpers.IOUtils; @@ -36,6 +37,7 @@ import org.apache.http.client.ClientProt import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; import org.apache.http.impl.client.DefaultHttpClient; import org.junit.Before; @@ -82,14 +84,17 @@ public class CrossOriginSimpleTest exten private void assertAllOrigin(boolean allOrigins, String[] originList, String[] requestOrigins, boolean permitted) throws ClientProtocolException, IOException { - connfigureAllowOrigins(allOrigins, originList); + configureAllowOrigins(allOrigins, originList); HttpClient httpclient = new DefaultHttpClient(); - HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/test/simpleGet/HelloThere"); + HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/simpleGet/HelloThere"); if (requestOrigins != null) { + StringBuffer ob = new StringBuffer(); for (String requestOrigin : requestOrigins) { - httpget.addHeader("Origin", requestOrigin); + ob.append(requestOrigin); + ob.append(" "); // extra trailing space won't hurt. } + httpget.addHeader("Origin", ob.toString()); } HttpResponse response = httpclient.execute(httpget); assertEquals(200, response.getStatusLine().getStatusCode()); @@ -110,9 +115,10 @@ public class CrossOriginSimpleTest exten assertEquals("*", aaoHeaders[0].getValue()); } else { List<String> ovalues = headerValues(aaoHeaders); - assertEquals(requestOrigins.length, ovalues.size()); + assertEquals(1, ovalues.size()); // get back one ac-allow-origin header. + String[] origins = ovalues.get(0).split(" +"); for (int x = 0; x < requestOrigins.length; x++) { - assertEquals(requestOrigins[x], ovalues.get(x)); + assertEquals(requestOrigins[x], origins[x]); } } } else { @@ -121,7 +127,7 @@ public class CrossOriginSimpleTest exten } } - private void connfigureAllowOrigins(boolean allOrigins, String[] originList) { + private void configureAllowOrigins(boolean allOrigins, String[] originList) { if (allOrigins) { originList = new String[0]; } @@ -208,14 +214,12 @@ public class CrossOriginSimpleTest exten assertEquals("ok", r); HttpClient httpclient = new DefaultHttpClient(); - HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/test/simpleGet/HelloThere"); + HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/simpleGet/HelloThere"); httpget.addHeader("Origin", "http://localhost:" + PORT); HttpResponse response = httpclient.execute(httpget); assertEquals(200, response.getStatusLine().getStatusCode()); - Header[] aaoHeaders = response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS); - assertEquals(1, aaoHeaders.length); - assertEquals("true", aaoHeaders[0].getValue()); + assertAllowCredentials(response, true); } @Test @@ -225,33 +229,79 @@ public class CrossOriginSimpleTest exten assertEquals("ok", r); HttpClient httpclient = new DefaultHttpClient(); - HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/test/simpleGet/HelloThere"); + HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/simpleGet/HelloThere"); httpget.addHeader("Origin", "http://localhost:" + PORT); HttpResponse response = httpclient.execute(httpget); assertEquals(200, response.getStatusLine().getStatusCode()); - Header[] aaoHeaders = response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS); - assertEquals(1, aaoHeaders.length); - assertEquals("false", aaoHeaders[0].getValue()); + assertAllowCredentials(response, false); } @Test public void testNonSimpleActualRequest() throws Exception { - connfigureAllowOrigins(true, null); + configureAllowOrigins(true, null); String r = configClient.replacePath("/setAllowCredentials/false") .accept("text/plain").post(null, String.class); assertEquals("ok", r); HttpClient httpclient = new DefaultHttpClient(); - HttpDelete httpdelete = new HttpDelete("http://localhost:" + PORT + "/test/delete"); + HttpDelete httpdelete = new HttpDelete("http://localhost:" + PORT + "/untest/delete"); httpdelete.addHeader("Origin", "http://localhost:" + PORT); HttpResponse response = httpclient.execute(httpdelete); assertEquals(200, response.getStatusLine().getStatusCode()); + assertAllowCredentials(response, false); + assertOriginResponse(true, null, true, response); + } + + private void assertAllowCredentials(HttpResponse response, boolean correct) { Header[] aaoHeaders = response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS); assertEquals(1, aaoHeaders.length); - assertEquals("false", aaoHeaders[0].getValue()); - assertOriginResponse(true, null, true, response); + assertEquals(Boolean.toString(correct), aaoHeaders[0].getValue()); + } + + @Test + public void testAnnotatedSimple() throws Exception { + configureAllowOrigins(true, null); + String r = configClient.replacePath("/setAllowCredentials/false") + .accept("text/plain").post(null, String.class); + assertEquals("ok", r); + HttpClient httpclient = new DefaultHttpClient(); + HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/annotatedGet/HelloThere"); + // this is the origin we expect to get. + httpget.addHeader("Origin", "http://area51.mil:31415"); + HttpResponse response = httpclient.execute(httpget); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertOriginResponse(false, new String[]{"http://area51.mil:31415"}, true, response); + assertAllowCredentials(response, false); + List<String> exposeHeadersValues + = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS)); + assertEquals(Arrays.asList(new String[] {"X-custom-3", "X-custom-4" }), exposeHeadersValues); + } + + @Test + public void testAnnotatedMethodPreflight() throws Exception { + configureAllowOrigins(true, null); + String r = configClient.replacePath("/setAllowCredentials/false") + .accept("text/plain").post(null, String.class); + assertEquals("ok", r); + HttpClient httpclient = new DefaultHttpClient(); + HttpOptions http = new HttpOptions("http://localhost:" + PORT + "/untest/annotatedPut"); + // this is the origin we expect to get. + http.addHeader("Origin", "http://area51.mil:31415"); + http.addHeader(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD, "PUT"); + http.addHeader(CorsHeaderConstants.HEADER_AC_REQUEST_HEADERS, "X-custom-1, X-custom-2"); + HttpResponse response = httpclient.execute(http); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertOriginResponse(false, new String[]{"http://area51.mil:31415"}, true, response); + assertAllowCredentials(response, false); + List<String> exposeHeadersValues + = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS)); + // depend on knowing the order. + assertEquals(Arrays.asList(new String[] {"X-custom-3", "X-custom-4" }), exposeHeadersValues); + List<String> allowHeadersValues + = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS)); + assertEquals(Arrays.asList(new String[] {"X-custom-1", "X-custom-2" }), allowHeadersValues); } @Ignore Copied: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java (from r1209425, cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java) URL: http://svn.apache.org/viewvc/cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java?p2=cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java&p1=cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java&r1=1209425&r2=1209463&rev=1209463&view=diff ============================================================================== --- cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java (original) +++ cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java Fri Dec 2 13:25:10 2011 @@ -19,17 +19,31 @@ package org.apache.cxf.systest.jaxrs.cors; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import org.apache.cxf.jaxrs.cors.CrossOriginResourceSharing; +import org.apache.cxf.jaxrs.cors.CrossOriginResourceSharingPaths; + /** - * + * Service bean with no class-level annotation for cross-script control. */ -public class CorsServer { +@CrossOriginResourceSharingPaths( + @CrossOriginResourceSharing(path = "/annotatedPut", + allowOrigins = { "http://area51.mil:31415" }, + allowCredentials = true, + maxAge = 1, + allowMethods = { "PUT" }, + allowHeaders = { "X-custom-1", "X-custom-2" }, + exposeHeaders = {"X-custom-3", "X-custom-4" } + )) +public class UnannotatedCorsServer { @GET @Produces("text/plain") @@ -37,10 +51,34 @@ public class CorsServer { public String simpleGet(@PathParam("echo") String echo) { return echo; } - + @DELETE @Path("/delete") public Response deleteSomething() { return Response.ok().build(); } + + @GET + @CrossOriginResourceSharing(allowOrigins = { + "http://area51.mil:31415" }, + allowCredentials = true, + exposeHeaders = {"X-custom-3", "X-custom-4" }) + @Produces("text/plain") + @Path("/annotatedGet/{echo}") + public String annotatedGet(@PathParam("echo") String echo) { + return echo; + } + + /** + * A method annotated to test preflight. + * @param input + * @return + */ + @PUT + @Consumes("text/plain") + @Produces("text/plain") + @Path("/annotatedPut") + public String annotatedPut(String input) { + return input; + } } Propchange: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Modified: cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml URL: http://svn.apache.org/viewvc/cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml?rev=1209463&r1=1209462&r2=1209463&view=diff ============================================================================== --- cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml (original) +++ cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml Fri Dec 2 13:25:10 2011 @@ -25,17 +25,17 @@ http://cxf.apache.org/core <property name="allowAllOrigins" value="true" /> </bean> - <jaxrs:server id="cors-service" address="/test"> + <jaxrs:server id="cors-service" address="/untest"> <jaxrs:serviceBeans> <ref bean="cors-server" /> </jaxrs:serviceBeans> <jaxrs:providers> <ref bean="cors-filter" /> - </jaxrs:providers><!-- + </jaxrs:providers> <jaxrs:features> <cxf:logging /> </jaxrs:features> - --> + </jaxrs:server> <jaxrs:server id="config-service" address="/config"> <jaxrs:serviceBeans> @@ -50,5 +50,5 @@ http://cxf.apache.org/core <property name='inputFilter' ref='cors-filter'/> </bean> <bean id="cors-server" scope="prototype" - class="org.apache.cxf.systest.jaxrs.cors.CorsServer" /> + class="org.apache.cxf.systest.jaxrs.cors.UnannotatedCorsServer" /> </beans>