Author: bimargulies Date: Fri Dec 2 17:30:42 2011 New Revision: 1209596 URL: http://svn.apache.org/viewvc?rev=1209596&view=rev Log: More tests, plus some substantive documentation. (CXF-3493)
Added: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/package.html (with props) cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/AnnotatedCorsServer.java (with props) Modified: 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 Added: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/package.html URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/package.html?rev=1209596&view=auto ============================================================================== --- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/package.html (added) +++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/package.html Fri Dec 2 17:30:42 2011 @@ -0,0 +1,83 @@ +<!DOCTYPE HTML > +<html> +<head> +<!-- + 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. +--> +</head> +<body bgcolor="white"> +<h1>CORS</h1> +<p>This package provides a filter to assist applications in implementing Cross Origin Resource Sharing, +as described in the <a href="http://www.w3.org/TR/cors">CORS specification</a>. +</p> +<h2>CORS Access Model</h2> +<p> +CORS exists to protect web servers from unexpected cross-origin access. The premise of CORS is that many web resources +are deployed by people who don't want to permit cross-origin access, but who couldn't detect it or didn't bother +to control it. Thus, CORS defines a set of restrictions <em>implemented on the client</em> that, by default, +prohibit cross-origin access. +</p> +<p> +If you want your service to permit cross-origin access, your service must return additional headers to the client to reassure +it that you really want to permit the access. {@link CrossOriginResourceSharingFilter} adds these headers to your service's +responses based on rules that you configure. +</p> +<h2>CORS Resource Model (versus JAX-RS)</h2> +<p> +CORS and JAX-RS differ, fundamentally, in how they define a resource for access control purposes. In CORS, a resource +is defined by the combination of URI and HTTP method. Once a client has obtained access information for a URI+METHOD, +it may cache it. JAX-RS, on the other hand, defines a resource as: +<ul> +<li>URI</li> +<li>method</li> +<li>Accepts</li> +<li>Accepts-Language</li> +<li>Content-Type</li> +</ul> +The logical place, in other words, to specify CORS policy in a JAX-RS application is at the level of an annotated method. However, each method is +applied to the narrow 'resource' defined by the list above, not just the URI+Method pair. This will motivate the annotation model below. +</p> +<h2>Simple and Preflight requests</h2> +<p>The CORS specification differentiates two kinds of HTTP requests: <em>simple</em> and <em>not simple</em>. (See the specification +for the definition.) For a simple request, the client simply +sends the request to the service, and then looks for the <tt>Access-Control-</tt> headers to indicate whether the server has explicitly granted +cross-origin access. For a non-simple request, the client sends a so-called <em>preflight</em> request and waits for a response before +issuing the original request. +<h2>Configuration via Annotation</h2> +<p> +One way to control the behavior of the filter is the @{@link CrossOriginResourceSharing} annotation on a method. +This is a complete solution for simple requests. You can specify all of the controls. However, if you have non-simple methods, the mismatch on +resource access models above makes it impossible for CXF to map the OPTIONS request that will arrive to the correct method. +</p> +<p> +If all the methods of a class can share a common policy, you can attach a single @{@link CrossOriginResourceSharing} +to a resource class, and it will apply to all the resource implied by all of the methods. +</p> +<p> +If you need finer control, you can use @{@link CrossOriginResourceSharingPaths} at the class level. This annotation contains a list of +@{@link CrossOriginResourceSharing} annotations. In the nested annotations, you add <tt>path = "/localResourcePath"</tt> to associate the +annotation with a path. The filter will find the annotation based on path, and then allow (or forbid) based on the method +as compared to the <tt>allowedMethods</tt>. +</p> +<h2>Bean Configuration</h2> +<p> +The simplest configuration applies when you want to apply the same configuration to all of your resources. In this case, you can +use the properties of {@link CrossOriginResourceSharingFilter} to specify the policy. +</p> +</body> +</html> \ No newline at end of file Propchange: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/package.html ------------------------------------------------------------------------------ svn:eol-style = native Propchange: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/package.html ------------------------------------------------------------------------------ svn:mime-type = text/html Added: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/AnnotatedCorsServer.java URL: http://svn.apache.org/viewvc/cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/AnnotatedCorsServer.java?rev=1209596&view=auto ============================================================================== --- cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/AnnotatedCorsServer.java (added) +++ cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/AnnotatedCorsServer.java Fri Dec 2 17:30:42 2011 @@ -0,0 +1,104 @@ +/** + * 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.systest.jaxrs.cors; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; +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.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.apache.cxf.jaxrs.cors.CorsHeaderConstants; +import org.apache.cxf.jaxrs.cors.CrossOriginResourceSharing; + +/** + * Service bean with no class-level annotation for cross-script control. + */ +@CrossOriginResourceSharing(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 AnnotatedCorsServer { + @Context + private HttpHeaders headers; + + @GET + @Produces("text/plain") + @Path("/simpleGet/{echo}") + public String simpleGet(@PathParam("echo") String echo) { + return echo; + } + + @DELETE + @Path("/delete") + public Response deleteSomething() { + return Response.ok().build(); + } + + @OPTIONS + @Path("/delete") + @CrossOriginResourceSharing(localPreflight = true) + public Response deleteOptions() { + String origin = headers.getRequestHeader("Origin").get(0); + if ("http://area51.mil:3333".equals(origin)) { + return Response.ok().header(CorsHeaderConstants.HEADER_AC_ALLOW_METHODS, "DELETE PUT") + .header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS, "false") + .header(CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, "http://area51.mil:3333").build(); + } else { + 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/AnnotatedCorsServer.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/AnnotatedCorsServer.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=1209596&r1=1209595&r2=1209596&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 17:30:42 2011 @@ -47,8 +47,7 @@ import org.junit.Ignore; import org.junit.Test; /** - * Unit tests for simple CORS requests. Simple requests traffic only in allowed origins, - * allowed credentials, and exposed headers. + * Unit tests for CORS. This isn't precisely simple as it's turned out. * * Note that it's not the server's job to detect invalid CORS requests. If a client * fails to preflight, it's just not our job. However, also note that all 'actual' @@ -304,6 +303,79 @@ public class CrossOriginSimpleTest exten = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS)); assertEquals(Arrays.asList(new String[] {"X-custom-1", "X-custom-2" }), allowHeadersValues); } + + @Test + public void testAnnotatedClassCorrectOrigin() throws Exception { + HttpClient httpclient = new DefaultHttpClient(); + HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/antest/simpleGet/HelloThere"); + httpget.addHeader("Origin", "http://area51.mil:31415"); + + HttpResponse response = httpclient.execute(httpget); + assertEquals(200, response.getStatusLine().getStatusCode()); + HttpEntity entity = response.getEntity(); + String e = IOUtils.toString(entity.getContent(), "utf-8"); + + assertEquals("HelloThere", e); // ensure that we didn't bust the operation itself. + assertOriginResponse(false, new String[] {"http://area51.mil:31415" }, true, response); + } + + @Test + public void testAnnotatedClassWrongOrigin() throws Exception { + HttpClient httpclient = new DefaultHttpClient(); + HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/antest/simpleGet/HelloThere"); + httpget.addHeader("Origin", "http://su.us:1001"); + + HttpResponse response = httpclient.execute(httpget); + assertEquals(200, response.getStatusLine().getStatusCode()); + HttpEntity entity = response.getEntity(); + String e = IOUtils.toString(entity.getContent(), "utf-8"); + + assertEquals("HelloThere", e); + assertOriginResponse(false, null, false, response); + } + + @Test + public void testAnnotatedLocalPreflight() 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 + "/antest/delete"); + // this is the origin we expect to get. + http.addHeader("Origin", "http://area51.mil:3333"); + http.addHeader(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD, "DELETE"); + HttpResponse response = httpclient.execute(http); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertOriginResponse(false, new String[]{"http://area51.mil:3333"}, true, response); + assertAllowCredentials(response, false); + List<String> exposeHeadersValues + = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS)); + // preflight never returns Expose-Headers + assertEquals(Collections.emptyList(), exposeHeadersValues); + List<String> allowedMethods + = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_METHODS)); + assertEquals(Arrays.asList("DELETE PUT"), allowedMethods); + } + + @Test + public void testAnnotatedLocalPreflightNoGo() 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 + "/antest/delete"); + // this is the origin we expect to get. + http.addHeader("Origin", "http://area51.mil:4444"); + http.addHeader(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD, "DELETE"); + HttpResponse response = httpclient.execute(http); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertOriginResponse(false, new String[]{"http://area51.mil:4444"}, false, response); + // we could check that the others are also missing. + } @Ignore public static class SpringServer extends AbstractSpringServer { 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=1209596&r1=1209595&r2=1209596&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 17:30:42 2011 @@ -25,18 +25,29 @@ http://cxf.apache.org/core <property name="allowAllOrigins" value="true" /> </bean> - <jaxrs:server id="cors-service" address="/untest"> + <jaxrs:server id="unann-cors-service" address="/untest"> <jaxrs:serviceBeans> - <ref bean="cors-server" /> + <ref bean="unann-cors-server" /> </jaxrs:serviceBeans> <jaxrs:providers> <ref bean="cors-filter" /> - </jaxrs:providers><!-- + </jaxrs:providers> + <!-- <jaxrs:features> <cxf:logging /> - </jaxrs:features>--> + </jaxrs:features> + --> + </jaxrs:server> + <jaxrs:server id="ann-cors-service" address="/antest"> + <jaxrs:serviceBeans> + <ref bean="ann-cors-server" /> + </jaxrs:serviceBeans> + <jaxrs:providers> + <ref bean="cors-filter" /> + </jaxrs:providers> </jaxrs:server> + <jaxrs:server id="config-service" address="/config"> <jaxrs:serviceBeans> <ref bean="config-server" /> @@ -49,6 +60,8 @@ http://cxf.apache.org/core <bean id="config-server" class="org.apache.cxf.systest.jaxrs.cors.ConfigServer"> <property name='inputFilter' ref='cors-filter'/> </bean> - <bean id="cors-server" scope="prototype" + <bean id="unann-cors-server" scope="prototype" class="org.apache.cxf.systest.jaxrs.cors.UnannotatedCorsServer" /> + <bean id="ann-cors-server" scope="prototype" + class="org.apache.cxf.systest.jaxrs.cors.AnnotatedCorsServer" /> </beans>