/*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.admin;
import org.apache.mina.util.CopyOnWriteMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* A servlet filter that plugin classes can use to dynamically register and un-register filter logic.
*
* The original, now deprecated, filter logic that each plugin can register was fairly limited; instead of having full
* control over the filter chain, each instance of {@link SimpleFilter} only has the ability to use the ServletRequest
* and ServletResponse objects and then return <tt>true</tt> if further filters in the chain should be run.
*
* The new, non-deprecated functionality allows for regular {@link Filter} instances to be registered with this class,
* which removes much of the limitations that was present in the SimpleFilter approach.
*
* This implementation assumes, but does not enforce, that filters installed by plugins are applied to URL patterns that
* match the plugin. When filters installed by different plugins are applied to the same URL, the behavior of this
* implementation is undetermined.
*
* @author Matt Tucker
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class PluginFilter implements Filter {
private static final Logger Log = LoggerFactory.getLogger( PluginFilter.class );
@Deprecated
private static List<SimpleFilter> pluginFilters = new CopyOnWriteArrayList<>();
private static Map<String, List<Filter>> filters = new CopyOnWriteMap<>();
/**
* Adds a filter to the list of filters that will be run on every request.
* This method should be called by plugins when starting up.
*
* @param filter the filter.
* @deprecated Replaced by {@link #addPluginFilter(String, Filter)}
*/
@Deprecated
public static void addPluginFilter(SimpleFilter filter) {
pluginFilters.add(filter);
}
/**
* Adds a filter to the list of filters that will be run on every request of which the URL matches the URL that
* is registered with this filter. More specifically, the request URL should be equal to, or start with, the filter
* URL.
*
* Multiple filters can be registered on the same URL, in which case they will be executed in the order in which
* they were added.
*
* Adding a filter does not initialize the plugin instance.
*
* @param filterUrl The URL pattern to which the filter is to be applied. Cannot be null nor an empty string.
* @param filter The filter. Cannot be null.
*/
public static void addPluginFilter( String filterUrl, Filter filter )
{
if ( filterUrl == null || filterUrl.isEmpty() || filter == null )
{
throw new IllegalArgumentException();
}
if ( !filters.containsKey( filterUrl ) )
{
filters.put( filterUrl, new ArrayList<Filter>() );
}
final List<Filter> urlFilters = PluginFilter.filters.get( filterUrl );
if ( urlFilters.contains( filter ) )
{
Log.warn( "Cannot add filter '{}' as it was already added for URL '{}'!", filter, filterUrl );
}
else
{
urlFilters.add( filter );
Log.debug( "Added filter '{}' for URL '{}'", filter, filterUrl );
}
}
/**
* Removes a filter from the list of filters that will be run on every request.
* This method should be called by plugins when shutting down.
*
* @param filter the filter.
* @deprecated
*/
@Deprecated
public static void removePluginFilter(SimpleFilter filter) {
pluginFilters.remove(filter);
}
/**
* Removes a filter that is applied to a certain URL.
*
* Removing a filter does not destroy the plugin instance.
*
* @param filterUrl The URL pattern to which the filter is applied. Cannot be null nor an empty string.
* @param filterClassName The filter class name. Cannot be null or empty string.
* @return The filter instance that was removed, or null if the URL and name combination did not match a filter.
*/
public static Filter removePluginFilter( String filterUrl, String filterClassName )
{
if ( filterUrl == null || filterUrl.isEmpty() || filterClassName == null || filterClassName.isEmpty() )
{
throw new IllegalArgumentException();
}
Filter result = null;
if ( filters.containsKey( filterUrl ) )
{
final List<Filter> urlFilters = PluginFilter.filters.get( filterUrl );
final Iterator<Filter> iterator = urlFilters.iterator();
while ( iterator.hasNext() )
{
final Filter filter = iterator.next();
if ( filter.getClass().getName().equals( filterClassName ) )
{
iterator.remove();
result = filter; // assumed to be unique, but check the entire collection to avoid leaks.
}
}
if ( urlFilters.isEmpty() )
{
filters.remove( filterUrl );
}
}
if ( result == null )
{
Log.warn( "Unable to removed filter of class '{}' for URL '{}'. No such filter is present.", filterClassName, filterUrl );
}
else
{
Log.debug( "Removed filter '{}' for URL '{}'", result, filterUrl );
}
return result;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
/**
* This class is a Filter implementation itself. It acts as a dynamic proxy to filters that are registered by
* Openfire plugins.
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException
{
boolean continueChain = true;
// Process each of the (deprecated) SimplePlugin filters.
for ( SimpleFilter filter : pluginFilters ) {
Log.trace( "(deprecated) Executing wrapped simple filter '{}'...", filter );
if (!filter.doFilter(servletRequest, servletResponse)) {
Log.debug( "The simple filter returned false so no further filters in the chain should be run." );
continueChain = false;
break;
}
}
// Process the 'regular' servlet filters.
if ( continueChain )
{
if ( servletRequest instanceof HttpServletRequest )
{
final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
final String requestPath = ( httpServletRequest.getContextPath() + httpServletRequest.getServletPath() + httpServletRequest.getPathInfo() ).toLowerCase();
final List<Filter> applicableFilters = new ArrayList<>();
for ( final Map.Entry<String, List<Filter>> entry : filters.entrySet() )
{
String filterUrl = entry.getKey();
if ( filterUrl.endsWith( "*" ))
{
filterUrl = filterUrl.substring( 0, filterUrl.length() -1 );
}
filterUrl = filterUrl.toLowerCase();
if ( requestPath.startsWith( filterUrl ) )
{
for ( final Filter filter : entry.getValue() )
{
applicableFilters.add( filter );
}
}
}
if ( !applicableFilters.isEmpty() )
{
Log.debug( "Wrapping filter chain in order to run plugin-specific filters." );
filterChain = new FilterChainInjector( filterChain, applicableFilters );
}
}
else
{
Log.warn( "ServletRequest is not an instance of an HttpServletRequest." );
}
// Plugin filtering is done. Progress down the filter chain that was initially provided.
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
// If the destroy method is being called, the Openfire instance is being shutdown.
// Therefore, clear out the list of plugin filters.
pluginFilters.clear();
filters.clear();
}
/**
* A simplified version of a servlet filter. Instead of having full control
* over the filter chain, a simple filter can only control whether further
* filters in the chain are run.
*
* @deprecated Use {@link Filter} instead.
*/
@Deprecated
public interface SimpleFilter {
/**
* The doFilter method of the Filter is called by the PluginFilter each time a
* request/response pair is passed through the chain due to a client request
* for a resource at the end of the chain. This method should return <tt>true</tt> if
* the additional filters in the chain should be processed or <tt>false</tt>
* if no additional filters should be run.<p>
*
* Note that the filter will apply to all requests for JSP pages in the admin console
* and not just requests in the respective plugins. To only apply filtering to
* individual plugins, examine the context path of the request and only filter
* relevant requests.
*
* @param request the servlet request.
* @param response the servlet response
* @throws IOException if an IOException occurs.
* @throws ServletException if a servlet exception occurs.
* @return true if further filters in the chain should be run.
*/
boolean doFilter( ServletRequest request, ServletResponse response )
throws IOException, ServletException;
}
/**
* A wrapper that can be used to inject a list of filters into an existing a filter chain.
*
* An instance of this class is expected to be created within the execution of a 'parent' filter chain. After
* instantiation, the caller is expected to invoke #doFilter once, after which all provided filters will be
* invoked. Afterwards, the original filter chain (as supplied in the constructor) will be resumed.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
private static class FilterChainInjector implements FilterChain
{
private static final Logger Log = LoggerFactory.getLogger( FilterChainInjector.class );
private final FilterChain parentChain;
private final List<Filter> filters;
private int index = 0;
/**
* Creates a new instance.
*
* @param parentChain the chain to which the filters are to be appended (cannot be null).
* @param filters The filters to append (cannot be null, but can be empty).
*/
private FilterChainInjector( FilterChain parentChain, List<Filter> filters )
{
if ( parentChain == null || filters == null )
{
throw new IllegalArgumentException();
}
this.parentChain = parentChain;
this.filters = filters;
}
@Override
public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse ) throws IOException, ServletException
{
if ( index < filters.size() )
{
Log.trace( "Executing injected filter {} of {}...", index + 1, filters.size() );
filters.get( index++ ).doFilter( servletRequest, servletResponse, this );
}
else
{
Log.trace( "Executed all injected filters. Resuming original chain." );
parentChain.doFilter( servletRequest, servletResponse );
}
}
}
}