org.sakaiproject.nakamura.http.usercontent.ServerProtectionServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.nakamura.http.usercontent.ServerProtectionServiceImpl.java

Source

/**
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF 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.sakaiproject.nakamura.http.usercontent;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
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.Modified;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.ReferenceStrategy;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.sakaiproject.nakamura.api.http.usercontent.ServerProtectionService;
import org.sakaiproject.nakamura.api.http.usercontent.ServerProtectionValidator;
import org.sakaiproject.nakamura.api.http.usercontent.ServerProtectionVeto;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * <p>
 * This implementation of the ServerProtectionService allows GETs and POSTs to the host,
 * provided they don't request bodies from areas not consider part of the application, and
 * provided the posts come with a referer header that is trusted.
 * </p>
 * <p>
 * GETs to application content (file bodies and encapsulated feeds) are allowed.
 * </p>
 * <p>
 * POSTS (or non GET/HEAD) operations from locations that are not trusted get a 400, bad
 * request.
 * </p>
 * <p>
 * GETs to non application content get redirected, with a HMAC SHA-512 digest to a host
 * that serves that content.
 * </p>
 * <p>
 * If the GET is an anon GET, then it gets redirected with no HMAC.
 * </p>
 * <p>
 * The default setup is http://localhost:8080 is the trusted server. http://localhost:8082
 * is the content server, referers of /* and http://localhost:8080* are trusted sources
 * of POST operations. Application data is any feed that does not stream a body, and
 * anything under /dev, /devwidgets, /index.html, all other GET operations are assumed to
 * be raw user content.
 * </p>
 * <p>
 * There is a distinct difference in what is required when calling each of the
 * <pre>is*Safe</pre> methods. Please check the javadoc on each method to see what
 * resource expectations there are.
 * </p>
 */
@Component(immediate = true, metatype = true)
@Service(value = ServerProtectionService.class)
public class ServerProtectionServiceImpl implements ServerProtectionService {
    private static final String HMAC_SHA512 = "HmacSHA512";
    private static final String HMAC_PARAM = ":hmac";
    private static final String[] DEFAULT_TRUSTED_HOSTS = { "localhost:8080 = http://localhost:8082" };
    private static final String[] DEFAULT_TRUSTED_PATHS = { "/dev", "/devwidgets", "/system", "/logout", "/var" };
    private static final String[] DEFAULT_TRUSTED_EXACT_PATHS = {};
    private static final String DEFAULT_TRUSTED_SECRET_VALUE = "This Must Be set in production";
    private static final String[] DEFAULT_WHITELIST_POST_PATHS = { "/system/console" };
    private static final String[] DEFAULT_ANON_WHITELIST_POST_PATHS = { "/system/userManager/user.create" };

    @Property(boolValue = false)
    private static final String DISABLE_XSS_PROTECTION_FOR_UI_DEV = "disable.protection.for.dev.mode";
    @Property(value = { "/dev", "/devwidgets", "/system", "/logout", "/var" })
    private static final String TRUSTED_PATHS_CONF = "trusted.paths";
    @Property(value = {})
    private static final String TRUSTED_EXACT_PATHS_CONF = "trusted.exact.paths";
    @Property(value = { "localhost:8080 = http://localhost:8082" }, cardinality = 9999999)
    protected static final String TRUSTED_HOSTS_CONF = "trusted.hosts";
    @Property(value = { DEFAULT_TRUSTED_SECRET_VALUE })
    private static final String TRUSTED_SECRET_CONF = "trusted.secret";
    @Property(value = { "/system/console" }, cardinality = 9999999)
    private static final String WHITELIST_POST_PATHS_CONF = "trusted.postwhitelist";
    @Property(value = { "/system/userManager/user.create", "/system/batch" })
    private static final String ANON_WHITELIST_POST_PATHS_CONF = "trusted.anonpostwhitelist";
    private static final Logger LOGGER = LoggerFactory.getLogger(ServerProtectionServiceImpl.class);

    /**
     * Map of Host headers to content hosts.
     */
    private Map<String, String> applicationContentRedirects;
    /**
     * Map of Host headers to acceptable refereres. 
     */
    private Map<String, String> applicationReferrerHeaders;
    /**
     * List of path stems its safe to stream content bodies from using a trusted host
     */
    private String[] safeToStreamPaths;
    /**
     * List of path stems its safe to stream content bodies from using a trusted host
     */
    private Set<String> safeToStreamExactPaths;
    /**
     * Array of keys created from the secret, indexed by the second digit of the timestamp
     */
    private Key[] transferKeys;
    /**
     * List of url stems that are always Ok to accept posts from on any URL (eg
     * /system/console). You will want to add additional protection on these.
     */
    private String[] postWhiteList;
    /**
     * list of paths where its safe for anon to post to.
     */
    private String[] safeForAnonToPostPaths;

    @Reference(cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC, strategy = ReferenceStrategy.EVENT, bind = "bindServerProtectionValidator", unbind = "unbindServerProtectionValidator")
    private ServerProtectionValidator[] serverProtectionValidators = new ServerProtectionValidator[0];
    private Map<ServiceReference, ServerProtectionValidator> serverProtectionValidatorsStore = Maps
            .newConcurrentMap();
    @Reference(cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC, strategy = ReferenceStrategy.EVENT, bind = "bindServerProtectionVeto", unbind = "unbindServerProtectionVeto")
    private ServerProtectionVeto[] serverProtectionVetos = new ServerProtectionVeto[0];
    private Map<ServiceReference, ServerProtectionVeto> serverProtectionVetosStore = Maps.newConcurrentMap();

    private BundleContext bundleContext;
    private boolean disableProtectionForDevMode;

    @Modified
    public void modified(ComponentContext componentContext)
            throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidSyntaxException {
        @SuppressWarnings("unchecked")

        Dictionary<String, Object> properties = componentContext.getProperties();
        disableProtectionForDevMode = PropertiesUtil.toBoolean(properties.get(DISABLE_XSS_PROTECTION_FOR_UI_DEV),
                false);
        if (disableProtectionForDevMode) {
            LOGGER.warn("XSS Protection is disabled [modified]");
            return;
        }
        String[] trustedHosts = PropertiesUtil.toStringArray(properties.get(TRUSTED_HOSTS_CONF),
                DEFAULT_TRUSTED_HOSTS);
        Builder<String, String> redirects = ImmutableMap.builder();
        Builder<String, String> referrers = ImmutableMap.builder();
        for (String h : trustedHosts) {
            String[] applicationContentHostPair = StringUtils.split(h, "=", 2);
            if (applicationContentHostPair == null || applicationContentHostPair.length != 2) {
                throw new IllegalArgumentException("Application Content Host Pair invalid " + h);
            }
            applicationContentHostPair[0] = applicationContentHostPair[0].trim();
            applicationContentHostPair[1] = applicationContentHostPair[1].trim();
            String[] proto = StringUtils.split(applicationContentHostPair[1], ":", 2);
            if (applicationContentHostPair[0].startsWith("http")) {
                throw new IllegalArgumentException(
                        "Trusted host must not start with protocol: " + applicationContentHostPair[0]);
            }
            if (proto == null || proto.length != 2) {
                throw new IllegalArgumentException("Content Host Must contain protocol " + h);
            }
            String[] hostHeader = StringUtils.split(applicationContentHostPair[0], ":", 2);
            if (hostHeader == null || hostHeader.length == 0) {
                throw new IllegalArgumentException("Application Host Must contain port " + h);
            }
            // we don't allow redirect onto a different protocol, which implies
            // the app is on the same protocol as
            // the content. Also this ensures that the trusted host does not
            // discriminate on protocol.
            LOGGER.info("Adding {} {} ", applicationContentHostPair[0], applicationContentHostPair[1]);
            redirects.put(applicationContentHostPair[0], applicationContentHostPair[1]);
            if ("http".equals(proto[0])) {
                if (hostHeader.length > 1 && "80".equals(hostHeader[1])) {
                    referrers.put(applicationContentHostPair[0], "http://" + hostHeader[0]);
                } else {
                    referrers.put(applicationContentHostPair[0], "http://" + applicationContentHostPair[0]);
                }
            } else if ("https".equals(proto[0])) {
                // requests on default ports will not have the port in the referrer.
                if (hostHeader.length > 1 && "443".equals(hostHeader[1])) {
                    referrers.put(applicationContentHostPair[0], "https://" + hostHeader[0]);
                } else {
                    referrers.put(applicationContentHostPair[0], "https://" + applicationContentHostPair[0]);
                }
            } else {
                LOGGER.warn("Protocol was not recognised {} {} ", proto[0], h);
                throw new IllegalArgumentException("Protocol not recognised " + h);
            }
        }
        applicationContentRedirects = redirects.build();
        applicationReferrerHeaders = referrers.build();
        safeToStreamPaths = PropertiesUtil.toStringArray(properties.get(TRUSTED_PATHS_CONF), DEFAULT_TRUSTED_PATHS);
        safeToStreamExactPaths = ImmutableSet.copyOf(PropertiesUtil
                .toStringArray(properties.get(TRUSTED_EXACT_PATHS_CONF), DEFAULT_TRUSTED_EXACT_PATHS));
        postWhiteList = PropertiesUtil.toStringArray(properties.get(WHITELIST_POST_PATHS_CONF),
                DEFAULT_WHITELIST_POST_PATHS);
        safeForAnonToPostPaths = PropertiesUtil.toStringArray(properties.get(ANON_WHITELIST_POST_PATHS_CONF),
                DEFAULT_ANON_WHITELIST_POST_PATHS);
        String transferSharedSecret = PropertiesUtil.toString(properties.get(TRUSTED_SECRET_CONF),
                DEFAULT_TRUSTED_SECRET_VALUE);
        if (DEFAULT_TRUSTED_SECRET_VALUE.equals(transferSharedSecret)) {
            LOGGER.error("Configuration Error =============================");
            LOGGER.error("Configuration Error: Please set {} to secure Content Server in procuction ",
                    TRUSTED_SECRET_CONF);
            LOGGER.error("Configuration Error =============================");
        }

        LOGGER.info("Trusted Hosts {}", applicationContentRedirects);
        LOGGER.info("Trusted Stream Paths {} ", Arrays.toString(safeToStreamPaths));
        LOGGER.info("Trusted Stream Resources {} ", safeToStreamExactPaths);
        LOGGER.info("POST Whitelist {} ", postWhiteList);
        LOGGER.info("Content Shared Secret [{}] ", transferSharedSecret);

        transferKeys = new Key[10];
        MessageDigest md = MessageDigest.getInstance("SHA-512");
        Base64 encoder = new Base64(true);
        byte[] input = transferSharedSecret.getBytes("UTF-8");
        // create a static ring of 10 keys by repeatedly hashing the last key seed
        // starting with the transferSharedSecret
        for (int i = 0; i < transferKeys.length; i++) {
            md.reset();
            byte[] data = md.digest(input);
            transferKeys[i] = new SecretKeySpec(data, HMAC_SHA512);
            input = encoder.encode(data);
        }

        bundleContext = componentContext.getBundleContext();
        ServiceReference[] srs = bundleContext.getAllServiceReferences(ServerProtectionValidator.class.getName(),
                null);
        if (srs != null) {
            for (ServiceReference sr : srs) {
                bindServerProtectionValidator(sr);
            }
        }
        ServiceReference[] srsVeto = bundleContext.getAllServiceReferences(ServerProtectionVeto.class.getName(),
                null);
        if (srsVeto != null) {
            for (ServiceReference sr : srsVeto) {
                bindServerProtectionVeto(sr);
            }
        }
    }

    @Activate
    public void activate(ComponentContext componentContext)
            throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidSyntaxException {
        modified(componentContext);
    }

    @Deactivate
    public void destroy(ComponentContext c) {
        if (disableProtectionForDevMode) {
            LOGGER.warn("XSS Protection is disabled [destroy]");
            return;
        }
        BundleContext bc = c.getBundleContext();
        for (Entry<ServiceReference, ServerProtectionValidator> e : serverProtectionValidatorsStore.entrySet()) {
            bc.ungetService(e.getKey());
        }
        serverProtectionValidatorsStore.clear();
        serverProtectionValidators = null;
    }

    /**
     * {@inheritDoc}
     * 
     * This method requires a resource to operate successfully and is best used by a Sling
     * Filter rather than a Servlet Filter, so that Sling will have made the appropriate
     * resource resolution.
     *
     * @see org.sakaiproject.nakamura.api.http.usercontent.ServerProtectionService#isRequestSafe(org.apache.sling.api.SlingHttpServletRequest,
     *      org.apache.sling.api.SlingHttpServletResponse)
     */
    public boolean isRequestSafe(SlingHttpServletRequest srequest, SlingHttpServletResponse sresponse)
            throws UnsupportedEncodingException, IOException {
        if (disableProtectionForDevMode) {
            LOGGER.warn("XSS Protection is disabled [isRequestSafe]");
            return true;
        }
        // if the method is not safe, the request can't be safe.
        if (!isMethodSafe(srequest, sresponse)) {
            return false;
        }
        String method = srequest.getMethod();
        if ("GET|OPTIONS|HEAD".indexOf(method) < 0) {
            String userId = srequest.getRemoteUser();
            if (User.ANON_USER.equals(userId)) {
                String path = srequest.getRequestURI();
                boolean safeForAnonToPost = false;
                for (String safePath : safeForAnonToPostPaths) {
                    if (path.startsWith(safePath)) {
                        safeForAnonToPost = true;
                        break;
                    }
                }
                if (!safeForAnonToPost) {
                    sresponse.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
                            "Anon users may not perform POST operations");
                    return false;
                }
            }
        }
        boolean safeHost = isSafeHost(srequest);
        if (safeHost && "GET".equals(method)) {
            boolean safeToStream = false;
            RequestPathInfo requestPathInfo = srequest.getRequestPathInfo();
            String ext = requestPathInfo.getExtension();
            if (ext == null || "res".equals(ext)) {
                // this is going to stream
                String path = srequest.getRequestURI();
                LOGGER.debug("Checking [{}] RequestPathInfo {}", path, requestPathInfo);
                safeToStream = isMatchToSafePath(path);
                if (!safeToStream) {
                    Resource resource = srequest.getResource();
                    if (resource != null) {
                        if ("sling:nonexisting".equals(resource.getResourceType())) {
                            // Trust a "GET" of non-existing content so that the 404 comes from
                            // the right port (KERN-2001)
                            LOGGER.debug("Non existing resource {}", resource.getPath());
                            return true;
                        }
                        // The original unrecognized URI might be a mapping to a trusted resource.
                        // Check again now that any aliases have been resolved.
                        String resourcePath = resource.getPath();
                        LOGGER.debug("Checking Resource Path [{}]", resourcePath);
                        safeToStream = isMatchToSafePath(resourcePath);
                        if (!safeToStream) {
                            for (ServerProtectionValidator serverProtectionValidator : serverProtectionValidators) {
                                if (serverProtectionValidator.safeToStream(srequest, resource)) {
                                    LOGGER.debug(" {} said this {} is safe to stream ", serverProtectionValidator,
                                            resourcePath);
                                    safeToStream = true;
                                    break;
                                }
                            }
                        }
                    }
                } else {
                    LOGGER.debug("Content was safe to stream ");
                }
            } else {
                safeToStream = true;
                LOGGER.debug("doesnt look like a body, checking with vetos");
            }
            LOGGER.debug("Checking server vetos, safe to stream ? {} ", safeToStream);
            for (ServerProtectionVeto serverProtectionVeto : serverProtectionVetos) {
                LOGGER.debug("Checking for Veto on {} ", serverProtectionVeto);
                if (serverProtectionVeto.willVeto(srequest)) {
                    safeToStream = serverProtectionVeto.safeToStream(srequest);
                    LOGGER.debug("{} vetoed {}  ", serverProtectionVeto, safeToStream);
                    break;
                }
            }
            if (!safeToStream) {
                redirectToContent(srequest, sresponse);
                return false;
            }
            LOGGER.debug("Request will be sent from this host, no redirect {}",
                    srequest.getRequestURL().toString());
        }
        return true;
    }

    private boolean isMatchToSafePath(String path) {
        boolean safeToStream = safeToStreamExactPaths.contains(path);
        if (!safeToStream) {
            LOGGER.debug("Checking [{}] looks like not safe to stream ", path);
            for (String safePath : safeToStreamPaths) {
                if (path.startsWith(safePath)) {
                    safeToStream = true;
                    LOGGER.debug("Safe To stream becuase starts with {} ", safePath);
                    break;
                }
            }
        }
        return safeToStream;
    }

    private void redirectToContent(HttpServletRequest request, HttpServletResponse response)
            throws UnsupportedEncodingException, IOException {
        StringBuffer requestURL = request.getRequestURL();
        String queryString = request.getQueryString();
        if (queryString != null) {
            requestURL.append("?").append(queryString);
        }
        String url = requestURL.toString();
        // replace the protocol and host with the CDN host.
        int pathStart = requestURL.indexOf("/", requestURL.indexOf(":") + 3);
        url = getTransferUrl(request, url.substring(pathStart));
        // send via the session establisher
        LOGGER.debug("Sending redirect for {} {} ", request.getMethod(), url);
        response.sendRedirect(url);
    }

    /**
     * @param request
     * @param urlPath
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws IllegalStateException
     * @throws UnsupportedEncodingException
     */
    private String getTransferUrl(HttpServletRequest request, String urlPath) {
        String trustedHostHeader = buildTrustedHostHeader(request);
        if (trustedHostHeader == null || !applicationContentRedirects.containsKey(trustedHostHeader)) {
            LOGGER.warn("No Content Host found for {} ", request.getRequestURL());
            throw new IllegalArgumentException("No Content Host foudn for request, cant transfer ");
        }
        String redirectUrl = applicationContentRedirects.get(trustedHostHeader) + urlPath;
        // only transfer authN from a trusted safe host
        if (isSafeHost(request)) {
            String userId = request.getRemoteUser();
            if (userId != null && !User.ANON_USER.equals(userId)) {
                try {
                    long ts = System.currentTimeMillis();
                    int keyIndex = (int) (ts - ((ts / 10) * 10));
                    Mac m = Mac.getInstance(HMAC_SHA512);
                    m.init(transferKeys[keyIndex]);

                    String message = createMessage(redirectUrl, userId, String.valueOf(ts));

                    m.update(message.getBytes("UTF-8"));
                    String hmac = Base64.encodeBase64URLSafeString(m.doFinal());
                    hmac = Base64.encodeBase64URLSafeString((hmac + ";" + userId + ";" + ts).getBytes("UTF-8"));
                    String spacer = "?";
                    if (redirectUrl.indexOf('?') > 0) {
                        spacer = "&";
                    }
                    redirectUrl = redirectUrl + spacer + HMAC_PARAM + "=" + hmac;
                    LOGGER.debug("Message was [{}] ", message);
                    LOGGER.debug("Key was [{}] [{}] ", keyIndex, transferKeys[keyIndex]);
                    LOGGER.debug("Transfer URL created as [{}] ", redirectUrl);
                } catch (Exception e) {
                    LOGGER.warn(e.getMessage(), e);
                }
            }
        }
        return redirectUrl;
    }

    private String buildTrustedHostHeader(HttpServletRequest request) {
        // try the host header first
        String host = request.getHeader("Host");
        if (host != null && host.trim().length() > 0) {
            return host;
        }
        // if not suitable resort to letting jetty build the host header
        int port = request.getServerPort();
        String scheme = request.getScheme();
        String serverName = request.getServerName();
        // default ports are not added to the header.
        if ((port == 80 && "http".equals(scheme)) || (port == 443 && "https".equals(scheme))) {
            return serverName;
        } else {
            return serverName + ":" + port;
        }
    }

    private String createMessage(String url, String userId, String ts) {
        // strip the protocol since it wont survive front end proxies, if those proxies re-write the protocol.
        if (url.startsWith("http:")) {
            url = url.substring(5);
        } else if (url.startsWith("https:")) {
            url = url.substring(6);
        }
        return url + ";" + userId + ";" + ts;
    }

    public String getTransferUserId(HttpServletRequest request) {
        // only ever get a user ID in this way on a non trusted safe host.
        if (disableProtectionForDevMode) {
            LOGGER.warn("XSS Protection is disabled [getTransferUserId]");
            return null;
        }
        // the host must not be safe to decode the user transfer UserID, and the method must be a GET or HEAD
        String method = request.getMethod();
        if (!isSafeHost(request) && ("GET".equals(method) || "HEAD".equals(method))) {
            String hmac = request.getParameter(HMAC_PARAM);
            if (hmac != null) {
                try {
                    hmac = new String(Base64.decodeBase64(hmac.getBytes("UTF-8")), "UTF-8");
                    String[] parts = StringUtils.split(hmac, ';');
                    String requestUrl = request.getRequestURL().append("?").append(request.getQueryString())
                            .toString();
                    LOGGER.debug("Checking requestUrl [{}", requestUrl);
                    int i = requestUrl.indexOf("&" + HMAC_PARAM);
                    if (i < 0) {
                        i = requestUrl.indexOf("?" + HMAC_PARAM);
                    }
                    String finalUrl = requestUrl.substring(0, i);
                    String requestHmac = parts[0];
                    String requestUserId = parts[1];
                    String requestTs = parts[2];
                    String message = createMessage(finalUrl, requestUserId, requestTs);
                    long requestTsL = Long.parseLong(requestTs);
                    if (Math.abs(System.currentTimeMillis() - requestTsL) < 60000L) {
                        int keyIndex = (int) (requestTsL - ((requestTsL / 10) * 10));
                        Mac m = Mac.getInstance(HMAC_SHA512);
                        m.init(transferKeys[keyIndex]);
                        m.update(message.getBytes("UTF-8"));
                        String testHmac = Base64.encodeBase64URLSafeString(m.doFinal());
                        if (testHmac.equals(requestHmac)) {
                            LOGGER.debug("Successfully extracted requestUserId {} from HMAC", requestUserId);
                            return requestUserId;
                        } else {
                            LOGGER.debug("Message was [{}] ", message);
                            LOGGER.debug("Key was [{}] [{}] ", keyIndex, transferKeys[keyIndex]);
                            LOGGER.debug("Hmac did not validate testHmac was [{}], requestHmac [{}] ", testHmac,
                                    requestHmac);
                        }
                    } else {
                        LOGGER.debug("Hmac has expired, older than 60s, hmac message was {} ", message);
                    }
                } catch (Exception e) {
                    LOGGER.warn(e.getMessage());
                    LOGGER.debug(e.getMessage(), e);
                }
            }
        } else {
            LOGGER.debug("Request is to a safe host, wont look for a transfer of trust to this host. {} ",
                    request.getRequestURL().toString());
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * This method has no resource requirements to operate successfully allowing for it to
     * be called by a filter without any cost to resource resolution.
     * 
     * @see org.sakaiproject.nakamura.api.http.usercontent.ServerProtectionService#isMethodSafe(javax.servlet.http.HttpServletRequest,
     *      javax.servlet.http.HttpServletResponse)
     */
    public boolean isMethodSafe(HttpServletRequest hrequest, HttpServletResponse hresponse) throws IOException {
        if (disableProtectionForDevMode) {
            LOGGER.warn("XSS Protection is disabled [isMethodSafe]");
            return true;
        }
        String method = hrequest.getMethod();
        boolean safeHost = isSafeHost(hrequest);

        // protect against POST originating from other domains, this assumes that there is no
        // browser bug in this area
        // and no flash bug.
        if (!("GET".equals(method) || "HEAD".equals(method))) {
            String path = hrequest.getRequestURI();
            for (String okPostStem : postWhiteList) {
                if (path.startsWith(okPostStem)) {
                    return true;
                }
            }
            // check the Referer
            @SuppressWarnings("unchecked")
            Enumeration<String> referers = hrequest.getHeaders("Referer");
            String referer = null;
            if (referers == null || !referers.hasMoreElements()) {
                LOGGER.debug("No Referer header present ");
                hresponse.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "POST Requests with no Referer are not acceptable");
                return false;
            }
            referer = referers.nextElement();
            if (referer == null) {
                LOGGER.debug("No Referer header present, was null ");
                hresponse.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "POST Requests with no Referer are not acceptable");
                return false;
            }

            // Do we allow non get operations to this host ?
            if (safeHost) {
                // and if we do, do we accept them from the Referer mentioned ?
                String safeReferer = applicationReferrerHeaders.get(buildTrustedHostHeader(hrequest));
                if (referer.startsWith("/")) {
                    LOGGER.warn("Referer header from test script, allowed safe:[{}] request:[{}] ", safeReferer,
                            referer);
                    safeHost = true;
                } else if (referer.startsWith(safeReferer)) {
                    safeHost = true;
                    LOGGER.debug("Accepted referred safe:[{}] request:[{}]", safeReferer, referer);
                } else {
                    safeHost = false;
                    LOGGER.debug("Rejecting referred safe:[{}] request:[{}]", safeReferer, referer);
                }
            }
            if (!safeHost) {
                hresponse.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "POST Requests are only accepted from the Application, this request was not from the application.");
                return false;
            }
        }
        return true;
    }

    /**
     * {@inheritDoc}
     * 
     * This method has no resource requirements to operate successfully allowing for it to
     * be called by a filter without any cost to resource resolution.
     * 
     * @see org.sakaiproject.nakamura.api.http.usercontent.ServerProtectionService#isSafeHost(javax.servlet.http.HttpServletRequest)
     */
    public boolean isSafeHost(HttpServletRequest hrequest) {
        if (disableProtectionForDevMode) {
            LOGGER.warn("XSS Protection is disabled [isSafeHost]");
            return true;
        }
        return applicationReferrerHeaders.containsKey(buildTrustedHostHeader(hrequest));
    }

    public void bindServerProtectionValidator(ServiceReference serviceReference) {
        if (bundleContext != null) {
            serverProtectionValidatorsStore.put(serviceReference,
                    (ServerProtectionValidator) bundleContext.getService(serviceReference));
            serverProtectionValidators = serverProtectionValidatorsStore.values()
                    .toArray(new ServerProtectionValidator[serverProtectionValidatorsStore.size()]);
        }
    }

    public void unbindServerProtectionValidator(ServiceReference serviceReference) {
        if (bundleContext != null) {
            serverProtectionValidatorsStore.remove(serviceReference);
            bundleContext.ungetService(serviceReference);
            serverProtectionValidators = serverProtectionValidatorsStore.values()
                    .toArray(new ServerProtectionValidator[serverProtectionValidatorsStore.size()]);
        }
    }

    public void bindServerProtectionVeto(ServiceReference serviceReference) {
        if (bundleContext != null) {
            serverProtectionVetosStore.put(serviceReference,
                    (ServerProtectionVeto) bundleContext.getService(serviceReference));
            serverProtectionVetos = serverProtectionVetosStore.values()
                    .toArray(new ServerProtectionVeto[serverProtectionVetosStore.size()]);
        }
    }

    public void unbindServerProtectionVeto(ServiceReference serviceReference) {
        if (bundleContext != null) {
            serverProtectionVetosStore.remove(serviceReference);
            bundleContext.ungetService(serviceReference);
            serverProtectionVetos = serverProtectionVetosStore.values()
                    .toArray(new ServerProtectionVeto[serverProtectionVetosStore.size()]);
        }
    }

}