From: Madhu Venugopal Date: Wed, 28 Aug 2013 05:26:13 +0000 (-0700) Subject: Added CorsFilter to enable secure cross site scripting X-Git-Tag: releasepom-0.1.0~158 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=commitdiff_plain;h=8cbcc63bbb004b50c66ce3c65d0b8d7943c8ffac Added CorsFilter to enable secure cross site scripting This is in addition to Ed's original Cors Filter changes. Default Cors Config doesnt seem to work in certain scenarios. Added some custom configurations and also added it per-bundle (started with Flow & i will add it to other bundles once this is verified). Also, by default AngularJS like frameworks uses HTTP OPTIONS method to check for server options and that doesnt carry authentication headers. Hence in order for the cors to work properly, we have to ignore authentication for OPTIONS method alone. This is taken care in the web.xml configuration for all the northbound bundle. See: http://tomcat.apache.org/tomcat-7.0-doc/config/filter.html#CORS_Filter And: http://en.wikipedia.org/wiki/Cross-origin_resource_sharing This is done to allow a web page using javascript to be able to make calls to our REST APIs even though it does not originate in our domain. Added CorsFilter bundle in Third Party to bring in the class as a Fragment on the org.apache.catalina bundle. Added CorsFilter to the web/root web.xml file so it will be used for all WebApps. Change-Id: I5fc6a53f2046816984fab722b841730c0eee396a Signed-off-by: Madhu Venugopal --- diff --git a/opendaylight/distribution/opendaylight/pom.xml b/opendaylight/distribution/opendaylight/pom.xml index abf508efc2..4dc4e6fa2e 100644 --- a/opendaylight/distribution/opendaylight/pom.xml +++ b/opendaylight/distribution/opendaylight/pom.xml @@ -68,6 +68,7 @@ ../../../third-party/net.sf.jung2 ../../../third-party/jersey-servlet ../../../third-party/commons/thirdparty + ../../../third-party/org.apache.catalina.filters.CorsFilter ../../sal/api diff --git a/opendaylight/northbound/flowprogrammer/pom.xml b/opendaylight/northbound/flowprogrammer/pom.xml index 94991c573e..201bf477ee 100644 --- a/opendaylight/northbound/flowprogrammer/pom.xml +++ b/opendaylight/northbound/flowprogrammer/pom.xml @@ -43,7 +43,8 @@ org.opendaylight.controller.northbound.commons.utils, org.opendaylight.controller.sal.authorization, org.opendaylight.controller.usermanager, - com.sun.jersey.spi.container.servlet, + com.sun.jersey.spi.container.servlet, + org.apache.catalina.filters, javax.ws.rs, javax.ws.rs.core, javax.xml.bind.annotation, @@ -96,5 +97,10 @@ com.sun.jersey.jersey-servlet 1.17-SNAPSHOT + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/flowprogrammer/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/flowprogrammer/src/main/resources/WEB-INF/web.xml index 4cedf2df89..5b3cec2292 100644 --- a/opendaylight/northbound/flowprogrammer/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/flowprogrammer/src/main/resources/WEB-INF/web.xml @@ -18,17 +18,56 @@ /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + @@ -48,4 +87,4 @@ BASIC opendaylight - \ No newline at end of file + diff --git a/opendaylight/northbound/hosttracker/pom.xml b/opendaylight/northbound/hosttracker/pom.xml index 40aaf462ad..b588c0715b 100644 --- a/opendaylight/northbound/hosttracker/pom.xml +++ b/opendaylight/northbound/hosttracker/pom.xml @@ -51,6 +51,7 @@ javax.xml.bind.annotation, javax.xml.bind, org.slf4j, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs /controller/nb/v2/host @@ -96,5 +97,10 @@ enunciate-core-annotations ${enunciate.version} + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/hosttracker/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/hosttracker/src/main/resources/WEB-INF/web.xml index 0fa8b95dd0..01b8fedce1 100644 --- a/opendaylight/northbound/hosttracker/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/hosttracker/src/main/resources/WEB-INF/web.xml @@ -17,17 +17,56 @@ /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/northbound/networkconfiguration/bridgedomain/pom.xml b/opendaylight/northbound/networkconfiguration/bridgedomain/pom.xml index cba14fdc78..8cb1320043 100644 --- a/opendaylight/northbound/networkconfiguration/bridgedomain/pom.xml +++ b/opendaylight/northbound/networkconfiguration/bridgedomain/pom.xml @@ -52,6 +52,7 @@ javax.ws.rs.core, javax.xml.bind.annotation, javax.xml.bind, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs @@ -104,5 +105,10 @@ com.sun.jersey.jersey-servlet 1.17-SNAPSHOT + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/networkconfiguration/bridgedomain/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/networkconfiguration/bridgedomain/src/main/resources/WEB-INF/web.xml index b7f35c3f96..f4de222acc 100644 --- a/opendaylight/northbound/networkconfiguration/bridgedomain/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/networkconfiguration/bridgedomain/src/main/resources/WEB-INF/web.xml @@ -17,17 +17,56 @@ /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - BridgeDomain Configuration NorthBound API - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/northbound/staticrouting/pom.xml b/opendaylight/northbound/staticrouting/pom.xml index bb88465907..fa9341f681 100644 --- a/opendaylight/northbound/staticrouting/pom.xml +++ b/opendaylight/northbound/staticrouting/pom.xml @@ -50,6 +50,7 @@ javax.ws.rs.core, javax.xml.bind.annotation, javax.xml.bind, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs @@ -92,5 +93,10 @@ com.sun.jersey.jersey-servlet 1.17-SNAPSHOT + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/staticrouting/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/staticrouting/src/main/resources/WEB-INF/web.xml index 4a040c1a1f..0bf186b50e 100644 --- a/opendaylight/northbound/staticrouting/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/staticrouting/src/main/resources/WEB-INF/web.xml @@ -13,21 +13,60 @@ - JAXRSStaticRouting - /* + JAXRSStaticRouting + /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/northbound/statistics/pom.xml b/opendaylight/northbound/statistics/pom.xml index cad50e2998..db4c4a9413 100644 --- a/opendaylight/northbound/statistics/pom.xml +++ b/opendaylight/northbound/statistics/pom.xml @@ -58,6 +58,7 @@ javax.xml.bind.annotation, javax.xml.bind, org.slf4j, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs @@ -100,5 +101,10 @@ enunciate-core-annotations ${enunciate.version} + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/statistics/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/statistics/src/main/resources/WEB-INF/web.xml index f152aa75a2..db0460ba56 100644 --- a/opendaylight/northbound/statistics/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/statistics/src/main/resources/WEB-INF/web.xml @@ -17,17 +17,56 @@ /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/northbound/subnets/pom.xml b/opendaylight/northbound/subnets/pom.xml index a3931beadd..43b8f9ebb0 100644 --- a/opendaylight/northbound/subnets/pom.xml +++ b/opendaylight/northbound/subnets/pom.xml @@ -65,6 +65,7 @@ javax.xml.bind, javax.xml.bind.annotation, org.slf4j, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs @@ -107,5 +108,10 @@ enunciate-core-annotations ${enunciate.version} + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/subnets/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/subnets/src/main/resources/WEB-INF/web.xml index f7eccef666..a5c70ee9d8 100644 --- a/opendaylight/northbound/subnets/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/subnets/src/main/resources/WEB-INF/web.xml @@ -16,17 +16,56 @@ JAXRSSubnets /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/northbound/switchmanager/pom.xml b/opendaylight/northbound/switchmanager/pom.xml index 556c964055..dd7ff9a75b 100644 --- a/opendaylight/northbound/switchmanager/pom.xml +++ b/opendaylight/northbound/switchmanager/pom.xml @@ -51,6 +51,7 @@ javax.xml.bind.annotation, javax.xml.bind, org.slf4j, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs /controller/nb/v2/switch @@ -102,6 +103,10 @@ enunciate-core-annotations ${enunciate.version} - + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/switchmanager/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/switchmanager/src/main/resources/WEB-INF/web.xml index 188b21b24d..ea6fcc99b2 100644 --- a/opendaylight/northbound/switchmanager/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/switchmanager/src/main/resources/WEB-INF/web.xml @@ -17,17 +17,56 @@ /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/northbound/topology/pom.xml b/opendaylight/northbound/topology/pom.xml index 726f7975f0..007bdbebdc 100644 --- a/opendaylight/northbound/topology/pom.xml +++ b/opendaylight/northbound/topology/pom.xml @@ -54,6 +54,7 @@ javax.xml.bind, javax.xml.bind.annotation, org.slf4j, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs /controller/nb/v2/topology @@ -94,5 +95,10 @@ com.sun.jersey.jersey-servlet 1.17-SNAPSHOT + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/northbound/topology/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/topology/src/main/resources/WEB-INF/web.xml index a46e433054..bc818c8c6d 100644 --- a/opendaylight/northbound/topology/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/northbound/topology/src/main/resources/WEB-INF/web.xml @@ -17,17 +17,56 @@ /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/samples/northbound/loadbalancer/pom.xml b/opendaylight/samples/northbound/loadbalancer/pom.xml index dc23940d55..ed078a0551 100644 --- a/opendaylight/samples/northbound/loadbalancer/pom.xml +++ b/opendaylight/samples/northbound/loadbalancer/pom.xml @@ -51,6 +51,7 @@ javax.xml.bind.annotation, javax.xml.bind, org.slf4j, + org.apache.catalina.filters, !org.codehaus.enunciate.jaxrs /one/nb/v2/lb @@ -96,5 +97,10 @@ enunciate-core-annotations ${enunciate.version} + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/samples/northbound/loadbalancer/src/main/resources/WEB-INF/web.xml b/opendaylight/samples/northbound/loadbalancer/src/main/resources/WEB-INF/web.xml index aac4647de6..0e8f8c1b56 100644 --- a/opendaylight/samples/northbound/loadbalancer/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/samples/northbound/loadbalancer/src/main/resources/WEB-INF/web.xml @@ -17,17 +17,56 @@ /* + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - NB api - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/opendaylight/web/root/pom.xml b/opendaylight/web/root/pom.xml index ed899c5b5c..809751d443 100644 --- a/opendaylight/web/root/pom.xml +++ b/opendaylight/web/root/pom.xml @@ -73,7 +73,8 @@ org.springframework.web.servlet.view.json, org.springframework.web.filter, org.springframework.web.context, - org.springframework.util + org.springframework.util, + org.apache.catalina.filters org.opendaylight.controller.web @@ -148,5 +149,10 @@ 3.2.1.RELEASE provided + + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + diff --git a/opendaylight/web/root/src/main/resources/WEB-INF/web.xml b/opendaylight/web/root/src/main/resources/WEB-INF/web.xml index 557b9c74f5..0c5cb3ac27 100644 --- a/opendaylight/web/root/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/web/root/src/main/resources/WEB-INF/web.xml @@ -6,28 +6,66 @@ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + - - free access - /js/* - /images/* - /css/* - /favicon.ico - + + free access + /js/* + /images/* + /css/* + /favicon.ico + - RootApp - - RootGUI - /* - - - System-Admin - Network-Admin - Network-Operator - Container-User - + RootApp + + RootGUI + /* + POST + GET + PUT + DELETE + HEAD + + + System-Admin + Network-Admin + Network-Operator + Container-User + diff --git a/third-party/org.apache.catalina.filters.CorsFilter/README b/third-party/org.apache.catalina.filters.CorsFilter/README new file mode 100644 index 0000000000..e2d22abe1a --- /dev/null +++ b/third-party/org.apache.catalina.filters.CorsFilter/README @@ -0,0 +1,12 @@ +See: http://tomcat.apache.org/tomcat-7.0-doc/config/filter.html#CORS_Filter +And: http://en.wikipedia.org/wiki/Cross-origin_resource_sharing +This is done to allow a web page using javascript to be able to make calls +to our REST APIs even though it does not originate in our domain. + +This bundle just rolls up org.apache.catalina.filters.CorsFilter and adds it as a +fragment to the org.apache.catalina bundle. + +The reason this is necessary is because the CorsFilter class was originally added +at Tomcat 7.0.42, and we are using 7.0.32. As the CorsFilter class is a simple one, +with very few dependencies, this seemed the best way to bring it in. + diff --git a/third-party/org.apache.catalina.filters.CorsFilter/pom.xml b/third-party/org.apache.catalina.filters.CorsFilter/pom.xml new file mode 100644 index 0000000000..0789d50443 --- /dev/null +++ b/third-party/org.apache.catalina.filters.CorsFilter/pom.xml @@ -0,0 +1,64 @@ + + + + + org.opendaylight.controller + commons.thirdparty + 1.1.0-SNAPSHOT + ../commons/thirdparty + + 4.0.0 + org.opendaylight.controller.thirdparty + org.apache.catalina.filters.CorsFilter + 7.0.42-SNAPSHOT + bundle + + + + org.apache.felix + maven-bundle-plugin + 2.3.6 + true + + + + org.apache.catalina + + + javax.servlet, + javax.servlet.http, + org.apache.catalina.filters, + org.apache.juli.logging, + org.apache.tomcat.util.res, + org.apache.catalina.comet, + org.apache.tomcat.util + + + ${project.basedir}/META-INF + + + + + + + equinoxSDK381 + javax.servlet + 3.0.0.v201112011016 + + + orbit + org.apache.juli.extras + 7.0.32.v201211081135 + + + orbit + org.apache.tomcat.util + 7.0.32.v201211201952 + + + orbit + org.apache.catalina + 7.0.32.v201211201336 + + + diff --git a/third-party/org.apache.catalina.filters.CorsFilter/src/main/java/org/apache/catalina/filters/CorsFilter.java b/third-party/org.apache.catalina.filters.CorsFilter/src/main/java/org/apache/catalina/filters/CorsFilter.java new file mode 100644 index 0000000000..8069c9939c --- /dev/null +++ b/third-party/org.apache.catalina.filters.CorsFilter/src/main/java/org/apache/catalina/filters/CorsFilter.java @@ -0,0 +1,1166 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.catalina.filters.Constants; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + *

+ * A {@link Filter} that enable client-side cross-origin requests by + * implementing W3C's CORS (Cross-Origin Resource + * Sharing) specification for resources. Each {@link HttpServletRequest} + * request is inspected as per specification, and appropriate response headers + * are added to {@link HttpServletResponse}. + *

+ * + *

+ * By default, it also sets following request attributes, that help to + * determine the nature of the request downstream. + *

    + *
  • cors.isCorsRequest: Flag to determine if the request is a CORS + * request. Set to true if a CORS request; false + * otherwise.
  • + *
  • cors.request.origin: The Origin URL, i.e. the URL of the page from + * where the request is originated.
  • + *
  • + * cors.request.type: Type of request. Possible values: + *
      + *
    • SIMPLE: A request which is not preceded by a pre-flight request.
    • + *
    • ACTUAL: A request which is preceded by a pre-flight request.
    • + *
    • PRE_FLIGHT: A pre-flight request.
    • + *
    • NOT_CORS: A normal same-origin request.
    • + *
    • INVALID_CORS: A cross-origin request which is invalid.
    • + *
    + *
  • + *
  • cors.request.headers: Request headers sent as + * 'Access-Control-Request-Headers' header, for pre-flight request.
  • + *
+ *

+ * + * @see CORS specification + * + */ +public final class CorsFilter implements Filter { + + private static final Log log = LogFactory.getLog(CorsFilter.class); + + private static final StringManager sm = + StringManager.getManager(Constants.Package); + + + /** + * A {@link Collection} of origins consisting of zero or more origins that + * are allowed access to the resource. + */ + private final Collection allowedOrigins; + + /** + * Determines if any origin is allowed to make request. + */ + private boolean anyOriginAllowed; + + /** + * A {@link Collection} of methods consisting of zero or more methods that + * are supported by the resource. + */ + private final Collection allowedHttpMethods; + + /** + * A {@link Collection} of headers consisting of zero or more header field + * names that are supported by the resource. + */ + private final Collection allowedHttpHeaders; + + /** + * A {@link Collection} of exposed headers consisting of zero or more header + * field names of headers other than the simple response headers that the + * resource might use and can be exposed. + */ + private final Collection exposedHeaders; + + /** + * A supports credentials flag that indicates whether the resource supports + * user credentials in the request. It is true when the resource does and + * false otherwise. + */ + private boolean supportsCredentials; + + /** + * Indicates (in seconds) how long the results of a pre-flight request can + * be cached in a pre-flight result cache. + */ + private long preflightMaxAge; + + /** + * Determines if the request should be decorated or not. + */ + private boolean decorateRequest; + + + public CorsFilter() { + this.allowedOrigins = new HashSet(); + this.allowedHttpMethods = new HashSet(); + this.allowedHttpHeaders = new HashSet(); + this.exposedHeaders = new HashSet(); + } + + + @Override + public void doFilter(final ServletRequest servletRequest, + final ServletResponse servletResponse, final FilterChain filterChain) + throws IOException, ServletException { + if (!(servletRequest instanceof HttpServletRequest) || + !(servletResponse instanceof HttpServletResponse)) { + throw new ServletException(sm.getString("corsFilter.onlyHttp")); + } + + // Safe to downcast at this point. + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Determines the CORS request type. + CorsFilter.CORSRequestType requestType = checkRequestType(request); + + // Adds CORS specific attributes to request. + if (decorateRequest) { + CorsFilter.decorateCORSProperties(request, requestType); + } + switch (requestType) { + case SIMPLE: + // Handles a Simple CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case ACTUAL: + // Handles an Actual CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case PRE_FLIGHT: + // Handles a Pre-flight CORS request. + this.handlePreflightCORS(request, response, filterChain); + break; + case NOT_CORS: + // Handles a Normal request that is not a cross-origin request. + this.handleNonCORS(request, response, filterChain); + break; + default: + // Handles a CORS request that violates specification. + this.handleInvalidCORS(request, response, filterChain); + break; + } + } + + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // Initialize defaults + parseAndStore(DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HTTP_METHODS, + DEFAULT_ALLOWED_HTTP_HEADERS, DEFAULT_EXPOSED_HEADERS, + DEFAULT_SUPPORTS_CREDENTIALS, DEFAULT_PREFLIGHT_MAXAGE, + DEFAULT_DECORATE_REQUEST); + + if (filterConfig != null) { + String configAllowedOrigins = filterConfig + .getInitParameter(PARAM_CORS_ALLOWED_ORIGINS); + String configAllowedHttpMethods = filterConfig + .getInitParameter(PARAM_CORS_ALLOWED_METHODS); + String configAllowedHttpHeaders = filterConfig + .getInitParameter(PARAM_CORS_ALLOWED_HEADERS); + String configExposedHeaders = filterConfig + .getInitParameter(PARAM_CORS_EXPOSED_HEADERS); + String configSupportsCredentials = filterConfig + .getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS); + String configPreflightMaxAge = filterConfig + .getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE); + String configDecorateRequest = filterConfig + .getInitParameter(PARAM_CORS_REQUEST_DECORATE); + + parseAndStore(configAllowedOrigins, configAllowedHttpMethods, + configAllowedHttpHeaders, configExposedHeaders, + configSupportsCredentials, configPreflightMaxAge, + configDecorateRequest); + } + } + + + /** + * Handles a CORS request of type {@link CORSRequestType}.SIMPLE. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + * @see Simple + * Cross-Origin Request, Actual Request, and Redirects + */ + protected void handleSimpleCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + + CorsFilter.CORSRequestType requestType = checkRequestType(request); + if (!(requestType == CorsFilter.CORSRequestType.SIMPLE || + requestType == CorsFilter.CORSRequestType.ACTUAL)) { + throw new IllegalArgumentException( + sm.getString("corsFilter.wrongType2", + CorsFilter.CORSRequestType.SIMPLE, + CorsFilter.CORSRequestType.ACTUAL)); + } + + final String origin = request + .getHeader(CorsFilter.REQUEST_HEADER_ORIGIN); + final String method = request.getMethod(); + + // Section 6.1.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + if (!allowedHttpMethods.contains(method)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.1.3 + // Add a single Access-Control-Allow-Origin header. + if (anyOriginAllowed && !supportsCredentials) { + // If resource doesn't support credentials and if any origin is + // allowed + // to make CORS request, return header with '*'. + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + "*"); + } else { + // If the resource supports credentials add a single + // Access-Control-Allow-Origin header, with the value of the Origin + // header as value. + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + + // Section 6.1.3 + // If the resource supports credentials, add a single + // Access-Control-Allow-Credentials header with the case-sensitive + // string "true" as value. + if (supportsCredentials) { + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } + + // Section 6.1.4 + // If the list of exposed headers is not empty add one or more + // Access-Control-Expose-Headers headers, with as values the header + // field names given in the list of exposed headers. + if ((exposedHeaders != null) && (exposedHeaders.size() > 0)) { + String exposedHeadersString = join(exposedHeaders, ","); + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + exposedHeadersString); + } + + // Forward the request down the filter chain. + filterChain.doFilter(request, response); + } + + + /** + * Handles CORS pre-flight request. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + protected void handlePreflightCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + + CORSRequestType requestType = checkRequestType(request); + if (requestType != CORSRequestType.PRE_FLIGHT) { + throw new IllegalArgumentException( + sm.getString("corsFilter.wrongType1", + CORSRequestType.PRE_FLIGHT.name().toLowerCase())); + } + + final String origin = request + .getHeader(CorsFilter.REQUEST_HEADER_ORIGIN); + + // Section 6.2.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.3 + String accessControlRequestMethod = request.getHeader( + CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethod == null || + !HTTP_METHODS.contains(accessControlRequestMethod.trim())) { + handleInvalidCORS(request, response, filterChain); + return; + } else { + accessControlRequestMethod = accessControlRequestMethod.trim(); + } + + // Section 6.2.4 + String accessControlRequestHeadersHeader = request.getHeader( + CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + List accessControlRequestHeaders = new LinkedList(); + if (accessControlRequestHeadersHeader != null && + !accessControlRequestHeadersHeader.trim().isEmpty()) { + String[] headers = accessControlRequestHeadersHeader.trim().split( + ","); + for (String header : headers) { + accessControlRequestHeaders.add(header.trim().toLowerCase()); + } + } + + // Section 6.2.5 + if (!allowedHttpMethods.contains(accessControlRequestMethod)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.6 + if (!accessControlRequestHeaders.isEmpty()) { + for (String header : accessControlRequestHeaders) { + if (!allowedHttpHeaders.contains(header)) { + handleInvalidCORS(request, response, filterChain); + return; + } + } + } + + // Section 6.2.7 + if (supportsCredentials) { + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } else { + if (anyOriginAllowed) { + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + "*"); + } else { + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + } + + // Section 6.2.8 + if (preflightMaxAge > 0) { + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE, + String.valueOf(preflightMaxAge)); + } + + // Section 6.2.9 + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS, + accessControlRequestMethod); + + // Section 6.2.10 + if ((allowedHttpHeaders != null) && (!allowedHttpHeaders.isEmpty())) { + response.addHeader( + CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, + join(allowedHttpHeaders, ",")); + } + + // Do not forward the request down the filter chain. + } + + + /** + * Handles a request, that's not a CORS request, but is a valid request i.e. + * it is not a cross-origin request. This implementation, just forwards the + * request down the filter chain. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + private void handleNonCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + // Let request pass. + filterChain.doFilter(request, response); + } + + + /** + * Handles a CORS request that violates specification. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + */ + private void handleInvalidCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) { + String origin = request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN); + String method = request.getMethod(); + String accessControlRequestHeaders = request.getHeader( + REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.resetBuffer(); + + if (log.isDebugEnabled()) { + // Debug so no need for i18n + StringBuilder message = + new StringBuilder("Invalid CORS request; Origin="); + message.append(origin); + message.append(";Method="); + message.append(method); + if (accessControlRequestHeaders != null) { + message.append(";Access-Control-Request-Headers="); + message.append(accessControlRequestHeaders); + } + log.debug(message.toString()); + } + } + + + @Override + public void destroy() { + // NOOP + } + + + /** + * Decorates the {@link HttpServletRequest}, with CORS attributes. + *
    + *
  • cors.isCorsRequest: Flag to determine if request is a CORS + * request. Set to true if CORS request; false + * otherwise.
  • + *
  • cors.request.origin: The Origin URL.
  • + *
  • cors.request.type: Type of request. Values: + * simple or preflight or not_cors or + * invalid_cors
  • + *
  • cors.request.headers: Request headers sent as + * 'Access-Control-Request-Headers' header, for pre-flight request.
  • + *
+ * + * @param request + * The {@link HttpServletRequest} object. + * @param corsRequestType + * The {@link CORSRequestType} object. + */ + protected static void decorateCORSProperties( + final HttpServletRequest request, + final CORSRequestType corsRequestType) { + if (request == null) { + throw new IllegalArgumentException( + sm.getString("corsFilter.nullRequest")); + } + + if (corsRequestType == null) { + throw new IllegalArgumentException( + sm.getString("corsFilter.nullRequestType")); + } + + switch (corsRequestType) { + case SIMPLE: + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.TRUE); + request.setAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case ACTUAL: + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.TRUE); + request.setAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case PRE_FLIGHT: + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.TRUE); + request.setAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + String headers = request.getHeader( + REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + if (headers == null) { + headers = ""; + } + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, headers); + break; + case NOT_CORS: + request.setAttribute( + CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.FALSE); + break; + default: + // Don't set any attributes + break; + } + } + + + /** + * Joins elements of {@link Set} into a string, where each element is + * separated by the provided separator. + * + * @param elements + * The {@link Set} containing elements to join together. + * @param joinSeparator + * The character to be used for separating elements. + * @return The joined {@link String}; null if elements + * {@link Set} is null. + */ + protected static String join(final Collection elements, + final String joinSeparator) { + String separator = ","; + if (elements == null) { + return null; + } + if (joinSeparator != null) { + separator = joinSeparator; + } + StringBuilder buffer = new StringBuilder(); + boolean isFirst = true; + for (String element : elements) { + if (!isFirst) { + buffer.append(separator); + } else { + isFirst = false; + } + + if (element != null) { + buffer.append(element); + } + } + + return buffer.toString(); + } + + + /** + * Determines the request type. + * + * @param request + */ + protected CORSRequestType checkRequestType(final HttpServletRequest request) { + CORSRequestType requestType = CORSRequestType.INVALID_CORS; + if (request == null) { + throw new IllegalArgumentException( + sm.getString("corsFilter.nullRequest")); + } + String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN); + // Section 6.1.1 and Section 6.2.1 + if (originHeader != null) { + if (originHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else if (!isValidOrigin(originHeader)) { + requestType = CORSRequestType.INVALID_CORS; + } else { + String method = request.getMethod(); + if (method != null && HTTP_METHODS.contains(method)) { + if ("OPTIONS".equals(method)) { + String accessControlRequestMethodHeader = + request.getHeader( + REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethodHeader != null && + !accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.PRE_FLIGHT; + } else if (accessControlRequestMethodHeader != null && + accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else { + requestType = CORSRequestType.ACTUAL; + } + } else if ("GET".equals(method) || "HEAD".equals(method)) { + requestType = CORSRequestType.SIMPLE; + } else if ("POST".equals(method)) { + String contentType = request.getContentType(); + if (contentType != null) { + contentType = contentType.toLowerCase().trim(); + if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES + .contains(contentType)) { + requestType = CORSRequestType.SIMPLE; + } else { + requestType = CORSRequestType.ACTUAL; + } + } + } else if (COMPLEX_HTTP_METHODS.contains(method)) { + requestType = CORSRequestType.ACTUAL; + } + } + } + } else { + requestType = CORSRequestType.NOT_CORS; + } + + return requestType; + } + + + /** + * Checks if the Origin is allowed to make a CORS request. + * + * @param origin + * The Origin. + * @return true if origin is allowed; false + * otherwise. + */ + private boolean isOriginAllowed(final String origin) { + if (anyOriginAllowed) { + return true; + } + + // If 'Origin' header is a case-sensitive match of any of allowed + // origins, then return true, else return false. + return allowedOrigins.contains(origin); + } + + + /** + * Parses each param-value and populates configuration variables. If a param + * is provided, it overrides the default. + * + * @param allowedOrigins + * A {@link String} of comma separated origins. + * @param allowedHttpMethods + * A {@link String} of comma separated HTTP methods. + * @param allowedHttpHeaders + * A {@link String} of comma separated HTTP headers. + * @param exposedHeaders + * A {@link String} of comma separated headers that needs to be + * exposed. + * @param supportsCredentials + * "true" if support credentials needs to be enabled. + * @param preflightMaxAge + * The amount of seconds the user agent is allowed to cache the + * result of the pre-flight request. + * @throws ServletException + */ + private void parseAndStore(final String allowedOrigins, + final String allowedHttpMethods, final String allowedHttpHeaders, + final String exposedHeaders, final String supportsCredentials, + final String preflightMaxAge, final String decorateRequest) + throws ServletException { + if (allowedOrigins != null) { + if (allowedOrigins.trim().equals("*")) { + this.anyOriginAllowed = true; + } else { + this.anyOriginAllowed = false; + Set setAllowedOrigins = + parseStringToSet(allowedOrigins); + this.allowedOrigins.clear(); + this.allowedOrigins.addAll(setAllowedOrigins); + } + } + + if (allowedHttpMethods != null) { + Set setAllowedHttpMethods = + parseStringToSet(allowedHttpMethods); + this.allowedHttpMethods.clear(); + this.allowedHttpMethods.addAll(setAllowedHttpMethods); + } + + if (allowedHttpHeaders != null) { + Set setAllowedHttpHeaders = + parseStringToSet(allowedHttpHeaders); + Set lowerCaseHeaders = new HashSet(); + for (String header : setAllowedHttpHeaders) { + String lowerCase = header.toLowerCase(); + lowerCaseHeaders.add(lowerCase); + } + this.allowedHttpHeaders.clear(); + this.allowedHttpHeaders.addAll(lowerCaseHeaders); + } + + if (exposedHeaders != null) { + Set setExposedHeaders = parseStringToSet(exposedHeaders); + this.exposedHeaders.clear(); + this.exposedHeaders.addAll(setExposedHeaders); + } + + if (supportsCredentials != null) { + // For any value other then 'true' this will be false. + this.supportsCredentials = Boolean + .parseBoolean(supportsCredentials); + } + + if (preflightMaxAge != null) { + try { + if (!preflightMaxAge.isEmpty()) { + this.preflightMaxAge = Long.parseLong(preflightMaxAge); + } else { + this.preflightMaxAge = 0L; + } + } catch (NumberFormatException e) { + throw new ServletException( + sm.getString("corsFilter.invalidPreflightMaxAge"), e); + } + } + + if (decorateRequest != null) { + // For any value other then 'true' this will be false. + this.decorateRequest = Boolean.parseBoolean(decorateRequest); + } + } + + /** + * Takes a comma separated list and returns a Set. + * + * @param data + * A comma separated list of strings. + * @return Set + */ + private Set parseStringToSet(final String data) { + String[] splits; + + if (data != null && data.length() > 0) { + splits = data.split(","); + } else { + splits = new String[] {}; + } + + Set set = new HashSet(); + if (splits.length > 0) { + for (String split : splits) { + set.add(split.trim()); + } + } + + return set; + } + + + /** + * Checks if a given origin is valid or not. Criteria: + *
    + *
  • If an encoded character is present in origin, it's not valid.
  • + *
  • Origin should be a valid {@link URI}
  • + *
+ * + * @param origin + * @see RFC952 + */ + protected static boolean isValidOrigin(String origin) { + // Checks for encoded characters. Helps prevent CRLF injection. + if (origin.contains("%")) { + return false; + } + + URI originURI; + + try { + originURI = new URI(origin); + } catch (URISyntaxException e) { + return false; + } + // If scheme for URI is null, return false. Return true otherwise. + return originURI.getScheme() != null; + + } + + + /** + * Determines if any origin is allowed to make CORS request. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isAnyOriginAllowed() { + return anyOriginAllowed; + } + + + /** + * Returns a {@link Set} of headers that should be exposed by browser. + */ + public Collection getExposedHeaders() { + return exposedHeaders; + } + + + /** + * Determines is supports credentials is enabled. + */ + public boolean isSupportsCredentials() { + return supportsCredentials; + } + + + /** + * Returns the preflight response cache time in seconds. + * + * @return Time to cache in seconds. + */ + public long getPreflightMaxAge() { + return preflightMaxAge; + } + + + /** + * Returns the {@link Set} of allowed origins that are allowed to make + * requests. + * + * @return {@link Set} + */ + public Collection getAllowedOrigins() { + return allowedOrigins; + } + + + /** + * Returns a {@link Set} of HTTP methods that are allowed to make requests. + * + * @return {@link Set} + */ + public Collection getAllowedHttpMethods() { + return allowedHttpMethods; + } + + + /** + * Returns a {@link Set} of headers support by resource. + * + * @return {@link Set} + */ + public Collection getAllowedHttpHeaders() { + return allowedHttpHeaders; + } + + + // -------------------------------------------------- CORS Response Headers + /** + * The Access-Control-Allow-Origin header indicates whether a resource can + * be shared based by returning the value of the Origin request header in + * the response. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = + "Access-Control-Allow-Origin"; + + /** + * The Access-Control-Allow-Credentials header indicates whether the + * response to request can be exposed when the omit credentials flag is + * unset. When part of the response to a preflight request it indicates that + * the actual request can include user credentials. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = + "Access-Control-Allow-Credentials"; + + /** + * The Access-Control-Expose-Headers header indicates which headers are safe + * to expose to the API of a CORS API specification + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = + "Access-Control-Expose-Headers"; + + /** + * The Access-Control-Max-Age header indicates how long the results of a + * preflight request can be cached in a preflight result cache. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE = + "Access-Control-Max-Age"; + + /** + * The Access-Control-Allow-Methods header indicates, as part of the + * response to a preflight request, which methods can be used during the + * actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS = + "Access-Control-Allow-Methods"; + + /** + * The Access-Control-Allow-Headers header indicates, as part of the + * response to a preflight request, which header field names can be used + * during the actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS = + "Access-Control-Allow-Headers"; + + // -------------------------------------------------- CORS Request Headers + /** + * The Origin header indicates where the cross-origin request or preflight + * request originates from. + */ + public static final String REQUEST_HEADER_ORIGIN = "Origin"; + + /** + * The Access-Control-Request-Method header indicates which method will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD = + "Access-Control-Request-Method"; + + /** + * The Access-Control-Request-Headers header indicates which headers will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS = + "Access-Control-Request-Headers"; + + // ----------------------------------------------------- Request attributes + /** + * The prefix to a CORS request attribute. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_PREFIX = "cors."; + + /** + * Attribute that contains the origin of the request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_ORIGIN = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.origin"; + + /** + * Boolean value, suggesting if the request is a CORS request or not. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "isCorsRequest"; + + /** + * Type of CORS request, of type {@link CORSRequestType}. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.type"; + + /** + * Request headers sent as 'Access-Control-Request-Headers' header, for + * pre-flight request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.headers"; + + // -------------------------------------------------------------- Constants + /** + * Enumerates varies types of CORS requests. Also, provides utility methods + * to determine the request type. + */ + protected static enum CORSRequestType { + /** + * A simple HTTP request, i.e. it shouldn't be pre-flighted. + */ + SIMPLE, + /** + * A HTTP request that needs to be pre-flighted. + */ + ACTUAL, + /** + * A pre-flight CORS request, to get meta information, before a + * non-simple HTTP request is sent. + */ + PRE_FLIGHT, + /** + * Not a CORS request, but a normal request. + */ + NOT_CORS, + /** + * An invalid CORS request, i.e. it qualifies to be a CORS request, but + * fails to be a valid one. + */ + INVALID_CORS + } + + /** + * {@link Collection} of HTTP methods. Case sensitive. + * + * @see http://tools.ietf.org/html/rfc2616#section-5.1.1 + * + */ + public static final Collection HTTP_METHODS = + new HashSet(Arrays.asList("OPTIONS", "GET", "HEAD", "POST", + "PUT", "DELETE", "TRACE", "CONNECT")); + /** + * {@link Collection} of non-simple HTTP methods. Case sensitive. + */ + public static final Collection COMPLEX_HTTP_METHODS = + new HashSet(Arrays.asList("PUT", "DELETE", "TRACE", + "CONNECT")); + /** + * {@link Collection} of Simple HTTP methods. Case sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_METHODS = + new HashSet(Arrays.asList("GET", "POST", "HEAD")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_HEADERS = + new HashSet(Arrays.asList("Accept", "Accept-Language", + "Content-Language")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_RESPONSE_HEADERS = + new HashSet(Arrays.asList("Cache-Control", + "Content-Language", "Content-Type", "Expires", + "Last-Modified", "Pragma")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES = + new HashSet(Arrays.asList( + "application/x-www-form-urlencoded", + "multipart/form-data", "text/plain")); + + // ------------------------------------------------ Configuration Defaults + /** + * By default, all origins are allowed to make requests. + */ + public static final String DEFAULT_ALLOWED_ORIGINS = "*"; + + /** + * By default, following methods are supported: GET, POST, HEAD and OPTIONS. + */ + public static final String DEFAULT_ALLOWED_HTTP_METHODS = + "GET,POST,HEAD,OPTIONS"; + + /** + * By default, time duration to cache pre-flight response is 30 mins. + */ + public static final String DEFAULT_PREFLIGHT_MAXAGE = "1800"; + + /** + * By default, support credentials is turned on. + */ + public static final String DEFAULT_SUPPORTS_CREDENTIALS = "true"; + + /** + * By default, following headers are supported: + * Origin,Accept,X-Requested-With, Content-Type, + * Access-Control-Request-Method, and Access-Control-Request-Headers. + */ + public static final String DEFAULT_ALLOWED_HTTP_HEADERS = + "Origin,Accept,X-Requested-With,Content-Type," + + "Access-Control-Request-Method,Access-Control-Request-Headers"; + + /** + * By default, none of the headers are exposed in response. + */ + public static final String DEFAULT_EXPOSED_HEADERS = ""; + + /** + * By default, request is decorated with CORS attributes. + */ + public static final String DEFAULT_DECORATE_REQUEST = "true"; + + // ----------------------------------------Filter Config Init param-name(s) + /** + * Key to retrieve allowed origins from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_ORIGINS = + "cors.allowed.origins"; + + /** + * Key to retrieve support credentials from {@link FilterConfig}. + */ + public static final String PARAM_CORS_SUPPORT_CREDENTIALS = + "cors.support.credentials"; + + /** + * Key to retrieve exposed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_EXPOSED_HEADERS = + "cors.exposed.headers"; + + /** + * Key to retrieve allowed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_HEADERS = + "cors.allowed.headers"; + + /** + * Key to retrieve allowed methods from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_METHODS = + "cors.allowed.methods"; + + /** + * Key to retrieve preflight max age from {@link FilterConfig}. + */ + public static final String PARAM_CORS_PREFLIGHT_MAXAGE = + "cors.preflight.maxage"; + + /** + * Key to determine if request should be decorated. + */ + public static final String PARAM_CORS_REQUEST_DECORATE = + "cors.request.decorate"; +}