From 205310153be518be515ac0f64cc30bf0fd172905 Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Wed, 7 May 2014 14:23:08 +0200 Subject: [PATCH] Bug 951 - Externalize cors definition of restconf 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 --- opendaylight/commons/filter-valve/pom.xml | 81 ++++++++ .../filtervalve/cors/FilterValve.java | 94 +++++++++ .../filtervalve/cors/jaxb/Context.java | 113 ++++++++++ .../filtervalve/cors/jaxb/Filter.java | 194 ++++++++++++++++++ .../filtervalve/cors/jaxb/FilterMapping.java | 50 +++++ .../filtervalve/cors/jaxb/Host.java | 80 ++++++++ .../filtervalve/cors/jaxb/InitParam.java | 53 +++++ .../filtervalve/cors/jaxb/Parser.java | 25 +++ .../cors/model/FilterProcessor.java | 72 +++++++ .../filtervalve/cors/model/UrlMatcher.java | 96 +++++++++ .../filtervalve/cors/jaxb/DummyFilter.java | 33 +++ .../filtervalve/cors/jaxb/MockedFilter.java | 39 ++++ .../filtervalve/cors/jaxb/ParserTest.java | 70 +++++++ .../cors/model/UrlMatcherTest.java | 47 +++++ .../src/test/resources/conflicting-class.xml | 34 +++ .../src/test/resources/no-filter-defined.xml | 61 ++++++ .../src/test/resources/sample-cors-config.xml | 37 ++++ opendaylight/commons/opendaylight/pom.xml | 6 + .../distribution/opendaylight/pom.xml | 4 + .../resources/configuration/cors-config.xml | 54 +++++ .../resources/configuration/tomcat-server.xml | 3 + .../src/main/resources/WEB-INF/web.xml | 33 --- pom.xml | 1 + 23 files changed, 1247 insertions(+), 33 deletions(-) create mode 100644 opendaylight/commons/filter-valve/pom.xml create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/FilterValve.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Context.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Filter.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/FilterMapping.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Host.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/InitParam.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Parser.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/FilterProcessor.java create mode 100644 opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcher.java create mode 100644 opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/DummyFilter.java create mode 100644 opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/MockedFilter.java create mode 100644 opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/ParserTest.java create mode 100644 opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcherTest.java create mode 100644 opendaylight/commons/filter-valve/src/test/resources/conflicting-class.xml create mode 100644 opendaylight/commons/filter-valve/src/test/resources/no-filter-defined.xml create mode 100644 opendaylight/commons/filter-valve/src/test/resources/sample-cors-config.xml create mode 100644 opendaylight/distribution/opendaylight/src/main/resources/configuration/cors-config.xml diff --git a/opendaylight/commons/filter-valve/pom.xml b/opendaylight/commons/filter-valve/pom.xml new file mode 100644 index 0000000000..7b5be02514 --- /dev/null +++ b/opendaylight/commons/filter-valve/pom.xml @@ -0,0 +1,81 @@ + + + + 4.0.0 + + org.opendaylight.controller + commons.opendaylight + 1.4.2-SNAPSHOT + ../opendaylight + + filter-valve + bundle + + + + com.google.guava + guava + + + commons-io + commons-io + + + equinoxSDK381 + javax.servlet + + + orbit + org.apache.catalina + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + test + + + junit + junit + test + + + + + + + org.apache.felix + maven-bundle-plugin + + + org.eclipse.gemini.web.tomcat + 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 + + + + + org.opendaylight.yangtools + yang-maven-plugin + + + + + 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 index 0000000000..54d8be11aa --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/FilterValve.java @@ -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 index 0000000000..dbe0745725 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Context.java @@ -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 filters = new ArrayList<>(); + private List filterMappings = new ArrayList<>(); + private boolean initialized; + private UrlMatcher urlMatcher; + + + public synchronized void initialize(String fileName, Map namesToTemplates) { + checkState(initialized == false, "Already initialized"); + Map 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 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 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 getFilters() { + return filters; + } + + public void setFilters(List filters) { + checkArgument(initialized == false, "Already initialized"); + this.filters = filters; + } + + @XmlElement(name = "filter-mapping") + public List getFilterMappings() { + return filterMappings; + } + + public void setFilterMappings(List 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 index 0000000000..3dde5b1cfa --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Filter.java @@ -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 initParams = new ArrayList<>(); + private javax.servlet.Filter actualFilter; + private boolean initialized, isTemplate; + + + /** + * Called in filter-template nodes defined in 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 + */ + 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 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 templateParams = template.getInitParamsMap(); + Map currentParams = getInitParamsMap(); + // add values of template that are not present in current + MapDifference difference = Maps.difference(templateParams, currentParams); + for (Entry 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 getInitParameterNames() { + final Iterator iterator = initParams.iterator(); + return new Enumeration() { + @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 getInitParams() { + return initParams; + } + + public void setInitParams(List initParams) { + this.initParams = initParams; + } + + + @Override + public String toString() { + return "Filter{" + + "filterName='" + filterName + '\'' + + '}'; + } + + public Map getInitParamsMap() { + Map 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 index 0000000000..03fcbf26ce --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/FilterMapping.java @@ -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 index 0000000000..4e3c3ba1df --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Host.java @@ -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 contexts = new ArrayList<>(); + private List filterTemplates = new ArrayList<>(); + private boolean initialized; + private Map contextMap; + + + public synchronized void initialize(String fileName) { + checkState(initialized == false, "Already initialized"); + Map 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 findContext(String contextPath) { + checkState(initialized, "Not initialized"); + Context context = contextMap.get(contextPath); + return Optional.fromNullable(context); + } + + @XmlElement(name = "Context") + public List getContexts() { + return contexts; + } + + public void setContexts(List contexts) { + checkArgument(initialized == false, "Already initialized"); + this.contexts = contexts; + } + + @XmlElement(name = "filter-template") + public List getFilterTemplates() { + return filterTemplates; + } + + public void setFilterTemplates(List 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 index 0000000000..edc9e4560d --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/InitParam.java @@ -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 index 0000000000..bc4f12e157 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/jaxb/Parser.java @@ -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 index 0000000000..dc3e9dcd49 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/FilterProcessor.java @@ -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 maybeContext = host.findContext(contextPath); + logger.trace("Processing context {} path {}, found {}", contextPath, pathInfo, maybeContext); + if (maybeContext.isPresent()) { + // process filters + Context context = maybeContext.get(); + List matchingFilters = context.findMatchingFilters(pathInfo); + FilterChain fromLast = nextValveFilterChain; + ListIterator 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 index 0000000000..9535fb1f70 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/main/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcher.java @@ -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: + *
    + *
  • A string beginning with a ‘/’ character and ending with a ‘/*’ suffix is used for path mapping.
  • + *
  • A string beginning with a ‘*.’ prefix is used as an extension mapping.
  • + *
  • All other strings are used for exact matches only.
  • + *
+ */ +public class UrlMatcher { + private static final Logger logger = LoggerFactory.getLogger(UrlMatcher.class); + // order index for each FILTER is kept as Entry.value + private final Map> prefixMap = new HashMap<>(); // contains patterns ending with '/*', '*' is stripped from each key + private final Map> suffixMap = new HashMap<>(); // contains patterns starting with '*.' prefix, '*' is stripped from each key + private final Map> exactMatchMap = new HashMap<>(); // contains exact matches only + + /** + * @param patternMap order preserving map containing path info pattern as key + */ + public UrlMatcher(LinkedHashMap patternMap) { + int idx = 0; + for (Entry entry : patternMap.entrySet()) { + idx++; + String pattern = checkNotNull(entry.getKey()); + FILTER value = entry.getValue(); + Entry 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 findMatchingFilters(String pathInfo) { + checkNotNull(pathInfo); + TreeMap sortedMap = new TreeMap<>(); + // add matching prefixes + for (Entry> prefixEntry : prefixMap.entrySet()) { + if (pathInfo.startsWith(prefixEntry.getKey())) { + put(sortedMap, prefixEntry.getValue()); + } + } + // add matching suffixes + for (Entry> suffixEntry : suffixMap.entrySet()) { + if (pathInfo.endsWith(suffixEntry.getKey())) { + put(sortedMap, suffixEntry.getValue()); + } + } + // add exact match + Entry exactMatch = exactMatchMap.get(pathInfo); + if (exactMatch != null) { + put(sortedMap, exactMatch); + } + ArrayList filters = new ArrayList<>(sortedMap.values()); + logger.trace("Matching filters for path {} are {}", pathInfo, filters); + return filters; + } + + private void put(TreeMap sortedMap, Entry 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 index 0000000000..d14caf9a86 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/DummyFilter.java @@ -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 index 0000000000..56d851bc3d --- /dev/null +++ b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/MockedFilter.java @@ -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 index 0000000000..fc6c01b381 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/jaxb/ParserTest.java @@ -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 = 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 index 0000000000..07f6354b19 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/test/java/org/opendaylight/controller/filtervalve/cors/model/UrlMatcherTest.java @@ -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 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 patternMap = new LinkedHashMap() { + { + 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 index 0000000000..c1faf34b38 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/test/resources/conflicting-class.xml @@ -0,0 +1,34 @@ + + + + + CorsFilter + org.opendaylight.controller.filtervalve.cors.jaxb.MockedFilter + + cors.preflight.maxage + 10 + + + cors.allowed.origins + * + + + + + + CorsFilter + + org.opendaylight.controller.filtervalve.cors.jaxb.DummyFilter + + + CorsFilter + /* + + + 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 index 0000000000..521d578da5 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/test/resources/no-filter-defined.xml @@ -0,0 +1,61 @@ + + + + + + CorsFilter + org.opendaylight.controller.filtervalve.cors.jaxb.MockedFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT,DELETE + + + 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 + + + + + CorsFilter + /* + + + + + + CorsFilter + /* + + + 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 index 0000000000..613dc825d7 --- /dev/null +++ b/opendaylight/commons/filter-valve/src/test/resources/sample-cors-config.xml @@ -0,0 +1,37 @@ + + + + + CorsFilter + org.opendaylight.controller.filtervalve.cors.jaxb.MockedFilter + + cors.preflight.maxage + 10 + + + cors.allowed.origins + * + + + + + + CorsFilter + + + cors.preflight.maxage + 11 + + + + CorsFilter + /* + + + diff --git a/opendaylight/commons/opendaylight/pom.xml b/opendaylight/commons/opendaylight/pom.xml index 077b452f0a..70a3d28ce8 100644 --- a/opendaylight/commons/opendaylight/pom.xml +++ b/opendaylight/commons/opendaylight/pom.xml @@ -65,6 +65,7 @@ 3.1.0 3.1.6 4.2.0 + 1.4.2-SNAPSHOT 0.4.2-SNAPSHOT 0.4.2-SNAPSHOT 0.5.2-SNAPSHOT @@ -821,6 +822,11 @@ devices.web ${devices.web.version} + + org.opendaylight.controller + filter-valve + ${filtervalve.version} + org.opendaylight.controller flowprogrammer.northbound diff --git a/opendaylight/distribution/opendaylight/pom.xml b/opendaylight/distribution/opendaylight/pom.xml index 3802370aca..3916e05496 100644 --- a/opendaylight/distribution/opendaylight/pom.xml +++ b/opendaylight/distribution/opendaylight/pom.xml @@ -862,6 +862,10 @@ org.opendaylight.controller config-persister-impl + + org.opendaylight.controller + filter-valve + org.opendaylight.controller logback-config 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 index 0000000000..00abf6cf33 --- /dev/null +++ b/opendaylight/distribution/opendaylight/src/main/resources/configuration/cors-config.xml @@ -0,0 +1,54 @@ + + + + + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT,DELETE + + + 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 + + + + + CorsFilter + /* + + + + diff --git a/opendaylight/distribution/opendaylight/src/main/resources/configuration/tomcat-server.xml b/opendaylight/distribution/opendaylight/src/main/resources/configuration/tomcat-server.xml index 56d469b599..da2500be62 100644 --- a/opendaylight/distribution/opendaylight/src/main/resources/configuration/tomcat-server.xml +++ b/opendaylight/distribution/opendaylight/src/main/resources/configuration/tomcat-server.xml @@ -56,6 +56,9 @@ rotatable="true" fileDateFormat="yyyy-MM" pattern="%{yyyy-MM-dd HH:mm:ss.SSS z}t - [%a] - %r"/> +
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/resources/WEB-INF/web.xml b/opendaylight/md-sal/sal-rest-connector/src/main/resources/WEB-INF/web.xml index 4b62bf7c2f..f39eae4542 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/resources/WEB-INF/web.xml +++ b/opendaylight/md-sal/sal-rest-connector/src/main/resources/WEB-INF/web.xml @@ -18,39 +18,6 @@ /* - - CorsFilter - org.apache.catalina.filters.CorsFilter - - cors.allowed.origins - * - - - cors.allowed.methods - GET,POST,HEAD,OPTIONS,PUT,DELETE - - - 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 diff --git a/pom.xml b/pom.xml index 3a3c3dcfb4..d75e582faa 100644 --- a/pom.xml +++ b/pom.xml @@ -122,6 +122,7 @@ opendaylight/commons/opendaylight opendaylight/commons/parent opendaylight/commons/logback_settings + opendaylight/commons/filter-valve features/base -- 2.36.6