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}