Bug 951 - Externalize cors definition of restconf 88/5888/6
authorTomas Olvecky <tolvecky@cisco.com>
Wed, 7 May 2014 12:23:08 +0000 (14:23 +0200)
committerTomas Olvecky <tolvecky@cisco.com>
Wed, 11 Jun 2014 07:35:32 +0000 (09:35 +0200)
Create new project called filter-valve that allows defining filters
outside of web.xml. An xml file is added to configuration folder
of distribution.

The valve allows any kind of filters to be applied around each
request, mapping contexts and path pattern same way as servlet
specification does. The xml file allows defining filter templates,
each context (WAB) can reuse and modify the common configuration.

Currently only restconf has externalized cors filter definition.

Change-Id: Ia8b6053efdff2b3c1150eec95e63b460d84c457e
Signed-off-by: Tomas Olvecky <tolvecky@cisco.com>
23 files changed:
opendaylight/commons/filter-valve/pom.xml [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/FilterValve.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Context.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Filter.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/FilterMapping.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Host.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/InitParam.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Parser.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/FilterProcessor.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcher.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/DummyFilter.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/MockedFilter.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/ParserTest.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcherTest.java [new file with mode: 0644]
opendaylight/commons/filter-valve/src/test/resources/conflicting-class.xml [new file with mode: 0644]
opendaylight/commons/filter-valve/src/test/resources/no-filter-defined.xml [new file with mode: 0644]
opendaylight/commons/filter-valve/src/test/resources/sample-cors-config.xml [new file with mode: 0644]
opendaylight/commons/opendaylight/pom.xml
opendaylight/distribution/opendaylight/pom.xml
opendaylight/distribution/opendaylight/src/main/resources/configuration/cors-config.xml [new file with mode: 0644]
opendaylight/distribution/opendaylight/src/main/resources/configuration/tomcat-server.xml
opendaylight/md-sal/sal-rest-connector/src/main/resources/WEB-INF/web.xml
pom.xml

diff --git a/opendaylight/commons/filter-valve/pom.xml b/opendaylight/commons/filter-valve/pom.xml
new file mode 100644 (file)
index 0000000..7b5be02
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.opendaylight.controller</groupId>
+    <artifactId>commons.opendaylight</artifactId>
+    <version>1.4.2-SNAPSHOT</version>
+    <relativePath>../opendaylight</relativePath>
+  </parent>
+  <artifactId>filter-valve</artifactId>
+  <packaging>bundle</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>equinoxSDK381</groupId>
+      <artifactId>javax.servlet</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>orbit</groupId>
+      <artifactId>org.apache.catalina</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Fragment-Host>org.eclipse.gemini.web.tomcat</Fragment-Host>
+            <Import-Package>javax.servlet,
+                            org.apache.catalina,
+                            org.apache.catalina.connector,
+                            org.apache.catalina.valves,
+                            org.slf4j,
+                            javax.xml.bind,
+                            javax.xml.bind.annotation,
+                            org.apache.commons.io,
+                            com.google.common.base,
+                            com.google.common.collect</Import-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.opendaylight.yangtools</groupId>
+        <artifactId>yang-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/FilterValve.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/FilterValve.java
new file mode 100644 (file)
index 0000000..54d8be1
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Objects;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.valves.ValveBase;
+import org.apache.commons.io.FileUtils;
+import org.opendaylight.controller.filtervalve.cors.jaxb.Host;
+import org.opendaylight.controller.filtervalve.cors.jaxb.Parser;
+import org.opendaylight.controller.filtervalve.cors.model.FilterProcessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Valve that allows adding filters per context. Each context can have its own filter definitions.
+ * Main purpose is to allow externalizing security filters from application bundles to a single
+ * file per OSGi distribution.
+ */
+public class FilterValve extends ValveBase {
+    private static final Logger logger = LoggerFactory.getLogger(FilterValve.class);
+    private FilterProcessor filterProcessor;
+
+    public void invoke(final Request request, final Response response) throws IOException, ServletException {
+        if (filterProcessor == null) {
+            throw new IllegalStateException("Initialization error");
+        }
+
+        FilterChain nextValveFilterChain = new FilterChain() {
+            @Override
+            public void doFilter(ServletRequest req, ServletResponse resp) throws IOException, ServletException {
+                boolean reqEquals = Objects.equals(request, req);
+                boolean respEquals = Objects.equals(response, resp);
+                if (reqEquals == false || respEquals == false) {
+                    logger.error("Illegal change was detected by valve - request {} or " +
+                            "response {} was replaced by a filter. This is not supported by this valve",
+                            reqEquals, respEquals);
+                    throw new IllegalStateException("Request or response was replaced in a filter");
+                }
+                getNext().invoke(request, response);
+            }
+        };
+        filterProcessor.process(request, response, nextValveFilterChain);
+    }
+
+    /**
+     * Called by Tomcat when configurationFile attribute is set.
+     * @param fileName path to xml file containing valve configuration
+     * @throws Exception
+     */
+    @SuppressWarnings("UnusedDeclaration")
+    public void setConfigurationFile(String fileName) throws Exception {
+        File configurationFile = new File(fileName);
+        if (configurationFile.exists() == false || configurationFile.canRead() == false) {
+            throw new IllegalArgumentException(
+                    "Cannot read 'configurationFile' of this valve defined in tomcat-server.xml: " + fileName);
+        }
+        String xmlContent;
+        try {
+            xmlContent = FileUtils.readFileToString(configurationFile);
+        } catch (IOException e) {
+            logger.error("Cannot read {} of this valve defined in tomcat-server.xml", fileName, e);
+            throw new IllegalStateException("Cannot read " + fileName, e);
+        }
+        Host host;
+        try {
+            host = Parser.parse(xmlContent, fileName);
+        } catch (Exception e) {
+            logger.error("Cannot parse {} of this valve defined in tomcat-server.xml", fileName, e);
+            throw new IllegalStateException("Error while parsing " + fileName, e);
+        }
+        filterProcessor = new FilterProcessor(host);
+    }
+
+    /**
+     * @see org.apache.catalina.valves.ValveBase#getInfo()
+     */
+    public String getInfo() {
+        return getClass() + "/1.0";
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Context.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Context.java
new file mode 100644 (file)
index 0000000..dbe0745
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+
+import com.google.common.base.Optional;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import org.opendaylight.controller.filtervalve.cors.model.UrlMatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@XmlRootElement
+public class Context {
+    private static final Logger logger = LoggerFactory.getLogger(Context.class);
+
+    private String path;
+    private List<Filter> filters = new ArrayList<>();
+    private List<FilterMapping> filterMappings = new ArrayList<>();
+    private boolean initialized;
+    private UrlMatcher<Filter> urlMatcher;
+
+
+    public synchronized void initialize(String fileName, Map<String, Filter> namesToTemplates) {
+        checkState(initialized == false, "Already initialized");
+        Map<String, Filter> namesToFilters = new HashMap<>();
+        for (Filter filter : filters) {
+            try {
+                filter.initialize(fileName, Optional.fromNullable(namesToTemplates.get(filter.getFilterName())));
+            } catch (Exception e) {
+                throw new IllegalStateException(format("Error while processing filter %s of context %s, defined in %s",
+                        filter.getFilterName(), path, fileName), e);
+            }
+            namesToFilters.put(filter.getFilterName(), filter);
+        }
+        filters = Collections.unmodifiableList(new ArrayList<>(filters));
+        LinkedHashMap<String, Filter> patternMap = new LinkedHashMap<>();
+        for (FilterMapping filterMapping : filterMappings) {
+            filterMapping.initialize();
+            Filter found = namesToFilters.get(filterMapping.getFilterName());
+            if (found != null) {
+                patternMap.put(filterMapping.getUrlPattern(), found);
+            } else {
+                logger.error("Cannot find matching filter for filter-mapping {} of context {}, defined in {}",
+                        filterMapping.getFilterName(), path, fileName);
+                throw new IllegalStateException(format(
+                        "Cannot find filter for filter-mapping %s of context %s, defined in %s",
+                        filterMapping.getFilterName(), path, fileName));
+            }
+        }
+        filterMappings = Collections.unmodifiableList(new ArrayList<>(filterMappings));
+        urlMatcher = new UrlMatcher<>(patternMap);
+        initialized = true;
+    }
+
+    public List<Filter> findMatchingFilters(String pathInfo) {
+        checkState(initialized, "Not initialized");
+        return urlMatcher.findMatchingFilters(pathInfo);
+    }
+
+    @XmlAttribute(name = "path")
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        checkArgument(initialized == false, "Already initialized");
+        this.path = path;
+    }
+
+    @XmlElement(name = "filter")
+    public List<Filter> getFilters() {
+        return filters;
+    }
+
+    public void setFilters(List<Filter> filters) {
+        checkArgument(initialized == false, "Already initialized");
+        this.filters = filters;
+    }
+
+    @XmlElement(name = "filter-mapping")
+    public List<FilterMapping> getFilterMappings() {
+        return filterMappings;
+    }
+
+    public void setFilterMappings(List<FilterMapping> filterMappings) {
+        checkArgument(initialized == false, "Already initialized");
+        this.filterMappings = filterMappings;
+    }
+
+    @Override
+    public String toString() {
+        return "Context{" +
+                "path='" + path + '\'' +
+                '}';
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Filter.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Filter.java
new file mode 100644 (file)
index 0000000..3dde5b1
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.MapDifference;
+import com.google.common.collect.Maps;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@XmlRootElement
+public class Filter implements FilterConfig {
+    private static final Logger logger = LoggerFactory.getLogger(Filter.class);
+
+    private String filterName;
+    private String filterClass;
+    private List<InitParam> initParams = new ArrayList<>();
+    private javax.servlet.Filter actualFilter;
+    private boolean initialized, isTemplate;
+
+
+    /**
+     * Called in filter-template nodes defined in <Host/> node - do not actually initialize the filter.
+     * In this case filter is only used to hold values of init params to be merged with
+     * filter defined in <Context/>
+     */
+    public synchronized void initializeTemplate(){
+        checkState(initialized == false, "Already initialized");
+        for (InitParam initParam : initParams) {
+            initParam.inititialize();
+        }
+        isTemplate = true;
+        initialized = true;
+    }
+
+
+    public synchronized void initialize(String fileName, Optional<Filter> maybeTemplate) {
+        checkState(initialized == false, "Already initialized");
+        logger.trace("Initializing filter {} : {}", filterName, filterClass);
+        for (InitParam initParam : initParams) {
+            initParam.inititialize();
+        }
+        if (maybeTemplate.isPresent()) {
+            // merge non conflicting init params
+            Filter template = maybeTemplate.get();
+            checkArgument(template.isTemplate);
+            Map<String, InitParam> templateParams = template.getInitParamsMap();
+            Map<String, InitParam> currentParams = getInitParamsMap();
+            // add values of template that are not present in current
+            MapDifference<String, InitParam> difference = Maps.difference(templateParams, currentParams);
+            for (Entry<String, InitParam> templateUnique : difference.entriesOnlyOnLeft().entrySet()) {
+                initParams.add(templateUnique.getValue());
+            }
+            // merge filterClass
+            if (filterClass == null) {
+                filterClass = template.filterClass;
+            } else if (Objects.equals(filterClass, template.filterClass) == false) {
+                logger.error("Conflict detected in filter-class of {} defined in {}, template class {}, child class {}" ,
+                        filterName, fileName, template.filterClass, filterClass);
+                throw new IllegalStateException("Conflict detected in template/filter filter-class definitions," +
+                        " filter name: " + filterName + " in file " + fileName);
+            }
+        }
+        initParams = Collections.unmodifiableList(new ArrayList<>(initParams));
+        Class<?> clazz;
+        try {
+            clazz = Class.forName(filterClass);
+        } catch (Exception e) {
+            throw new IllegalStateException("Cannot instantiate class defined in filter " + filterName
+                    + " in file " + fileName, e);
+        }
+        try {
+            actualFilter = (javax.servlet.Filter) clazz.newInstance();
+        } catch (Exception e) {
+            throw new IllegalStateException("Cannot instantiate class defined in filter " + filterName
+                    + " in file " + fileName, e);
+        }
+        logger.trace("Initializing {} with following init-params:{}", filterName, getInitParams());
+        try {
+            actualFilter.init(this);
+        } catch (Exception e) {
+            throw new IllegalStateException("Cannot initialize filter " + filterName
+                    + " in file " + fileName, e);
+        }
+        initialized = true;
+    }
+
+    @Override
+    public ServletContext getServletContext() {
+        throw new UnsupportedOperationException("Getting ServletContext is currently not supported");
+    }
+
+    @Override
+    public String getInitParameter(String name) {
+        for (InitParam initParam : initParams) {
+            if (Objects.equals(name, initParam.getParamName())) {
+                return initParam.getParamValue();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Enumeration<String> getInitParameterNames() {
+        final Iterator<InitParam> iterator = initParams.iterator();
+        return new Enumeration<String>() {
+            @Override
+            public boolean hasMoreElements() {
+                return iterator.hasNext();
+            }
+
+            @Override
+            public String nextElement() {
+                return iterator.next().getParamName();
+            }
+        };
+    }
+
+    public javax.servlet.Filter getActualFilter() {
+        checkState(initialized, "Not initialized");
+        return actualFilter;
+    }
+
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+
+    @XmlElement(name = "filter-name")
+    public String getFilterName() {
+        return filterName;
+    }
+
+    public void setFilterName(String filterName) {
+        this.filterName = filterName;
+    }
+
+    @XmlElement(name = "filter-class")
+    public String getFilterClass() {
+        return filterClass;
+    }
+
+    public void setFilterClass(String filterClass) {
+        this.filterClass = filterClass;
+    }
+
+    @XmlElement(name = "init-param")
+    public List<InitParam> getInitParams() {
+        return initParams;
+    }
+
+    public void setInitParams(List<InitParam> initParams) {
+        this.initParams = initParams;
+    }
+
+
+    @Override
+    public String toString() {
+        return "Filter{" +
+                "filterName='" + filterName + '\'' +
+                '}';
+    }
+
+    public Map<String, InitParam> getInitParamsMap() {
+        Map<String, InitParam> result = new HashMap<>();
+        for (InitParam initParam : initParams) {
+            checkState(initParam.isInitialized());
+            result.put(initParam.getParamName(), initParam);
+        }
+        return result;
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/FilterMapping.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/FilterMapping.java
new file mode 100644 (file)
index 0000000..03fcbf2
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement
+public class FilterMapping {
+    private String filterName;
+    private String urlPattern;
+    private boolean initialized;
+
+    @XmlElement(name = "filter-name")
+    public String getFilterName() {
+        return filterName;
+    }
+
+    public void setFilterName(String filterName) {
+        checkArgument(initialized == false, "Already initialized");
+        this.filterName = filterName;
+    }
+
+    @XmlElement(name = "url-pattern")
+    public String getUrlPattern() {
+        return urlPattern;
+    }
+
+    public void setUrlPattern(String urlPattern) {
+        checkArgument(initialized == false, "Already initialized");
+        this.urlPattern = urlPattern;
+    }
+
+    public synchronized void initialize() {
+        checkArgument(initialized == false, "Already initialized");
+        initialized = true;
+    }
+
+    public boolean isInitialized() {
+        return initialized;
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Host.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Host.java
new file mode 100644 (file)
index 0000000..4e3c3ba
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Optional;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+
+/**
+ * Root element, arbitrarily named Host to match tomcat-server.xml, but does not allow specifying which host
+ * name to be matched.
+ */
+@XmlRootElement(name = "Host")
+public class Host {
+    private List<Context> contexts = new ArrayList<>();
+    private List<Filter> filterTemplates = new ArrayList<>();
+    private boolean initialized;
+    private Map<String, Context> contextMap;
+
+
+    public synchronized void initialize(String fileName) {
+        checkState(initialized == false, "Already initialized");
+        Map<String, Filter> namesToTemplates = new HashMap<>();
+        for (Filter template : filterTemplates) {
+            template.initializeTemplate();
+            namesToTemplates.put(template.getFilterName(), template);
+        }
+        contextMap = new HashMap<>();
+        for (Context context : getContexts()) {
+            checkState(contextMap.containsKey(context.getPath()) == false,
+                    "Context {} already defined in {}", context.getPath(), fileName);
+            context.initialize(fileName, namesToTemplates);
+            contextMap.put(context.getPath(), context);
+        }
+        contextMap = Collections.unmodifiableMap(new HashMap<>(contextMap));
+        contexts = Collections.unmodifiableList(new ArrayList<>(contexts));
+        initialized = true;
+    }
+
+    public Optional<Context> findContext(String contextPath) {
+        checkState(initialized, "Not initialized");
+        Context context = contextMap.get(contextPath);
+        return Optional.fromNullable(context);
+    }
+
+    @XmlElement(name = "Context")
+    public List<Context> getContexts() {
+        return contexts;
+    }
+
+    public void setContexts(List<Context> contexts) {
+        checkArgument(initialized == false, "Already initialized");
+        this.contexts = contexts;
+    }
+
+    @XmlElement(name = "filter-template")
+    public List<Filter> getFilterTemplates() {
+        return filterTemplates;
+    }
+
+    public void setFilterTemplates(List<Filter> filterTemplates) {
+        checkArgument(initialized == false, "Already initialized");
+        this.filterTemplates = filterTemplates;
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/InitParam.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/InitParam.java
new file mode 100644 (file)
index 0000000..edc9e45
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement
+public class InitParam {
+    private String paramName;
+    private String paramValue;
+    private boolean initialized;
+
+    public synchronized void inititialize() {
+        checkState(initialized == false, "Already initialized");
+        initialized = true;
+    }
+
+    @XmlElement(name = "param-name")
+    public String getParamName() {
+        return paramName;
+    }
+
+    public void setParamName(String paramName) {
+        this.paramName = paramName;
+    }
+
+    @XmlElement(name = "param-value")
+    public String getParamValue() {
+        return paramValue;
+    }
+
+    public void setParamValue(String paramValue) {
+        this.paramValue = paramValue;
+    }
+
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    @Override
+    public String toString() {
+        return "{" + paramName + '=' + paramValue + "}";
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Parser.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Parser.java
new file mode 100644 (file)
index 0000000..bc4f12e
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import java.io.StringReader;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+
+public class Parser {
+
+    public static Host parse(String xmlFileContent, String fileName) throws JAXBException {
+        JAXBContext context = JAXBContext.newInstance(Host.class);
+        javax.xml.bind.Unmarshaller um = context.createUnmarshaller();
+        Host host = (Host) um.unmarshal(new StringReader(xmlFileContent));
+        host.initialize(fileName);
+        return host;
+    }
+
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/FilterProcessor.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/FilterProcessor.java
new file mode 100644 (file)
index 0000000..dc3e9dc
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.model;
+
+import com.google.common.base.Optional;
+import java.io.IOException;
+import java.util.List;
+import java.util.ListIterator;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.opendaylight.controller.filtervalve.cors.jaxb.Context;
+import org.opendaylight.controller.filtervalve.cors.jaxb.Filter;
+import org.opendaylight.controller.filtervalve.cors.jaxb.Host;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FilterProcessor {
+    private static final Logger logger = LoggerFactory.getLogger(FilterProcessor.class);
+
+    private final Host host;
+
+    public FilterProcessor(Host host) {
+        this.host = host;
+    }
+
+    public void process(Request request, Response response, FilterChain nextValveFilterChain)
+            throws IOException, ServletException {
+
+        String contextPath = request.getContext().getPath();
+        String pathInfo = request.getPathInfo();
+
+        Optional<Context> maybeContext = host.findContext(contextPath);
+        logger.trace("Processing context {} path {}, found {}", contextPath, pathInfo, maybeContext);
+        if (maybeContext.isPresent()) {
+            // process filters
+            Context context = maybeContext.get();
+            List<Filter> matchingFilters = context.findMatchingFilters(pathInfo);
+            FilterChain fromLast = nextValveFilterChain;
+            ListIterator<Filter> it = matchingFilters.listIterator(matchingFilters.size());
+            final boolean trace = logger.isTraceEnabled();
+            while (it.hasPrevious()) {
+                final Filter currentFilter = it.previous();
+                final FilterChain copy = fromLast;
+                fromLast = new FilterChain() {
+                    @Override
+                    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+                        if (trace) {
+                            logger.trace("Applying {}", currentFilter);
+                        }
+                        javax.servlet.Filter actualFilter = currentFilter.getActualFilter();
+                        actualFilter.doFilter(request, response, copy);
+                    }
+                };
+            }
+            // call first filter
+            fromLast.doFilter(request, response);
+        } else {
+            // move to next valve
+            nextValveFilterChain.doFilter(request, response);
+        }
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcher.java b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcher.java
new file mode 100644 (file)
index 0000000..9535fb1
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.model;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Maps.immutableEntry;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Match incoming URL with user defined patterns according to servlet specification.
+ * In the Web application deployment descriptor, the following syntax is used to define mappings:
+ * <ul>
+ * <li>A string beginning with a â€˜/’ character and ending with a â€˜/*’ suffix is used for path mapping.</li>
+ * <li>A string beginning with a â€˜*.’ prefix is used as an extension mapping.</li>
+ * <li>All other strings are used for exact matches only.</li>
+ * </ul>
+ */
+public class UrlMatcher<FILTER> {
+    private static final Logger logger = LoggerFactory.getLogger(UrlMatcher.class);
+    // order index for each FILTER is kept as Entry.value
+    private final Map<String, Entry<FILTER, Integer>> prefixMap = new HashMap<>(); // contains patterns ending with '/*', '*' is stripped from each key
+    private final Map<String, Entry<FILTER, Integer>> suffixMap = new HashMap<>(); // contains patterns starting with '*.' prefix, '*' is stripped from each key
+    private final Map<String, Entry<FILTER, Integer>> exactMatchMap = new HashMap<>(); // contains exact matches only
+
+    /**
+     * @param patternMap order preserving map containing path info pattern as key
+     */
+    public UrlMatcher(LinkedHashMap<String, FILTER> patternMap) {
+        int idx = 0;
+        for (Entry<String, FILTER> entry : patternMap.entrySet()) {
+            idx++;
+            String pattern = checkNotNull(entry.getKey());
+            FILTER value = entry.getValue();
+            Entry<FILTER, Integer> valueWithIdx = immutableEntry(value, idx);
+            if (pattern.startsWith("/") && pattern.endsWith("/*")) {
+                pattern = pattern.substring(0, pattern.length() - 1);
+                prefixMap.put(pattern, valueWithIdx);
+            } else if (pattern.startsWith("*.")) {
+                pattern = pattern.substring(1);
+                suffixMap.put(pattern, valueWithIdx);
+            } else {
+                exactMatchMap.put(pattern, valueWithIdx);
+            }
+        }
+    }
+
+    /**
+     * Find filters matching path
+     *
+     * @param pathInfo as returned by request.getPathInfo()
+     * @return list of matching filters
+     */
+    public List<FILTER> findMatchingFilters(String pathInfo) {
+        checkNotNull(pathInfo);
+        TreeMap<Integer, FILTER> sortedMap = new TreeMap<>();
+        // add matching prefixes
+        for (Entry<String, Entry<FILTER, Integer>> prefixEntry : prefixMap.entrySet()) {
+            if (pathInfo.startsWith(prefixEntry.getKey())) {
+                put(sortedMap, prefixEntry.getValue());
+            }
+        }
+        // add matching suffixes
+        for (Entry<String, Entry<FILTER, Integer>> suffixEntry : suffixMap.entrySet()) {
+            if (pathInfo.endsWith(suffixEntry.getKey())) {
+                put(sortedMap, suffixEntry.getValue());
+            }
+        }
+        // add exact match
+        Entry<FILTER, Integer> exactMatch = exactMatchMap.get(pathInfo);
+        if (exactMatch != null) {
+            put(sortedMap, exactMatch);
+        }
+        ArrayList<FILTER> filters = new ArrayList<>(sortedMap.values());
+        logger.trace("Matching filters for path {} are {}", pathInfo, filters);
+        return filters;
+    }
+
+    private void put(TreeMap<Integer, FILTER> sortedMap, Entry<FILTER, Integer> entry) {
+        sortedMap.put(entry.getValue(), entry.getKey());
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/DummyFilter.java b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/DummyFilter.java
new file mode 100644 (file)
index 0000000..d14caf9
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+public class DummyFilter implements javax.servlet.Filter {
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void destroy() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/MockedFilter.java b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/MockedFilter.java
new file mode 100644 (file)
index 0000000..56d851b
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+public class MockedFilter implements javax.servlet.Filter {
+    private FilterConfig filterConfig;
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        this.filterConfig = filterConfig;
+    }
+
+    public FilterConfig getFilterConfig() {
+        return filterConfig;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void destroy() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/ParserTest.java b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/ParserTest.java
new file mode 100644 (file)
index 0000000..fc6c01b
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.jaxb;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.matchers.JUnitMatchers.containsString;
+
+import com.google.common.base.Optional;
+import java.io.File;
+import javax.servlet.FilterConfig;
+import org.apache.commons.io.FileUtils;
+import org.junit.Test;
+
+public class ParserTest {
+
+    @Test
+    public void testParsing() throws Exception {
+        File xmlFile = new File(getClass().getResource("/sample-cors-config.xml").getFile());
+        assertThat(xmlFile.canRead(), is(true));
+        String xmlFileContent = FileUtils.readFileToString(xmlFile);
+        Host host = Parser.parse(xmlFileContent, "fileName");
+        assertEquals(1, host.getContexts().size());
+        // check that MockedFilter has init params merged/replaced
+        Optional<Context> context = host.findContext("/restconf");
+        assertTrue(context.isPresent());
+        assertEquals(1, context.get().getFilters().size());
+        MockedFilter filter = (MockedFilter) context.get().getFilters().get(0).getActualFilter();
+        FilterConfig filterConfig = filter.getFilterConfig();
+        assertEquals("*", filterConfig.getInitParameter("cors.allowed.origins"));
+        assertEquals("11", filterConfig.getInitParameter("cors.preflight.maxage"));
+    }
+
+
+    @Test
+    public void testParsing_NoFilterDefined() throws Exception {
+        File xmlFile = new File(getClass().getResource("/no-filter-defined.xml").getFile());
+        assertThat(xmlFile.canRead(), is(true));
+        String xmlFileContent = FileUtils.readFileToString(xmlFile);
+        try {
+            Parser.parse(xmlFileContent, "fileName");
+            fail();
+        }catch(Exception e){
+            assertThat(e.getMessage(), containsString("Cannot find filter for filter-mapping CorsFilter"));
+        }
+    }
+
+    @Test
+    public void testConflictingClass() throws Exception {
+        File xmlFile = new File(getClass().getResource("/conflicting-class.xml").getFile());
+        assertThat(xmlFile.canRead(), is(true));
+        String xmlFileContent = FileUtils.readFileToString(xmlFile);
+        try {
+            Parser.parse(xmlFileContent, "fileName");
+            fail();
+        } catch (RuntimeException e) {
+            assertThat(e.getMessage(), containsString("Error while processing filter CorsFilter of context /restconf"));
+            assertThat(e.getCause().getMessage(), containsString("Conflict detected in template/filter filter-class definitions, filter name: CorsFilter"));
+        }
+    }
+}
diff --git a/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcherTest.java b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcherTest.java
new file mode 100644 (file)
index 0000000..07f6354
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.filtervalve.cors.model;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+
+import java.util.LinkedHashMap;
+import org.junit.Test;
+
+public class UrlMatcherTest {
+    UrlMatcher<String> urlMatcher;
+
+    @Test
+    public void test() throws Exception {
+        final String defaultFilter = "default";
+        final String exactMatchFilter = "someFilter";
+        final String jspFilter = "jspFilter";
+        final String exactMatch = "/somePath";
+        final String prefixFilter = "prefixFilter";
+        LinkedHashMap<String, String> patternMap = new LinkedHashMap<String, String>() {
+            {
+                put(exactMatch, exactMatchFilter);
+                put("/*", defaultFilter);
+                put("*.jsp", jspFilter);
+                put("/foo/*", prefixFilter);
+            }
+        };
+        urlMatcher = new UrlMatcher<>(patternMap);
+        assertMatches("/abc", defaultFilter);
+        assertMatches(exactMatch, exactMatchFilter, defaultFilter);
+        assertMatches("/some.jsp", defaultFilter, jspFilter);
+        assertMatches("/foo/bar", defaultFilter, prefixFilter);
+        assertMatches("/foo/bar.jsp", defaultFilter, jspFilter, prefixFilter);
+    }
+
+    public void assertMatches(String testedPath, String... filters) {
+        assertEquals(asList(filters), urlMatcher.findMatchingFilters(testedPath));
+    }
+
+}
diff --git a/opendaylight/commons/filter-valve/src/test/resources/conflicting-class.xml b/opendaylight/commons/filter-valve/src/test/resources/conflicting-class.xml
new file mode 100644 (file)
index 0000000..c1faf34
--- /dev/null
@@ -0,0 +1,34 @@
+<!--
+  ~ Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<Host>
+    <filter-template>
+        <filter-name>CorsFilter</filter-name>
+        <filter-class>org.opendaylight.controller.filtervalve.cors.jaxb.MockedFilter</filter-class>
+        <init-param>
+            <param-name>cors.preflight.maxage</param-name>
+            <param-value>10</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.allowed.origins</param-name>
+            <param-value>*</param-value>
+        </init-param>
+    </filter-template>
+
+    <Context path="/restconf">
+        <filter>
+            <filter-name>CorsFilter</filter-name>
+            <!-- conflict -->
+            <filter-class>org.opendaylight.controller.filtervalve.cors.jaxb.DummyFilter</filter-class>
+        </filter>
+        <filter-mapping>
+            <filter-name>CorsFilter</filter-name>
+            <url-pattern>/*</url-pattern>
+        </filter-mapping>
+    </Context>
+</Host>
diff --git a/opendaylight/commons/filter-valve/src/test/resources/no-filter-defined.xml b/opendaylight/commons/filter-valve/src/test/resources/no-filter-defined.xml
new file mode 100644 (file)
index 0000000..521d578
--- /dev/null
@@ -0,0 +1,61 @@
+<!--
+  ~ Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<Host>
+    <!-- Filters are allowed here, only serving as a template -->
+    <filter-template>
+        <filter-name>CorsFilter</filter-name>
+        <filter-class>org.opendaylight.controller.filtervalve.cors.jaxb.MockedFilter</filter-class>
+        <init-param>
+            <param-name>cors.allowed.origins</param-name>
+            <param-value>*</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.allowed.methods</param-name>
+            <param-value>GET,POST,HEAD,OPTIONS,PUT,DELETE</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.allowed.headers</param-name>
+            <param-value>Content-Type,X-Requested-With,accept,authorization,
+                origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers
+            </param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.exposed.headers</param-name>
+            <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.support.credentials</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.preflight.maxage</param-name>
+            <param-value>10</param-value>
+        </init-param>
+    </filter-template>
+
+    <Context path="/restconf">
+        <!-- Filters are also allowed here. -->
+        <filter>
+            <filter-name>CorsFilter</filter-name>
+            <!-- init params can be added/overriden if template is used -->
+        </filter>
+        <!-- only local references are allowed -->
+        <filter-mapping>
+            <filter-name>CorsFilter</filter-name>
+            <url-pattern>/*</url-pattern>
+        </filter-mapping>
+    </Context>
+
+    <Context path="/controller/nb/v2/connectionmanager">
+        <filter-mapping>
+            <filter-name>CorsFilter</filter-name>
+            <url-pattern>/*</url-pattern>
+        </filter-mapping>
+    </Context>
+</Host>
diff --git a/opendaylight/commons/filter-valve/src/test/resources/sample-cors-config.xml b/opendaylight/commons/filter-valve/src/test/resources/sample-cors-config.xml
new file mode 100644 (file)
index 0000000..613dc82
--- /dev/null
@@ -0,0 +1,37 @@
+<!--
+  ~ Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<Host>
+    <filter-template>
+        <filter-name>CorsFilter</filter-name>
+        <filter-class>org.opendaylight.controller.filtervalve.cors.jaxb.MockedFilter</filter-class>
+        <init-param>
+            <param-name>cors.preflight.maxage</param-name>
+            <param-value>10</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.allowed.origins</param-name>
+            <param-value>*</param-value>
+        </init-param>
+    </filter-template>
+
+    <Context path="/restconf">
+        <filter>
+            <filter-name>CorsFilter</filter-name>
+            <!-- override value -->
+            <init-param>
+                <param-name>cors.preflight.maxage</param-name>
+                <param-value>11</param-value>
+            </init-param>
+        </filter>
+        <filter-mapping>
+            <filter-name>CorsFilter</filter-name>
+            <url-pattern>/*</url-pattern>
+        </filter-mapping>
+    </Context>
+</Host>
index 077b452f0ac96623c4b9f445d91445f9cae57cd1..70a3d28ce831c766fd27ef0e0eb8d37970e7c80d 100644 (file)
@@ -65,6 +65,7 @@
     <felix.dependencymanager.version>3.1.0</felix.dependencymanager.version>
     <felix.fileinstall.version>3.1.6</felix.fileinstall.version>
     <felix.webconsole.version>4.2.0</felix.webconsole.version>
+    <filtervalve.version>1.4.2-SNAPSHOT</filtervalve.version>
     <flowprogrammer.northbound.version>0.4.2-SNAPSHOT</flowprogrammer.northbound.version>
     <flows.web.version>0.4.2-SNAPSHOT</flows.web.version>
     <forwarding.staticrouting>0.5.2-SNAPSHOT</forwarding.staticrouting>
         <artifactId>devices.web</artifactId>
         <version>${devices.web.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.opendaylight.controller</groupId>
+        <artifactId>filter-valve</artifactId>
+        <version>${filtervalve.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.opendaylight.controller</groupId>
         <artifactId>flowprogrammer.northbound</artifactId>
index 3802370aca3e8a7726d27d7fab1ca67d6eeb1944..3916e05496ab2a40fc34e2adb71fd53839172ceb 100644 (file)
           <groupId>org.opendaylight.controller</groupId>
           <artifactId>config-persister-impl</artifactId>
         </dependency>
+        <dependency>
+          <groupId>org.opendaylight.controller</groupId>
+          <artifactId>filter-valve</artifactId>
+        </dependency>
         <dependency>
           <groupId>org.opendaylight.controller</groupId>
           <artifactId>logback-config</artifactId>
diff --git a/opendaylight/distribution/opendaylight/src/main/resources/configuration/cors-config.xml b/opendaylight/distribution/opendaylight/src/main/resources/configuration/cors-config.xml
new file mode 100644 (file)
index 0000000..00abf6c
--- /dev/null
@@ -0,0 +1,54 @@
+<!--
+  ~ Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+
+<Host>
+    <!-- Filters are allowed here, only serving as a template -->
+    <filter-template>
+        <filter-name>CorsFilter</filter-name>
+        <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
+        <init-param>
+            <param-name>cors.allowed.origins</param-name>
+            <param-value>*</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.allowed.methods</param-name>
+            <param-value>GET,POST,HEAD,OPTIONS,PUT,DELETE</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.allowed.headers</param-name>
+            <param-value>Content-Type,X-Requested-With,accept,authorization,
+                origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers
+            </param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.exposed.headers</param-name>
+            <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.support.credentials</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>cors.preflight.maxage</param-name>
+            <param-value>10</param-value>
+        </init-param>
+    </filter-template>
+
+    <Context path="/restconf">
+        <filter>
+            <filter-name>CorsFilter</filter-name>
+            <!-- init params can be added/overriden if template is used -->
+        </filter>
+        <!-- references to templates without <filter> declaration are not allowed -->
+        <filter-mapping>
+            <filter-name>CorsFilter</filter-name>
+            <url-pattern>/*</url-pattern>
+        </filter-mapping>
+    </Context>
+
+</Host>
index 56d469b59932f591b93cd91eb7e491ab3c683085..da2500be622e48cf735f5ac349de8d96c4e39c96 100644 (file)
@@ -56,6 +56,9 @@
                         rotatable="true" fileDateFormat="yyyy-MM"
                         pattern="%{yyyy-MM-dd HH:mm:ss.SSS z}t - [%a] - %r"/>
 
+          <Valve className="org.opendaylight.controller.filtervalve.cors.FilterValve"
+                 configurationFile="configuration/cors-config.xml"
+                  />
       </Host>
     </Engine>
   </Service>
index 4b62bf7c2fcb2a1c219c7936063f721e1e7a2f9b..f39eae45424941355b2d9a7291d62eea744048a4 100644 (file)
         <url-pattern>/*</url-pattern>
     </servlet-mapping>
 
-    <filter>
-        <filter-name>CorsFilter</filter-name>
-        <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
-        <init-param>
-            <param-name>cors.allowed.origins</param-name>
-            <param-value>*</param-value>
-        </init-param>
-        <init-param>
-            <param-name>cors.allowed.methods</param-name>
-            <param-value>GET,POST,HEAD,OPTIONS,PUT,DELETE</param-value>
-        </init-param>
-        <init-param>
-            <param-name>cors.allowed.headers</param-name>
-            <param-value>Content-Type,X-Requested-With,accept,authorization,
-        origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
-        </init-param>
-        <init-param>
-            <param-name>cors.exposed.headers</param-name>
-            <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>
-        </init-param>
-        <init-param>
-            <param-name>cors.support.credentials</param-name>
-            <param-value>true</param-value>
-        </init-param>
-        <init-param>
-            <param-name>cors.preflight.maxage</param-name>
-            <param-value>10</param-value>
-        </init-param>
-    </filter>
-    <filter-mapping>
-        <filter-name>CorsFilter</filter-name>
-        <url-pattern>/*</url-pattern>
-    </filter-mapping>
     <security-constraint>
         <web-resource-collection>
             <web-resource-name>NB api</web-resource-name>
diff --git a/pom.xml b/pom.xml
index 3a3c3dcfb462b2d2a56341799760e9074added56..d75e582faa2be2319db162b6ab509f4feac8dac8 100644 (file)
--- a/pom.xml
+++ b/pom.xml
     <module>opendaylight/commons/opendaylight</module>
     <module>opendaylight/commons/parent</module>
     <module>opendaylight/commons/logback_settings</module>
+    <module>opendaylight/commons/filter-valve</module>
 
     <!-- Karaf Distribution -->
     <module>features/base</module>