/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-util/src/main/java/org/sakaiproject/util/RemoteHostFilter.java $
* $Id: RemoteHostFilter.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2005, 2006, 2008 Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
// Add the ability to look in sakai.properties for properties
import org.sakaiproject.component.cover.ServerConfigurationService;
/**
* This Servlet Filter allows/denies requests based on comparing the remote
* hostname and/or ip-address against a set of regular expressions configured in
* the init parameters.
* <p>
* The <code>allow</code> and/or <code>deny</code> properties are expected
* to be comma-delimited list of regular expressions indicating hostnames and/or
* ip addresses of allowed/denied hosts. Here is the evaluation logic:
* <ul>
* <li>The hostname and address are first compared to the deny expressions
* configured. If a match is found, the request is rejected with a "Forbidden"
* HTTP response.</li>
*
* <li>Next the hostname and address are compared to allow expressions. If a
* match is found, this request will be allowed.</li>
*
* <li>If one or more deny expressions were specified but no allow expressions
* were, allow this request to pass through (because none of the deny
* expressions matched it).
* <li>The request will be rejected with a "Forbidden" HTTP response.</li>
* </ul>
*
* To summarize, the pseudo-code looks like: <pre>
* if (explicitly denied) "Forbidden";
* else if (explicitly allowed) "Pass";
* else if (allow set is null, but deny is not) "Pass";
* else "Forbidden";
* </pre>
*
* <code>log-allowed</code> and <code>log-denied</code> may be specified to
* true/false to log allowed/denied requests. <code>log-allowed</code>
* defaults to false, and <code>log-denied</code> defaults to true;
*
* @author <a href="vgoenka@sungardsct.com">Vishal Goenka</a>
*
*/
public class RemoteHostFilter implements Filter {
// Our logger
private static Log M_log = LogFactory.getLog(RemoteHostFilter.class);
/**
* Define an empty pattern to save re-constructing a null array multiple
* times
*/
private static final Pattern[] EMPTY_PATTERN = new Pattern[0];
/**
* The comma-delimited set of allowed hosts (hostnames/addresses).
*/
protected String allowList = null;
/**
* The comma-delimited set of denied hosts (hostnames/addresses)
*/
protected String denyList = null;
/**
* The set of allowed hosts/addresses expressed as regular expressions
*/
protected Pattern[] allow = EMPTY_PATTERN;
/**
* The set of denied hosts/addresses expressed as regular expressions
*/
protected Pattern[] deny = EMPTY_PATTERN;
/**
* Should allowed requests be logged
*/
protected boolean logAllowed = false;
/**
* Should denied requests be logged
*/
protected boolean logDenied = true;
/**
* Read the allow/deny parameters and initialize patterns
*
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
public void init(FilterConfig config) throws ServletException {
allowList = getParameter(config,"allow");
if ( ":empty:".equals(allowList) ) allowList = null;
allow = getRegExPatterns(allowList);
logAllowed = Boolean.valueOf(getParameter(config,"log-allowed"))
.booleanValue();
denyList = getParameter(config,"deny");
if ( ":empty:".equals(denyList) ) denyList = null;
deny = getRegExPatterns(denyList);
logDenied = Boolean.valueOf(getParameter(config,"log-denied"))
.booleanValue();
}
private String getParameter(FilterConfig config, String parmName)
{
String retval = ServerConfigurationService.getString("webservices."+parmName, null);
if ( retval != null ) return retval;
return config.getInitParameter(parmName);
}
/*
* See class description above.
*
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilter(ServletRequest sreq, ServletResponse sres,
FilterChain chain) throws IOException, ServletException {
// we are expecting HTTP
if (!((sreq instanceof HttpServletRequest) && (sres instanceof HttpServletResponse))) {
// if not, just pass it through
chain.doFilter(sreq, sres);
return;
}
HttpServletRequest request = (HttpServletRequest) sreq;
HttpServletResponse response = (HttpServletResponse) sres;
String host = request.getRemoteHost();
String addr = request.getRemoteAddr();
// Check if explicit denied ...
for (int i = 0; i < deny.length; i++) {
if (deny[i].matcher(host).matches()
|| deny[i].matcher(addr).matches()) {
if (logDenied && M_log.isInfoEnabled())
M_log.info("Access denied (" + deny[i].pattern() + "): " + host + "/" + addr);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
}
// Check if explicitly allowed ...
for (int i = 0; i < allow.length; i++) {
if (allow[i].matcher(host).matches()
|| allow[i].matcher(addr).matches()) {
if (logAllowed && M_log.isInfoEnabled())
M_log.info("Access granted (" + allow[i].pattern() + "): " + host + "/" + addr);
chain.doFilter(sreq, sres);
return;
}
}
// Allow if allows is null, but denied is not
if ((deny.length > 0) && (allow.length == 0)) {
if (logAllowed && M_log.isInfoEnabled())
M_log.info("Access granted (implicit): " + host + "/" + addr);
chain.doFilter(sreq, sres);
return;
}
// Deny this request
if (logDenied && M_log.isInfoEnabled())
M_log.info("Access denied (implicit): " + host + "/" + addr);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
/**
* @see javax.servlet.Filter#destroy()
*/
public void destroy() {
// Do nothing
}
/**
* Converts the given list of comma-delimited regex patterns to an array of
* Pattern objects
*
* @param list
* The comma-separated list of patterns
*
* @exception IllegalArgumentException
* if one of the patterns has invalid regular expression
* syntax
*/
protected Pattern[] getRegExPatterns(String list) {
if (list == null)
return EMPTY_PATTERN;
list = list.trim();
if (list.length() < 1)
return EMPTY_PATTERN;
StringTokenizer st = new StringTokenizer(list, ",");
ArrayList<Pattern> patterns = new ArrayList<Pattern>();
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
try {
// Host names are case insensitive
patterns.add(Pattern.compile(token, Pattern.CASE_INSENSITIVE));
} catch (PatternSyntaxException e) {
throw new IllegalArgumentException(
"Illegal Regular Expression Syntax: [" + token + "] - "
+ e.getMessage());
}
}
return ((Pattern[]) patterns.toArray(EMPTY_PATTERN));
}
}