/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.sling.security.impl;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
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.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyUnbounded;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(metatype=true, description="%referrer.description",
label="%referrer.name")
@Properties({
@Property(name=HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, value="/", propertyPrivate=true),
@Property(name=HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT,
value="(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)", propertyPrivate=true)
})
@Service(value=Filter.class)
public class ReferrerFilter implements Filter {
/**
* Request header providing the clients user agent information used
* by {@link #isBrowserRequest(HttpServletRequest)} to decide whether
* a request is probably sent by a browser or not.
*/
private static final String USER_AGENT = "User-Agent";
/**
* String contained in a {@link #USER_AGENT} header indicating a Mozilla
* class browser. Examples of such browsers are Firefox (generally Gecko
* based browsers), Safari, Chrome (probably generally WebKit based
* browsers), and Microsoft IE.
*/
private static final String BROWSER_CLASS_MOZILLA = "Mozilla";
/**
* String contained in a {@link #USER_AGENT} header indicating a Opera class
* browser. The only known browser in this class is the Opera browser.
*/
private static final String BROWSER_CLASS_OPERA = "Opera";
/** Logger. */
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** Default value for allow empty. */
private static final boolean DEFAULT_ALLOW_EMPTY = false;
/** Allow empty property. */
@Property(boolValue=DEFAULT_ALLOW_EMPTY)
private static final String PROP_ALLOW_EMPTY = "allow.empty";
private static final String[] DEFAULT_PROP_HOSTS = {};
/** Allow referrer uri hosts property. */
@Property(unbounded=PropertyUnbounded.ARRAY)
private static final String PROP_HOSTS = "allow.hosts";
/** Allow referrer regex hosts property */
@Property(unbounded=PropertyUnbounded.ARRAY)
private static final String PROP_HOSTS_REGEX = "allow.hosts.regexp";
/** Filtered methods property */
@Property(unbounded=PropertyUnbounded.ARRAY, value={"POST", "PUT", "DELETE"})
private static final String PROP_METHODS = "filter.methods";
/** Do we allow empty referrer? */
private boolean allowEmpty;
/** Allowed uri referrers */
private URL[] allowedUriReferrers;
/** Allowed regexp referrers */
private Pattern[] allowedRegexReferrers;
/** Methods to be filtered. */
private String[] filterMethods;
private ServiceRegistration<Object> configPrinterRegistration;
/**
* Create a default list of referrers
*/
private Set<String> getDefaultAllowedReferrers() {
final Set<String> referrers = new HashSet<String>();
try {
final Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();
while(ifaces.hasMoreElements()){
final NetworkInterface iface = ifaces.nextElement();
logger.info("Adding Allowed referers for Interface:" + iface.getDisplayName());
final Enumeration<InetAddress> ias = iface.getInetAddresses();
while(ias.hasMoreElements()){
final InetAddress ia = ias.nextElement();
final String address = ia.getHostAddress().trim().toLowerCase();
if ( ia instanceof Inet4Address ) {
referrers.add("http://" + address + ":0");
referrers.add("https://" + address + ":0");
}
if ( ia instanceof Inet6Address ) {
referrers.add("http://[" + address + "]" + ":0");
referrers.add("https://[" + address + "]" + ":0");
}
}
}
} catch ( final SocketException se) {
logger.error("Unable to detect network interfaces", se);
}
referrers.add("http://localhost" + ":0");
referrers.add("http://127.0.0.1" + ":0");
referrers.add("http://[::1]" + ":0");
referrers.add("https://localhost" + ":0");
referrers.add("https://127.0.0.1" + ":0");
referrers.add("https://[::1]" + ":0");
return referrers;
}
private void add(final List<URL> urls, final String ref) {
try {
final URL u = new URL(ref);
urls.add(u);
} catch (final MalformedURLException mue) {
logger.warn("Unable to create URL from " + ref + " : " + mue.getMessage());
}
}
/**
* Create URLs out of the uri referrer set
*/
private URL[] createReferrerUrls(final Set<String> referrers) {
final List<URL> urls = new ArrayList<URL>();
for(final String ref : referrers) {
final int pos = ref.indexOf("://");
// valid url?
if ( pos != -1 ) {
this.add(urls, ref);
} else {
this.add(urls, "http://" + ref + ":0");
this.add(urls, "https://" + ref + ":0");
}
}
return urls.toArray(new URL[urls.size()]);
}
/**
* Create Patterns out of the regexp referrer list
*/
private Pattern[] createReferrerPatterns(final String[] regexps) {
final List<Pattern> patterns = new ArrayList<Pattern>();
for(final String regexp : regexps) {
try {
final Pattern pattern = Pattern.compile(regexp);
patterns.add(pattern);
} catch (final Exception e) {
logger.warn("Unable to create Pattern from {} : {}", new Object[]{regexp, e.getMessage()});
}
}
return patterns.toArray(new Pattern[patterns.size()]);
}
/**
* Activate
*/
@Activate
protected void activate(final BundleContext context, final Map<String, Object> props) {
this.allowEmpty = PropertiesUtil.toBoolean(props.get(PROP_ALLOW_EMPTY), DEFAULT_ALLOW_EMPTY);
final String[] allowRegexHosts = defaultIfEmpty(PropertiesUtil.toStringArray(props.get(PROP_HOSTS_REGEX),
DEFAULT_PROP_HOSTS), DEFAULT_PROP_HOSTS);
this.allowedRegexReferrers = createReferrerPatterns(allowRegexHosts);
final Set<String> allowUriReferrers = getDefaultAllowedReferrers();
final String[] allowHosts = defaultIfEmpty(PropertiesUtil.toStringArray(props.get(PROP_HOSTS),
DEFAULT_PROP_HOSTS), DEFAULT_PROP_HOSTS);
allowUriReferrers.addAll(Arrays.asList(allowHosts));
this.allowedUriReferrers = createReferrerUrls(allowUriReferrers);
this.filterMethods = PropertiesUtil.toStringArray(props.get(PROP_METHODS));
if ( this.filterMethods != null && this.filterMethods.length == 1 && (this.filterMethods[0] == null || this.filterMethods[0].trim().length() == 0) ) {
this.filterMethods = null;
}
if ( this.filterMethods != null ) {
for(int i=0; i<filterMethods.length; i++) {
filterMethods[i] = filterMethods[i].toUpperCase();
}
}
this.configPrinterRegistration = registerConfigPrinter(context);
}
@Deactivate
protected void deactivate() {
this.configPrinterRegistration.unregister();
}
private ServiceRegistration<Object> registerConfigPrinter(BundleContext bundleContext) {
final ConfigurationPrinter cfgPrinter = new ConfigurationPrinter();
final Dictionary<String, String> serviceProps = new Hashtable<String, String>();
serviceProps.put(Constants.SERVICE_DESCRIPTION,
"Apache Sling Referrer Filter Configuration Printer");
serviceProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
serviceProps.put("felix.webconsole.label", "slingreferrerfilter");
serviceProps.put("felix.webconsole.title", "Sling Referrer Filter");
serviceProps.put("felix.webconsole.configprinter.modes", "always");
return bundleContext.registerService(Object.class,
cfgPrinter, serviceProps);
}
private boolean isModification(final HttpServletRequest req) {
final String method = req.getMethod();
if ( filterMethods != null ) {
for(final String m : filterMethods) {
if ( m.equals(method) ) {
return true;
}
}
}
return false;
}
@Override
public void doFilter(final ServletRequest req,
final ServletResponse res,
final FilterChain chain)
throws IOException, ServletException {
if ( req instanceof HttpServletRequest && res instanceof HttpServletResponse ) {
final HttpServletRequest request = (HttpServletRequest)req;
// is this a modification request from a browser
if ( this.isBrowserRequest(request) && this.isModification(request) ) {
if ( !this.isValidRequest(request) ) {
final HttpServletResponse response = (HttpServletResponse)res;
// we use 403
response.sendError(403);
return;
}
}
}
chain.doFilter(req, res);
}
final static class HostInfo {
public String host;
public String scheme;
public int port;
public String toURI() {
return scheme + "://" + host + ":" + port;
}
}
HostInfo getHost(final String referrer) {
final int startPos = referrer.indexOf("://") + 3;
if ( startPos == 2 ) {
// we consider this illegal
return null;
}
final HostInfo info = new HostInfo();
info.scheme = referrer.substring(0, startPos - 3);
final int paramStart = referrer.indexOf('?');
final String hostAndPath = (paramStart == -1 ? referrer : referrer.substring(0, paramStart));
final int endPos = hostAndPath.indexOf('/', startPos);
final String hostPart = (endPos == -1 ? hostAndPath.substring(startPos) : hostAndPath.substring(startPos, endPos));
final int hostNameStart = hostPart.indexOf('@') + 1;
final int hostNameEnd = hostPart.lastIndexOf(':');
if (hostNameEnd < hostNameStart ) {
info.host = hostPart.substring(hostNameStart);
if ( info.scheme.equals("http") ) {
info.port = 80;
} else if ( info.scheme.equals("https") ) {
info.port = 443;
}
} else {
info.host = hostPart.substring(hostNameStart, hostNameEnd);
info.port = Integer.valueOf(hostPart.substring(hostNameEnd + 1));
}
return info;
}
boolean isValidRequest(final HttpServletRequest request) {
final String referrer = request.getHeader("referer");
// check for missing/empty referrer
if ( referrer == null || referrer.trim().length() == 0 ) {
if ( !this.allowEmpty ) {
this.logger.info("Rejected empty referrer header for {} request to {}", request.getMethod(), request.getRequestURI());
}
return this.allowEmpty;
}
// check for relative referrer - which is always allowed
if ( referrer.indexOf(":/") == - 1 ) {
return true;
}
// check for air referrer - which is always allowed
if ( referrer.startsWith("app:/") ) {
return true;
}
final HostInfo info = getHost(referrer);
if ( info == null ) {
// if this is invalid we just return invalid
this.logger.info("Rejected illegal referrer header for {} request to {} : {}",
new Object[] {request.getMethod(), request.getRequestURI(), referrer});
return false;
}
// allow the request if the host name of the referrer is
// the same as the request's host name
if ( info.host.equals(request.getServerName()) ) {
return true;
}
// allow the request if the referrer matches any of the allowed referrers
boolean valid = isValidUriReferrer(info) || isValidRegexReferrer(info);
if ( !valid) {
this.logger.info("Rejected referrer header for {} request to {} : {}",
new Object[] {request.getMethod(), request.getRequestURI(), referrer});
}
return valid;
}
/**
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public void init(final FilterConfig config) throws ServletException {
// nothing to do
}
/**
* @see javax.servlet.Filter#destroy()
*/
@Override
public void destroy() {
// nothing to do
}
/**
* @param hostInfo The hostInfo to check for validity
* @return <code>true</code> if the hostInfo matches any of the allowed URI referrer.
*/
private boolean isValidUriReferrer(HostInfo hostInfo) {
for(final URL ref : this.allowedUriReferrers) {
if ( hostInfo.host.equals(ref.getHost()) && hostInfo.scheme.equals(ref.getProtocol()) ) {
if ( ref.getPort() == 0 || hostInfo.port == ref.getPort() ) {
return true;
}
}
}
return false;
}
/**
* @param hostInfo The hostInfo to check for validity
* @return <code>true</code> if the hostInfo matches any of the allowed regexp referrer.
*/
private boolean isValidRegexReferrer(HostInfo hostInfo) {
for(final Pattern ref : this.allowedRegexReferrers) {
String url = hostInfo.toURI();
if (ref.matcher(url).matches()) {
return true;
}
}
return false;
}
/**
* @return The <code>defaultProperties</code> if <code>properties</code> contains a single empty string,
* <code>properties</code> otherwise.
*/
private String[] defaultIfEmpty(String[] properties, String[] defaultProperties) {
return properties.length == 1 && properties[0].trim().length() == 0
? defaultProperties
: properties;
}
/**
* Returns <code>true</code> if the given request can be assumed to be sent
* by a client browser such as Firefix, Internet Explorer, etc.
* <p>
* This method inspects the <code>User-Agent</code> header and returns
* <code>true</code> if the header contains the string <i>Mozilla</i> (known
* to be contained in Firefox, Internet Explorer, WebKit-based browsers
* User-Agent) or <i>Opera</i> (known to be contained in the Opera
* User-Agent).
*
* @param request The request to inspect
* @return <code>true</code> if the request is assumed to be sent by a
* browser.
*/
private boolean isBrowserRequest(final HttpServletRequest request) {
final String userAgent = request.getHeader(USER_AGENT);
if (userAgent != null && (userAgent.contains(BROWSER_CLASS_MOZILLA) || userAgent.contains(BROWSER_CLASS_OPERA))) {
return true;
}
return false;
}
public class ConfigurationPrinter {
/**
* Print out the allowedReferrers
* @see org.apache.felix.webconsole.ConfigurationPrinter#printConfiguration(java.io.PrintWriter)
*/
public void printConfiguration(final PrintWriter pw) {
pw.println("Current Apache Sling Referrer Filter Allowed Referrers:");
pw.println();
for (final URL url : allowedUriReferrers) {
pw.println(url.toString());
}
for (final Pattern pattern : allowedRegexReferrers) {
pw.println(pattern.toString());
}
}
}
}