001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.web.filter;
017
018import org.apache.log4j.Logger;
019import org.kuali.rice.core.api.config.property.Config;
020import org.kuali.rice.core.api.config.property.ConfigContext;
021import org.kuali.rice.core.api.reflect.ObjectDefinition;
022import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
023import org.kuali.rice.core.api.util.ClassLoaderUtils;
024
025import javax.servlet.Filter;
026import javax.servlet.FilterChain;
027import javax.servlet.FilterConfig;
028import javax.servlet.ServletContext;
029import javax.servlet.ServletException;
030import javax.servlet.ServletRequest;
031import javax.servlet.ServletResponse;
032import javax.servlet.http.HttpServletRequest;
033import java.io.IOException;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Collections;
037import java.util.Enumeration;
038import java.util.HashMap;
039import java.util.Iterator;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Map;
043import java.util.SortedSet;
044import java.util.TreeSet;
045
046/**
047 * A filter which at runtime reads a series of filter configurations, constructs
048 * and initializes those filters, and invokes them when it is invoked. This
049 * allows runtime user configuration of arbitrary filters in the webapp context.
050 *
051 * <p>
052 * Note : filter mapping order numbers must be unique across all filters. Filter exclusions will do a regex match
053 * against the full path of the request.
054 * </p>
055 *
056 * @author Kuali Rice Team (rice.collab@kuali.org)
057 */
058public class BootstrapFilter implements Filter {
059        private static final Logger LOG = Logger.getLogger(BootstrapFilter.class);
060
061        private static final String FILTER_PREFIX = "filter.";
062
063        private static final String CLASS_SUFFIX = ".class";
064
065        private static final String FILTER_MAPPING_PREFIX = "filtermapping.";
066
067    private static final String FILTER_EXCLUDE_PREFIX = "filterexclude.";
068
069        private FilterConfig config;
070
071        private final Map<String, Filter> filters = new HashMap<String, Filter>();
072
073        private final SortedSet<FilterMapping> filterMappings = new TreeSet<FilterMapping>();
074
075    private final Map<String, ArrayList<String>> filterExclusions = new HashMap<String, ArrayList<String>>();
076
077        private boolean initted = false;
078
079        public void init(FilterConfig cfg) throws ServletException {
080                this.config = cfg;
081        }
082
083        private void addFilter(String name, String classname, Map<String, String> props) throws ServletException {
084                LOG.debug("Adding filter: " + name + "=" + classname);
085                Object filterObject = GlobalResourceLoader.getResourceLoader().getObject(new ObjectDefinition(classname));
086                if (filterObject == null) {
087                        throw new ServletException("Filter '" + name + "' class not found: " + classname);
088
089                }
090                if (!(filterObject instanceof Filter)) {
091                        LOG.error("Class '" + filterObject.getClass() + "' does not implement servlet javax.servlet.Filter");
092                        return;
093                }
094                Filter filter = (Filter) filterObject;
095                BootstrapFilterConfig fc = new BootstrapFilterConfig(config.getServletContext(), name);
096                for (Map.Entry<String, String> entry : props.entrySet()) {
097                        String key = entry.getKey().toString();
098                        final String prefix = FILTER_PREFIX + name + ".";
099                        if (!key.startsWith(prefix) || key.equals(FILTER_PREFIX + name + CLASS_SUFFIX)) {
100                                continue;
101                        }
102                        String paramName = key.substring(prefix.length());
103                        fc.addInitParameter(paramName, entry.getValue());
104                }
105                try {
106                        filter.init(fc);
107                        filters.put(name, filter);
108                } catch (ServletException se) {
109                        LOG.error("Error initializing filter: " + name + " [" + classname + "]", se);
110                }
111        }
112
113        private void addFilterMapping(String filterName, String orderNumber, String value) {
114                filterMappings.add(new FilterMapping(filterName, orderNumber, value));
115        }
116
117    /**
118     * Adds an exclusion to the exclusion list for a filter
119     *
120     * <p>
121     * If this is the first exclusion to be added, the list will be created and added to the exclusion map
122     * </p>
123     *
124     * @param filterName - name of the filter
125     * @param exclusion - exclusion string
126     */
127    private void addFilterExclusion(String filterName, String exclusion) {
128        if (filterExclusions.containsKey(filterName)) {            
129            filterExclusions.get(filterName).add(exclusion);
130        } else {
131            filterExclusions.put(filterName, new ArrayList(Arrays.asList(exclusion)));
132        }
133    }
134
135        private synchronized void init() throws ServletException {
136                if (initted) {
137                        return;
138                }
139                LOG.debug("initializing...");
140                Config cfg = ConfigContext.getCurrentContextConfig();
141                
142                @SuppressWarnings({ "unchecked", "rawtypes" })
143                final Map<String, String> p = new HashMap<String, String>((Map) cfg.getProperties());
144                
145                for (Map.Entry<String, String> entry : p.entrySet()) {
146                        String key = entry.getKey().toString();
147                        if (key.startsWith(FILTER_MAPPING_PREFIX)) {
148                                String[] values = key.split("\\.");
149                                if (values.length != 2 && values.length != 3) {
150                                        throw new ServletException("Invalid filter mapping defined.  Should contain 2 or 3 pieces in the form of filtermapping.<<filter name>>.<<order number>> with the last piece optional.");
151                                }
152                                String filterName = values[1];
153                                String orderNumber = (values.length == 2 ? "0" : values[2]);
154                                String value = entry.getValue();
155                                addFilterMapping(filterName, orderNumber, value);
156                        } else if (key.startsWith(FILTER_PREFIX) && key.endsWith(CLASS_SUFFIX)) {
157                                String name = key.substring(FILTER_PREFIX.length(), key.length() - CLASS_SUFFIX.length());
158                                String value = entry.getValue();
159                                // ClassLoader cl =
160                                // SpringServiceLocator.getPluginRegistry().getInstitutionPlugin().getClassLoader();
161                                // addFilter(name, value, cl, p);
162                                addFilter(name, value, p);
163                        } else if (key.startsWith(FILTER_EXCLUDE_PREFIX)) {
164                String[] values = key.split("\\.");
165                if (values.length != 2 && values.length != 3) {
166                    throw new ServletException("Invalid filter mapping defined.  Should contain 2 or 3 pieces in the form of filterexclusion.<<filter name>>.<<number>> with the last piece optional.");
167                }
168                String filterName = values[1];
169                String value = entry.getValue();
170                addFilterExclusion(filterName, value);
171            }
172                }
173                // do a diff log a warn if any filter has no mappings
174                for (String filterName : filters.keySet()) {
175                        if (!hasFilterMapping(filterName)) {
176                                LOG.warn("NO FILTER MAPPING DETECTED.  Filter " + filterName + " has no mapping and will not be called.");
177                        }
178                }
179                initted = true;
180        }
181
182        private boolean hasFilterMapping(String filterName) {
183                for (FilterMapping filterMapping : filterMappings) {
184                        if (filterMapping.getFilterName().equals(filterName)) {
185                                return true;
186                        }
187                }
188                return false;
189        }
190
191        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
192            LOG.debug("Begin BootstrapFilter...");
193                init();
194                // build the filter chain and execute it
195                if (!filterMappings.isEmpty() && request instanceof HttpServletRequest) {
196                        chain = buildChain((HttpServletRequest) request, chain);
197                }
198                LOG.debug("...ending BootstrapFilter preperation, executing BootstrapFilter Chain.");
199                chain.doFilter(request, response);
200
201        }
202
203        private FilterChain buildChain(HttpServletRequest request, FilterChain targetChain) {
204                BootstrapFilterChain chain = new BootstrapFilterChain(targetChain, ClassLoaderUtils.getDefaultClassLoader());
205                String requestPath = request.getServletPath();
206                for (FilterMapping mapping : filterMappings) {
207                        Filter filter = filters.get(mapping.getFilterName());
208                        if (!chain.containsFilter(filter) && matchFiltersURL(mapping.getUrlPattern(), requestPath) 
209                    && !excludeFilter(mapping.getFilterName(), request.getRequestURL().toString())) {
210                chain.addFilter(filter);
211                        }
212                }
213                return chain;
214        }
215
216    /**
217     * Returns true if the path matches the exclusion regex
218     *
219     * @param filterName - filter name
220     * @param requestPath - full path of request
221     * @return boolean
222     */
223    private boolean excludeFilter(String filterName, String requestPath) {
224        if (filterExclusions.containsKey(filterName)) {
225            for (String exclusionString : filterExclusions.get(filterName)) {
226                if (requestPath.matches(exclusionString)) {
227                    return true;
228                }
229            }
230        }
231        return false;    
232    }
233
234        public void destroy() {
235                for (Filter filter : filters.values()) {
236                        try {
237                                filter.destroy();
238                        } catch (Exception e) {
239                                LOG.error("Error destroying filter: " + filter, e);
240                        }
241                }
242        }
243
244        /**
245         * This method was borrowed from the Tomcat codebase.
246         */
247        private boolean matchFiltersURL(String urlPattern, String requestPath) {
248
249                if (requestPath == null) {
250                        return (false);
251                }
252
253                // Match on context relative request path
254                if (urlPattern == null) {
255                        return (false);
256                }
257
258                // Case 1 - Exact Match
259                if (urlPattern.equals(requestPath)) {
260                        return (true);
261                }
262
263                // Case 2 - Path Match ("/.../*")
264                if (urlPattern.equals("/*") || urlPattern.equals("*")) {
265                        return (true);
266                }
267                if (urlPattern.endsWith("/*")) {
268                        if (urlPattern.regionMatches(0, requestPath, 0, urlPattern.length() - 2)) {
269                                if (requestPath.length() == (urlPattern.length() - 2)) {
270                                        return (true);
271                                } else if ('/' == requestPath.charAt(urlPattern.length() - 2)) {
272                                        return (true);
273                                }
274                        }
275                        return (false);
276                }
277
278                // Case 3 - Extension Match
279                if (urlPattern.startsWith("*.")) {
280                        int slash = requestPath.lastIndexOf('/');
281                        int period = requestPath.lastIndexOf('.');
282                        if ((slash >= 0) && (period > slash) && (period != requestPath.length() - 1) && ((requestPath.length() - period) == (urlPattern.length() - 1))) {
283                                return (urlPattern.regionMatches(2, requestPath, period + 1, urlPattern.length() - 2));
284                        }
285                }
286
287                // Case 4 - "Default" Match
288                return (false); // NOTE - Not relevant for selecting filters
289
290        }
291
292}
293
294/**
295 * A filter chain that invokes a series of filters with which it was
296 * initialized, and then delegates to a target filterchain.
297 *
298 * @author Kuali Rice Team (rice.collab@kuali.org)
299 */
300class BootstrapFilterChain implements FilterChain {
301
302        private final List<Filter> filters = new LinkedList<Filter>();
303
304        private final FilterChain target;
305
306        private Iterator<Filter> filterIterator;
307
308        private ClassLoader originalClassLoader;
309
310        public BootstrapFilterChain(FilterChain target, ClassLoader originalClassLoader) {
311                this.target = target;
312                this.originalClassLoader = originalClassLoader;
313        }
314
315        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
316                if (filterIterator == null) {
317                        filterIterator = filters.iterator();
318                }
319                if (filterIterator.hasNext()) {
320                        (filterIterator.next()).doFilter(request, response, this);
321                } else {
322                        // reset the CCL to the original classloader before calling the non
323                        // workflow configured filter - this makes it so our
324                        // CCL is the webapp classloader in workflow action classes and the
325                        // code they call
326                        Thread.currentThread().setContextClassLoader(originalClassLoader);
327                        target.doFilter(request, response);
328                }
329        }
330
331        public void addFilter(Filter filter) {
332                filters.add(filter);
333        }
334
335        public boolean containsFilter(Filter filter) {
336                return filters.contains(filter);
337        }
338
339        public boolean isEmpty() {
340                return filters.isEmpty();
341        }
342
343}
344
345/**
346 * Borrowed from spring-mock.
347 *
348 * @author Kuali Rice Team (rice.collab@kuali.org)
349 */
350class BootstrapFilterConfig implements FilterConfig {
351
352        private final ServletContext servletContext;
353
354        private final String filterName;
355
356        private final Map<String, String> initParameters = new HashMap<String, String>();
357
358        public BootstrapFilterConfig() {
359                this(null, "");
360        }
361
362        public BootstrapFilterConfig(String filterName) {
363                this(null, filterName);
364        }
365
366        public BootstrapFilterConfig(ServletContext servletContext) {
367                this(servletContext, "");
368        }
369
370        public BootstrapFilterConfig(ServletContext servletContext, String filterName) {
371                this.servletContext = servletContext;
372                this.filterName = filterName;
373        }
374
375        public String getFilterName() {
376                return filterName;
377        }
378
379        public ServletContext getServletContext() {
380                return servletContext;
381        }
382
383        public void addInitParameter(String name, String value) {
384                this.initParameters.put(name, value);
385        }
386
387        public String getInitParameter(String name) {
388                return this.initParameters.get(name);
389        }
390
391        public Enumeration<String> getInitParameterNames() {
392                return Collections.enumeration(this.initParameters.keySet());
393        }
394
395}
396
397class FilterMapping implements Comparable<FilterMapping> {
398
399        private String filterName;
400
401        private String orderValue;
402
403        private String urlPattern;
404
405        public FilterMapping(String filterName, String orderValue, String urlPattern) {
406                this.filterName = filterName;
407                this.orderValue = orderValue;
408                this.urlPattern = urlPattern;
409        }
410
411        public int compareTo(FilterMapping object) {
412                return orderValue.compareTo(object.orderValue);
413        }
414
415        public String getFilterName() {
416                return filterName;
417        }
418
419        public String getOrderValue() {
420                return orderValue;
421        }
422
423        public String getUrlPattern() {
424                return urlPattern;
425        }
426
427}