org.sakaiproject.util.RequestFilter.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.util.RequestFilter.java

Source

/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2005, 2006, 2007, 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 org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.cluster.api.ClusterNode;
import org.sakaiproject.cluster.api.ClusterService;
import org.sakaiproject.cluster.api.ClusterService.Status;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.event.api.UsageSession;
import org.sakaiproject.event.api.UsageSessionService;
import org.sakaiproject.thread_local.cover.ThreadLocalManager;
import org.sakaiproject.tool.api.ClosingException;
import org.sakaiproject.tool.api.RebuildBreakdownService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.Tool;
import org.sakaiproject.tool.api.ToolSession;
import org.sakaiproject.tool.cover.SessionManager;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;

/**
 * RequestFilter Filters all requests to Sakai tools. It is responsible for keeping the Sakai session, done using a cookie to the
 * end user's browser storing the user's session id.
 */
@SuppressWarnings("deprecation")
public class RequestFilter implements Filter {
    /** The request attribute name used to store the Sakai session. */
    public static final String ATTR_SESSION = "sakai.session";
    /** The request attribute name used to ask the RequestFilter to output
     * a client cookie at the end of the request cycle. */
    public static final String ATTR_SET_COOKIE = "sakai.set.cookie";
    /** The request attribute name (and value) used to indicated that the request has been filtered. */
    public static final String ATTR_FILTERED = "sakai.filtered";
    /** The request attribute name (and value) used to indicated that file uploads have been parsed. */
    public static final String ATTR_UPLOADS_DONE = "sakai.uploads.done";
    /** The request attribute name (and value) used to indicated that character encoding has been set. */
    public static final String ATTR_CHARACTER_ENCODING_DONE = "sakai.character.encoding.done";
    /** The request attribute name used to indicated that the *response* has been redirected. */
    public static final String ATTR_REDIRECT = "sakai.redirect";
    /** The request parameter name used to indicated that the request is automatic, not from a user action. */
    public static final String PARAM_AUTO = "auto";
    /** Config parameter to control http session handling. */
    public static final String CONFIG_SESSION = "http.session";
    /** Config parameter to control whether to check the request principal before any cookie to establish a session */
    public static final String CONFIG_SESSION_AUTH = "sakai.session.auth";
    /** Config parameter to control remote user handling. */
    public static final String CONFIG_REMOTE_USER = "remote.user";
    /** Config parameter to control tool placement URL en/de-coding. */
    public static final String CONFIG_TOOL_PLACEMENT = "tool.placement";
    /** Config parameter to control whether to set the character encoding on the request. Default is true. */
    public static final String CONFIG_CHARACTER_ENCODING_ENABLED = "encoding.enabled";
    /** Config parameter which to control character encoding to apply to the request. Default is UTF-8. */
    public static final String CONFIG_CHARACTER_ENCODING = "encoding";
    /**
     * Config parameter to control whether the request filter parses file uploads. Default is true. If false, the tool will need to
     * provide its own upload filter that executes BEFORE the Sakai request filter.
     */
    public static final String CONFIG_UPLOAD_ENABLED = "upload.enabled";
    /**
     * Config parameter to control the maximum allowed upload size (in MEGABYTES) from the browser.<br />
     * If defined on the filter, overrides the system property. Default is 1 (one megabyte).<br />
     * This is an aggregate limit on the sum of all files included in a single request.<br />
     * Also used as a per-request request parameter, encoded in the URL, to set the max for that particular request.
     */
    public static final String CONFIG_UPLOAD_MAX = "upload.max";
    /**
     * System property to control the maximum allowed upload size (in MEGABYTES) from the browser. Default is 1 (one megabyte). This
     * is an aggregate limit on the sum of all files included in a single request.
     */
    public static final String SYSTEM_UPLOAD_MAX = "sakai.content.upload.max";
    /**
     * System property to control the maximum allowed upload size (in MEGABYTES) from any other method - system wide, request
     * filter, or per-request.
     */
    public static final String SYSTEM_UPLOAD_CEILING = "sakai.content.upload.ceiling";
    /**
     * Config parameter (in bytes) to control the threshold at which to store uploaded files on-disk (temporarily) instead of
     * in-memory. Default is 1024 bytes.
     */
    public static final String CONFIG_UPLOAD_THRESHOLD = "upload.threshold";
    /**
     * Config parameter that specifies the absolute path of a temporary directory in which to store file uploads. Default is the
     * servlet container temporary directory. Note that this is TRANSIENT storage, used by the commons-fileupload API. The files
     * must be renamed or otherwise processed (by the tool through the commons-fileupload API) in order for the data to become
     * permenant.
     */
    public static final String CONFIG_UPLOAD_DIR = "upload.dir";
    /** System property to control the temporary directory in which to store file uploads. */
    public static final String SYSTEM_UPLOAD_DIR = "sakai.content.upload.dir";
    /** Config parameter to set the servlet context for context based session (overriding the servlet's context name). */
    public static final String CONFIG_CONTEXT = "context";
    /** Key in the ThreadLocalManager for access to the current http request object. */
    public final static String CURRENT_HTTP_REQUEST = "org.sakaiproject.util.RequestFilter.http_request";
    /** Key in the ThreadLocalManager for access to the current http response object. */
    public final static String CURRENT_HTTP_RESPONSE = "org.sakaiproject.util.RequestFilter.http_response";
    /** Key in the ThreadLocalManager for access to the current servlet context. */
    public final static String CURRENT_SERVLET_CONTEXT = "org.sakaiproject.util.RequestFilter.servlet_context";
    /**
     * Config parameter to continue (or abort, if false) upload field processing if there's a file upload max size exceeded
     * exception.
     */
    protected static final String CONFIG_CONTINUE = "upload.continueOverMax";
    /**
     * Config parameter to treat the max upload size as for the individual files in the request (or, if false, for the entire
     * request).
     */
    protected static final String CONFIG_MAX_PER_FILE = "upload.maxPerFile";
    /** sakaiHttpSession setting for don't do anything. */
    protected final static int CONTAINER_SESSION = 0;
    /** sakaiHttpSession setting for use the sakai wide session. */
    protected final static int SAKAI_SESSION = 1;
    /** sakaiHttpSession setting for use the context session. */
    protected final static int CONTEXT_SESSION = 2;
    /** sakaiHttpSession setting for use the tool session, in any, else context. */
    protected final static int TOOL_SESSION = 3;
    /** If true, we deliver the Sakai wide session as the Http session for each request. */
    protected int m_sakaiHttpSession = TOOL_SESSION;
    /** Key in the ThreadLocalManager for binding our remoteUser preference. */
    protected final static String CURRENT_REMOTE_USER = "org.sakaiproject.util.RequestFilter.remote_user";
    /** Key in the ThreadLocalManager for binding our http session preference. */
    protected final static String CURRENT_HTTP_SESSION = "org.sakaiproject.util.RequestFilter.http_session";
    /** Key in the ThreadLocalManager for binding our context id. The servlet context is stored against this. */
    protected final static String CURRENT_CONTEXT = "org.sakaiproject.util.RequestFilter.context";
    /** The "." character */
    protected static final String DOT = ".";

    /** The name of the system property that will be used when setting the value of the session cookie. */
    protected static final String SAKAI_SERVERID = "sakai.serverId";

    /** The name of the system property that will be used when setting the name of the session cookie. */
    protected static final String SAKAI_COOKIE_NAME = "sakai.cookieName";

    /** The name of the system property that will be used when setting the domain of the session cookie. */
    protected static final String SAKAI_COOKIE_DOMAIN = "sakai.cookieDomain";

    /** The name of the Sakai property to disable setting the HttpOnly attribute on the cookie (if false). */
    protected static final String SAKAI_COOKIE_HTTP_ONLY = "sakai.cookieHttpOnly";

    /** The name of the Sakai property to set the X-UA Compatible header
     */
    protected static final String SAKAI_UA_COMPATIBLE = "sakai.X-UA-Compatible";

    /** The name of the Sakai property to allow passing a session id in the ATTR_SESSION request parameter */
    protected static final String SAKAI_SESSION_PARAM_ALLOW = "session.parameter.allow";

    /** The tools allowed as lti provider **/
    protected static final String SAKAI_BLTI_PROVIDER_TOOLS = "basiclti.provider.allowedtools";

    /** The name of the Skaia property to say we should redirect to another node when in shutdown */
    protected static final String SAKAI_CLUSTER_REDIRECT_RANDOM = "cluster.redirect.random.node";

    /** Our log (commons). */
    private static Log M_log = LogFactory.getLog(RequestFilter.class);
    /** If true, we deliver the Sakai end user enterprise id as the remote user in each request. */
    protected boolean m_sakaiRemoteUser = true;

    /** If true, we encode / decode the tool placement using the a URL parameter. */
    protected boolean m_toolPlacement = true;

    /** Our contex (i.e. servlet context) id. */
    protected String m_contextId = null;

    protected String m_characterEncoding = "UTF-8";

    protected boolean m_characterEncodingEnabled = true;

    protected boolean m_uploadEnabled = true;

    protected boolean m_checkPrincipal = false;

    protected long m_uploadMaxSize = 1L * 1024L * 1024L;

    protected long m_uploadCeiling = 1L * 1024L * 1024L;

    protected int m_uploadThreshold = 1024;

    protected String m_uploadTempDir = null;

    protected boolean m_displayModJkWarning = true;

    protected boolean m_redirectRandomNode = true;

    /** Default is to abort further upload processing if the max is exceeded. */
    protected boolean m_uploadContinue = false;

    /** Default is to treat the m_uploadMaxSize as for the entire request, not per file. */
    protected boolean m_uploadMaxPerFile = false;

    /** The servlet context for the filter. */
    protected ServletContext m_servletContext = null;

    /** Is this a Terracotta clustered environment? */
    protected boolean TERRACOTTA_CLUSTER = false;

    /** Allow setting the cookie in a request parameter */
    protected boolean m_sessionParamAllow = false;

    /** The name of the cookie we use to keep sakai session. */
    protected String cookieName = "JSESSIONID";

    protected String cookieDomain = null;

    /** Set the HttpOnly attribute on the cookie */
    protected boolean m_cookieHttpOnly = true;

    protected String m_UACompatible = null;

    protected boolean isLTIProviderAllowed = false;
    // knl-640
    private String chsDomain;
    private String appUrl;
    private String chsUrl;
    private boolean useContentHostingDomain;
    private String[] contentPaths;
    private String[] loginPaths;
    private String[] contentExceptions;

    /**
     * Compute the URL that would return to this server based on the current request.
     *
     * Note: this method is used by the one in /sakai-kernel-util/src/main/java/org/sakaiproject/util/Web.java
     *
     * @param req
     *        The request.
     * @return The URL back to this server based on the current request.
     */
    public static String serverUrl(HttpServletRequest req) {
        String transport = null;
        int port = 0;
        boolean secure = false;

        // if force.url.secure is set (to a https port number), use https and this port
        String forceSecure = System.getProperty("sakai.force.url.secure");
        if (forceSecure != null && !"".equals(forceSecure)) {
            // allow the value to be forced to 0 or blank to disable this
            int portNum;
            try {
                portNum = Integer.parseInt(forceSecure);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException(
                        "force.url.secure must be set to the port number which should be a numeric value > 0 (or set it to 0 to disable secure urls)",
                        e);
            }
            if (portNum > 0) {
                transport = "https";
                port = portNum;
                secure = true;
            }
        } else {
            // otherwise use the request scheme and port
            transport = req.getScheme();
            port = req.getServerPort();
            secure = req.isSecure();
        }

        StringBuilder url = new StringBuilder();
        url.append(transport);
        url.append("://");
        url.append(req.getServerName());
        if (((port != 80) && (!secure)) || ((port != 443) && secure)) {
            url.append(":");
            url.append(port);
        }

        return url.toString();
    }

    /**
     * Take this filter out of service.
     */
    public void destroy() {
    }

    private boolean startsWithAny(String source, String[] toMatch) {
        for (String test : toMatch) {
            if (source.startsWith(test)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Filter a request / response.
     */
    public void doFilter(ServletRequest requestObj, ServletResponse responseObj, FilterChain chain)
            throws IOException, ServletException {
        StringBuffer sb = null;
        long startTime = System.currentTimeMillis();

        // bind some preferences as "current"
        Boolean curRemoteUser = (Boolean) ThreadLocalManager.get(CURRENT_REMOTE_USER);
        Integer curHttpSession = (Integer) ThreadLocalManager.get(CURRENT_HTTP_SESSION);
        String curContext = (String) ThreadLocalManager.get(CURRENT_CONTEXT);
        ServletRequest curRequest = (ServletRequest) ThreadLocalManager.get(CURRENT_HTTP_REQUEST);
        ServletResponse curResponse = (ServletResponse) ThreadLocalManager.get(CURRENT_HTTP_RESPONSE);
        boolean cleared = false;

        // keep track of temp files with this request that need to be deleted on the way out
        List<FileItem> tempFiles = new ArrayList<FileItem>();

        try {
            ThreadLocalManager.set(CURRENT_REMOTE_USER, Boolean.valueOf(m_sakaiRemoteUser));
            ThreadLocalManager.set(CURRENT_HTTP_SESSION, Integer.valueOf(m_sakaiHttpSession));
            ThreadLocalManager.set(CURRENT_CONTEXT, m_contextId);

            // make the servlet context available
            ThreadLocalManager.set(CURRENT_SERVLET_CONTEXT, m_servletContext);

            // we are expecting HTTP stuff
            if (!((requestObj instanceof HttpServletRequest) && (responseObj instanceof HttpServletResponse))) {
                // if not, just pass it through
                chain.doFilter(requestObj, responseObj);
                return;
            }

            HttpServletRequest req = (HttpServletRequest) requestObj;
            HttpServletResponse resp = (HttpServletResponse) responseObj;

            // knl-640
            // The AppDomain should reject:
            // 1) all GET URL's starting with contentPaths
            //
            // The FileDomain should only accept:
            // 1) any URL's in loginPath. We have to accept POST methods here
            //    as well so folks can log in on this node.
            // 2) any GET URL's from contentPaths (POST's any other methods not
            //    allowed.
            if (useContentHostingDomain) {
                String requestURI = req.getRequestURI();
                if (req.getQueryString() != null)
                    requestURI += "?" + req.getQueryString();
                if (startsWithAny(requestURI, contentPaths) && "GET".equalsIgnoreCase(req.getMethod())) {
                    if (!req.getServerName().equals(chsDomain) && !(startsWithAny(requestURI, contentExceptions))) {
                        resp.sendRedirect(chsUrl + requestURI);
                        return;
                    }
                } else {
                    if (req.getServerName().equals(chsDomain)
                            && !(startsWithAny(requestURI, contentPaths)
                                    && !"GET".equalsIgnoreCase(req.getMethod()))
                            && !(startsWithAny(requestURI, loginPaths))) {
                        resp.sendRedirect(appUrl + requestURI);
                        return;
                    }
                }
            }

            // check on file uploads and character encoding BEFORE checking if
            // this request has already been filtered, as the character encoding
            // and file upload handling are configurable at the tool level.
            // so the 2nd invokation of the RequestFilter (at the tool level)
            // may actually cause character encoding and file upload parsing
            // to happen.

            // handle character encoding
            handleCharacterEncoding(req, resp);

            // handle file uploads
            req = handleFileUpload(req, resp, tempFiles);

            // if we have already filtered this request, pass it on
            if (req.getAttribute(ATTR_FILTERED) != null) {
                // set the request and response for access via the thread local
                ThreadLocalManager.set(CURRENT_HTTP_REQUEST, req);
                ThreadLocalManager.set(CURRENT_HTTP_RESPONSE, resp);

                chain.doFilter(req, resp);
            }

            // filter the request
            else {
                if (M_log.isDebugEnabled()) {
                    sb = new StringBuffer("http-request: ");
                    sb.append(req.getMethod());
                    sb.append(" ");
                    sb.append(req.getRequestURL());
                    if (req.getQueryString() != null) {
                        sb.append("?");
                        sb.append(req.getQueryString());
                    }
                    M_log.debug(sb);
                }

                try {
                    // mark the request as filtered to avoid re-filtering it later in the request processing
                    req.setAttribute(ATTR_FILTERED, ATTR_FILTERED);

                    // some useful info
                    ThreadLocalManager.set(ServerConfigurationService.CURRENT_SERVER_URL, serverUrl(req));

                    // make sure we have a session
                    Session s = assureSession(req, resp);

                    // pre-process request
                    req = preProcessRequest(s, req);

                    // detect a tool placement and set the current tool session
                    detectToolPlacement(s, req);

                    // pre-process response
                    resp = preProcessResponse(s, req, resp);

                    // set the request and response for access via the thread local
                    ThreadLocalManager.set(CURRENT_HTTP_REQUEST, req);
                    ThreadLocalManager.set(CURRENT_HTTP_RESPONSE, resp);

                    // set the portal into thread local
                    if (m_contextId != null && m_contextId.length() > 0) {
                        ThreadLocalManager.set(ServerConfigurationService.CURRENT_PORTAL_PATH, "/" + m_contextId);
                    }

                    // Only synchronize on session for Terracotta. See KNL-218, KNL-75.
                    if (TERRACOTTA_CLUSTER) {
                        synchronized (s) {
                            // Pass control on to the next filter or the servlet
                            chain.doFilter(req, resp);

                            // post-process response
                            postProcessResponse(s, req, resp);
                        }
                    } else {
                        // Pass control on to the next filter or the servlet
                        chain.doFilter(req, resp);

                        // post-process response
                        postProcessResponse(s, req, resp);
                    }

                    // Output client cookie if requested to do so
                    if (s != null && req.getAttribute(ATTR_SET_COOKIE) != null) {

                        // check for existing cookie
                        String suffix = getCookieSuffix();
                        Cookie c = findCookie(req, cookieName, suffix);

                        // the cookie value we need to use
                        String sessionId = s.getId() + DOT + suffix;

                        // set the cookie if necessary
                        if ((c == null) || (!c.getValue().equals(sessionId))) {
                            c = new Cookie(cookieName, sessionId);
                            c.setPath("/");
                            c.setMaxAge(-1);
                            if (cookieDomain != null) {
                                c.setDomain(cookieDomain);
                            }
                            if (req.isSecure() == true) {
                                c.setSecure(true);
                            }
                            addCookie(resp, c);
                        }
                    }

                } catch (ClosingException se) {
                    closingRedirect(req, resp);
                } catch (RuntimeException t) {
                    M_log.error("", t);
                    throw t;
                } catch (IOException ioe) {
                    M_log.error("", ioe);
                    throw ioe;
                } catch (ServletException se) {
                    M_log.error(se.getMessage(), se);
                    throw se;
                } finally {
                    // clear any bound current values
                    ThreadLocalManager.clear();
                    cleared = true;
                }
            }

        } finally {
            if (!cleared) {
                // restore the "current" bindings
                ThreadLocalManager.set(CURRENT_REMOTE_USER, curRemoteUser);
                ThreadLocalManager.set(CURRENT_HTTP_SESSION, curHttpSession);
                ThreadLocalManager.set(CURRENT_CONTEXT, curContext);
                ThreadLocalManager.set(CURRENT_HTTP_REQUEST, curRequest);
                ThreadLocalManager.set(CURRENT_HTTP_RESPONSE, curResponse);
            }

            // delete any temp files
            deleteTempFiles(tempFiles);

            if (M_log.isDebugEnabled() && sb != null) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                M_log.debug("request timing (ms): " + elapsedTime + " for " + sb);
            }
        }
    }

    /**
     * This is called when a request is made to a node that is in the process of closing down
     * and so we don't want to allow new session to be created.
     * @param req The servlet request.
     * @param res The servlet response.
     */
    protected void closingRedirect(HttpServletRequest req, HttpServletResponse res) throws IOException {
        // We should avoid redirecting on non get methods as the body will be lost.
        if (!"GET".equals(req.getMethod())) {
            M_log.warn("Non GET request for " + req.getPathInfo());
        }

        // We could check that we aren't in a redirect loop here, but if the load balancer doesn't know that
        // a node is no longer responding to new sessions it may still be sending it new clients, and so after
        // a couple of redirects it should hop off this node.
        String value = getRedirectNode();
        // set the cookie
        Cookie c = new Cookie(cookieName, value);
        c.setPath("/");
        // Delete the cookie
        c.setMaxAge(0);
        if (cookieDomain != null) {
            c.setDomain(cookieDomain);
        }
        if (req.isSecure() == true) {
            c.setSecure(true);
        }
        addCookie(res, c);

        // We want the non-decoded ones so we don't have to re-encode.
        StringBuilder url = new StringBuilder(req.getRequestURI());
        if (req.getQueryString() != null) {
            url.append("?").append(req.getQueryString());
        }
        res.sendRedirect(url.toString());
    }

    /**
     * This looks to find a node to redirect to or if it can't find one it just empties the cookie
     * so the load balancer chooses.
     * @return The cookie value for a different node.
     */
    protected String getRedirectNode() {
        if (m_redirectRandomNode) {
            ClusterService clusterService = (ClusterService) ComponentManager.get(ClusterService.class);
            Map<String, ClusterNode> nodes = clusterService.getServerStatus();
            // There may be more than one node listed for each node ID, just list the latest ones.
            Map<String, ClusterNode> latestNodes = new HashMap<>();
            for (ClusterNode node : nodes.values()) {
                ClusterNode latest = latestNodes.get(node.getServerId());
                if (latest == null || latest.getUpdated().after(node.getUpdated())) {
                    latestNodes.put(node.getServerId(), node);
                }
            }
            // This node shouldn't ever be included but it's better safe than sorry.
            latestNodes.remove(System.getProperty(SAKAI_SERVERID));
            // Remove all the non-running servers.
            List<String> activeServers = new ArrayList<>(latestNodes.size());
            for (ClusterNode node : latestNodes.values()) {
                if (Status.RUNNING.equals(node.getStatus())) {
                    activeServers.add(node.getServerId());
                }
            }
            // Pick a random remaining server if we have one.
            if (!(activeServers.isEmpty())) {
                Random random = new Random();
                int i = random.nextInt(activeServers.size());
                String serverId = activeServers.get(i);
                return DOT + serverId;
            }
        }
        return "";
    }

    /**
     * If any of these files exist, delete them.
     *
     * @param tempFiles
     *        The file items to delete.
     */
    protected void deleteTempFiles(List<FileItem> tempFiles) {
        for (FileItem item : tempFiles) {
            item.delete();
        }
    }

    /**
     * Place this filter into service.
     *
     * @param filterConfig
     *        The filter configuration object
     */
    public void init(FilterConfig filterConfig) throws ServletException {

        // Requesting the ServerConfigurationService here also triggers the promotion of certain
        // sakai.properties settings to system properties - see SakaiPropertyPromoter()
        ServerConfigurationService configService = org.sakaiproject.component.cover.ServerConfigurationService
                .getInstance();

        // knl-640
        appUrl = configService.getString("serverUrl", null);
        chsDomain = configService.getString("content.chs.serverName", null);
        chsUrl = configService.getString("content.chs.serverUrl", null);
        useContentHostingDomain = configService.getBoolean("content.separateDomains", false);
        contentPaths = configService.getStrings("content.chs.urlprefixes");
        if (contentPaths == null) {
            contentPaths = new String[] { "/access/", "/web/" };
        }
        loginPaths = configService.getStrings("content.login.urlprefixes");
        if (loginPaths == null) {
            loginPaths = new String[] { "/access/login", "/sakai-login-tool", "/access/require", "/access/accept" };
        }
        contentExceptions = configService.getStrings("content.chsexception.urlprefixes");
        if (contentExceptions == null) {
            // add in default exceptions here, if desired
            contentExceptions = new String[] { "/access/calendar/", "/access/citation/export_ris_sel/",
                    "/access/citation/export_ris_all/" };
        }

        // capture the servlet context for later user
        m_servletContext = filterConfig.getServletContext();

        if (filterConfig.getInitParameter(CONFIG_SESSION) != null) {
            String s = filterConfig.getInitParameter(CONFIG_SESSION);
            if ("container".equalsIgnoreCase(s)) {
                m_sakaiHttpSession = CONTAINER_SESSION;
            } else if ("sakai".equalsIgnoreCase(s)) {
                m_sakaiHttpSession = SAKAI_SESSION;
            } else if ("context".equalsIgnoreCase(s)) {
                m_sakaiHttpSession = CONTEXT_SESSION;
            } else if ("tool".equalsIgnoreCase(s)) {
                m_sakaiHttpSession = TOOL_SESSION;
            } else {
                M_log.warn("invalid " + CONFIG_SESSION + " setting (" + s
                        + "): not one of container, sakai, context, tool");
            }
        }

        if (filterConfig.getInitParameter(CONFIG_REMOTE_USER) != null) {
            m_sakaiRemoteUser = Boolean.valueOf(filterConfig.getInitParameter(CONFIG_REMOTE_USER)).booleanValue();
        }

        if (filterConfig.getInitParameter(CONFIG_SESSION_AUTH) != null) {
            m_checkPrincipal = "basic".equals(filterConfig.getInitParameter(CONFIG_SESSION_AUTH));
        }

        if (filterConfig.getInitParameter(CONFIG_TOOL_PLACEMENT) != null) {
            m_toolPlacement = Boolean.valueOf(filterConfig.getInitParameter(CONFIG_TOOL_PLACEMENT)).booleanValue();
        }

        if (filterConfig.getInitParameter(CONFIG_CONTEXT) != null) {
            m_contextId = filterConfig.getInitParameter(CONFIG_CONTEXT);
        } else {
            // This is a little confusing as we're taking a display name and using it as an ID.
            m_contextId = m_servletContext.getServletContextName();
            if (m_contextId == null) {
                m_contextId = toString();
            }
        }

        if (filterConfig.getInitParameter(CONFIG_CHARACTER_ENCODING) != null) {
            m_characterEncoding = filterConfig.getInitParameter(CONFIG_CHARACTER_ENCODING);
        }

        if (filterConfig.getInitParameter(CONFIG_CHARACTER_ENCODING_ENABLED) != null) {
            m_characterEncodingEnabled = Boolean
                    .valueOf(filterConfig.getInitParameter(CONFIG_CHARACTER_ENCODING_ENABLED)).booleanValue();
        }

        if (filterConfig.getInitParameter(CONFIG_UPLOAD_ENABLED) != null) {
            m_uploadEnabled = Boolean.valueOf(filterConfig.getInitParameter(CONFIG_UPLOAD_ENABLED)).booleanValue();
        }

        // get the maximum allowed upload size from the system property - use if not overriden, and also use as the ceiling if that
        // is not defined.
        if (System.getProperty(SYSTEM_UPLOAD_MAX) != null) {
            m_uploadMaxSize = Long.valueOf(System.getProperty(SYSTEM_UPLOAD_MAX).trim()).longValue() * 1024L
                    * 1024L;
            m_uploadCeiling = m_uploadMaxSize;
        }

        // if the maximum allowed upload size is configured on the filter, it overrides the system property
        if (filterConfig.getInitParameter(CONFIG_UPLOAD_MAX) != null) {
            m_uploadMaxSize = Long.valueOf(filterConfig.getInitParameter(CONFIG_UPLOAD_MAX).trim()).longValue()
                    * 1024L * 1024L;
        }

        // get the upload max ceiling that limits any other upload max, if defined
        if (System.getProperty(SYSTEM_UPLOAD_CEILING) != null) {
            m_uploadCeiling = Long.valueOf(System.getProperty(SYSTEM_UPLOAD_CEILING).trim()).longValue() * 1024L
                    * 1024L;
        }

        // get the system wide settin, if present, for the temp dir
        if (System.getProperty(SYSTEM_UPLOAD_DIR) != null) {
            m_uploadTempDir = System.getProperty(SYSTEM_UPLOAD_DIR);
        }

        // override with our configuration for temp dir, if set
        if (filterConfig.getInitParameter(CONFIG_UPLOAD_DIR) != null) {
            m_uploadTempDir = filterConfig.getInitParameter(CONFIG_UPLOAD_DIR);
        }

        if (filterConfig.getInitParameter(CONFIG_UPLOAD_THRESHOLD) != null) {
            m_uploadThreshold = Integer.valueOf(filterConfig.getInitParameter(CONFIG_UPLOAD_THRESHOLD)).intValue();
        }

        if (filterConfig.getInitParameter(CONFIG_CONTINUE) != null) {
            m_uploadContinue = Boolean.valueOf(filterConfig.getInitParameter(CONFIG_CONTINUE)).booleanValue();
        }

        if (filterConfig.getInitParameter(CONFIG_MAX_PER_FILE) != null) {
            m_uploadMaxPerFile = Boolean.valueOf(filterConfig.getInitParameter(CONFIG_MAX_PER_FILE)).booleanValue();
        }

        // Note: if set to continue processing max exceeded uploads, we only support per-file max, not overall max
        if (m_uploadContinue && !m_uploadMaxPerFile) {
            M_log.warn("overridding " + CONFIG_MAX_PER_FILE + " setting: must be 'true' with " + CONFIG_CONTINUE
                    + " ='true'");
            m_uploadMaxPerFile = true;
        }

        String clusterTerracotta = System.getProperty("sakai.cluster.terracotta");
        TERRACOTTA_CLUSTER = "true".equals(clusterTerracotta);

        // retrieve the configured cookie name, if any
        if (System.getProperty(SAKAI_COOKIE_NAME) != null) {
            cookieName = System.getProperty(SAKAI_COOKIE_NAME);
        }

        // retrieve the configured cookie domain, if any
        if (System.getProperty(SAKAI_COOKIE_DOMAIN) != null) {
            cookieDomain = System.getProperty(SAKAI_COOKIE_DOMAIN);
        }

        m_sessionParamAllow = configService.getBoolean(SAKAI_SESSION_PARAM_ALLOW, false);

        // retrieve option to enable or disable cookie HttpOnly
        m_cookieHttpOnly = configService.getBoolean(SAKAI_COOKIE_HTTP_ONLY, true);

        m_UACompatible = configService.getString(SAKAI_UA_COMPATIBLE, null);

        isLTIProviderAllowed = (configService.getString(SAKAI_BLTI_PROVIDER_TOOLS, null) != null);

        m_redirectRandomNode = configService.getBoolean(SAKAI_CLUSTER_REDIRECT_RANDOM, true);

    }

    /**
     * If setting character encoding is enabled for this filter, and there isn't already a character encoding on the request, then
     * set the encoding.
     */
    protected void handleCharacterEncoding(HttpServletRequest req, HttpServletResponse resp)
            throws UnsupportedEncodingException {
        if (m_characterEncodingEnabled && req.getCharacterEncoding() == null && m_characterEncoding != null
                && m_characterEncoding.length() > 0 && req.getAttribute(ATTR_CHARACTER_ENCODING_DONE) == null) {
            req.setAttribute(ATTR_CHARACTER_ENCODING_DONE, ATTR_CHARACTER_ENCODING_DONE);
            req.setCharacterEncoding(m_characterEncoding);
        }
    }

    /**
     * if the filter is configured to parse file uploads, AND the request is multipart (typically a file upload), then parse the
     * request.
     *
     * @return If there is a file upload, and the filter handles it, return the wrapped request that has the results of the parsed
     *         file upload. Parses the files using Apache commons-fileuplaod. Exposes the results through a wrapped request. Files
     *         are available like: fileItem = (FileItem) request.getAttribute("myHtmlFileUploadId");
     */
    protected HttpServletRequest handleFileUpload(HttpServletRequest req, HttpServletResponse resp,
            List<FileItem> tempFiles) throws ServletException, UnsupportedEncodingException {
        if (!m_uploadEnabled || !ServletFileUpload.isMultipartContent(req)
                || req.getAttribute(ATTR_UPLOADS_DONE) != null) {
            return req;
        }

        // mark that the uploads have been parsed, so they aren't parsed again on this request
        req.setAttribute(ATTR_UPLOADS_DONE, ATTR_UPLOADS_DONE);

        // Result - map that will be created of request parameters,
        // parsed parameters, and uploaded files
        Map map = new HashMap();

        // parse using commons-fileupload

        // Create a factory for disk-based file items
        DiskFileItemFactory factory = new DiskFileItemFactory();

        // set the factory parameters: the temp dir and the keep-in-memory-if-smaller threshold
        if (m_uploadTempDir != null)
            factory.setRepository(new File(m_uploadTempDir));
        if (m_uploadThreshold > 0)
            factory.setSizeThreshold(m_uploadThreshold);

        // Create a new file upload handler
        ServletFileUpload upload = new ServletFileUpload(factory);

        // set the encoding
        String encoding = req.getCharacterEncoding();
        if (encoding != null && encoding.length() > 0)
            upload.setHeaderEncoding(encoding);

        // set the max upload size
        long uploadMax = -1;
        if (m_uploadMaxSize > 0)
            uploadMax = m_uploadMaxSize;

        // check for request-scoped override to upload.max (value in megs)
        String override = req.getParameter(CONFIG_UPLOAD_MAX);
        if (override != null) {
            try {
                // get the max in bytes
                uploadMax = Long.parseLong(override) * 1024L * 1024L;
            } catch (NumberFormatException e) {
                M_log.warn(CONFIG_UPLOAD_MAX + " set to non-numeric: " + override);
            }
        }

        // limit to the ceiling
        if (uploadMax > m_uploadCeiling) {
            /**
             * KNL-602 This is the expected behaviour of the request filter honouring the globaly configured
             * value -DH
             */
            M_log.debug("Upload size exceeds ceiling: " + ((uploadMax / 1024L) / 1024L) + " > "
                    + ((m_uploadCeiling / 1024L) / 1024L) + " megs");

            uploadMax = m_uploadCeiling;
        }

        // to let commons-fileupload throw the exception on over-max, and also halt full processing of input fields
        if (!m_uploadContinue) {
            // TODO: when we switch to commons-fileupload 1.2
            // // either per file or overall request, as configured
            // if (m_uploadMaxPerFile)
            // {
            // upload.setFileSizeMax(uploadMax);
            // }
            // else
            // {
            // upload.setSizeMax(uploadMax);
            // }

            upload.setSizeMax(uploadMax);
        }

        try {
            // parse multipart encoded parameters
            boolean uploadOk = true;
            List list = upload.parseRequest(req);
            for (int i = 0; i < list.size(); i++) {
                FileItem item = (FileItem) list.get(i);

                if (item.isFormField()) {
                    String str = item.getString(encoding);

                    Object obj = map.get(item.getFieldName());
                    if (obj == null) {
                        map.put(item.getFieldName(), new String[] { str });
                    } else if (obj instanceof String[]) {
                        String[] old_vals = (String[]) obj;
                        String[] values = new String[old_vals.length + 1];
                        for (int i1 = 0; i1 < old_vals.length; i1++) {
                            values[i1] = old_vals[i1];
                        }
                        values[values.length - 1] = str;
                        map.put(item.getFieldName(), values);
                    } else if (obj instanceof String) {
                        String[] values = new String[2];
                        values[0] = (String) obj;
                        values[1] = str;
                        map.put(item.getFieldName(), values);
                    }
                } else {
                    // collect it for delete at the end of the request
                    tempFiles.add(item);

                    // check the max, unless we are letting commons-fileupload throw exception on max exceeded
                    // Note: the continue option assumes the max is per-file, not overall.
                    if (m_uploadContinue && (item.getSize() > uploadMax)) {
                        uploadOk = false;

                        M_log.info("Upload size limit exceeded: " + ((uploadMax / 1024L) / 1024L));

                        req.setAttribute("upload.status", "size_limit_exceeded");
                        // TODO: for 1.2 commons-fileupload, switch this to a FileSizeLimitExceededException
                        req.setAttribute("upload.exception",
                                new FileUploadBase.SizeLimitExceededException("", item.getSize(), uploadMax));
                        req.setAttribute("upload.limit", Long.valueOf((uploadMax / 1024L) / 1024L));
                    } else {
                        req.setAttribute(item.getFieldName(), item);
                    }
                }
            }

            // unless we had an upload file that exceeded max, set the upload status to "ok"
            if (uploadOk) {
                req.setAttribute("upload.status", "ok");
            }
        } catch (FileUploadBase.SizeLimitExceededException ex) {
            M_log.info("Upload size limit exceeded: " + ((upload.getSizeMax() / 1024L) / 1024L));

            // DON'T throw an exception, instead note the exception
            // so that the tool down-the-line can handle the problem
            req.setAttribute("upload.status", "size_limit_exceeded");
            req.setAttribute("upload.exception", ex);
            req.setAttribute("upload.limit", Long.valueOf((upload.getSizeMax() / 1024L) / 1024L));
        }
        // TODO: put in for commons-fileupload 1.2
        // catch (FileUploadBase.FileSizeLimitExceededException ex)
        // {
        // M_log.info("Upload size limit exceeded: " + ((upload.getFileSizeMax() / 1024L) / 1024L));
        //
        // // DON'T throw an exception, instead note the exception
        // // so that the tool down-the-line can handle the problem
        // req.setAttribute("upload.status", "size_limit_exceeded");
        // req.setAttribute("upload.exception", ex);
        // req.setAttribute("upload.limit", new Long((upload.getFileSizeMax() / 1024L) / 1024L));
        // }
        catch (FileUploadException ex) {
            M_log.info("Unexpected exception in upload parsing", ex);
            req.setAttribute("upload.status", "exception");
            req.setAttribute("upload.exception", ex);
        }

        // add any parameters that were in the URL - make sure to get multiples
        for (Enumeration e = req.getParameterNames(); e.hasMoreElements();) {
            String name = (String) e.nextElement();
            String[] values = req.getParameterValues(name);
            map.put(name, values);
        }

        // return a wrapped response that exposes the parsed parameters and files
        return new WrappedRequestFileUpload(req, map);
    }

    /**
     * Make sure we have a Sakai session.
     *
     * @param req
     *        The request object.
     * @param res
     *        The response object.
     * @return The Sakai Session object.
     */
    protected Session assureSession(HttpServletRequest req, HttpServletResponse res) {
        Session s = null;
        String sessionId = null;
        boolean allowSetCookieEarly = true;
        Cookie c = null;

        // automatic, i.e. not from user activity, request?
        boolean auto = req.getParameter(PARAM_AUTO) != null;

        // session id provided in a request parameter?
        boolean reqsession = m_sessionParamAllow && req.getParameter(ATTR_SESSION) != null;

        String suffix = getCookieSuffix();

        // try finding a non-cookie session based on the remote user / principal
        // Note: use principal instead of remote user to avoid any possible confusion with the remote user set by single-signon
        // auth.
        // Principal is set by our Dav interface, which this is designed to cover. -ggolden

        Principal principal = req.getUserPrincipal();

        if (m_checkPrincipal && (principal != null) && (principal.getName() != null)) {
            // set our session id to the remote user id
            sessionId = SessionManager.makeSessionId(req, principal);

            // don't supply this cookie to the client
            allowSetCookieEarly = false;

            // find the session
            s = SessionManager.getSession(sessionId);

            // if not found, make a session for this user
            if (s == null) {
                s = SessionManager.startSession(sessionId);
            }

            // Make these sessions expire after 10 minutes
            s.setMaxInactiveInterval(10 * 60);
        }

        // if no principal, check request parameter and cookie
        if (sessionId == null || s == null) {
            if (m_sessionParamAllow) {
                sessionId = req.getParameter(ATTR_SESSION);
            }

            // find our session id from our cookie
            c = findCookie(req, cookieName, suffix);

            if (sessionId == null && c != null) {
                // get our session id
                sessionId = c.getValue();
            }

            if (sessionId != null) {
                // remove the server id suffix
                final int dotPosition = sessionId.indexOf(DOT);
                if (dotPosition > -1) {
                    sessionId = sessionId.substring(0, dotPosition);
                }
                if (M_log.isDebugEnabled()) {
                    M_log.debug("assureSession found sessionId in cookie: " + sessionId);
                }

                // find the session
                s = SessionManager.getSession(sessionId);
            }

            // ignore the session id provided in a request parameter
            // if the session is not authenticated
            if (reqsession && s != null && s.getUserId() == null) {
                s = null;
            }
        }

        // if found and not automatic, mark it as active
        if ((s != null) && (!auto)) {
            synchronized (s) {
                s.setActive();
            }
        }
        if (s == null && sessionId != null) {
            // check to see if this session has already been built.  If not, rebuild
            RebuildBreakdownService rebuildBreakdownService = (RebuildBreakdownService) ComponentManager
                    .get(RebuildBreakdownService.class);
            if (rebuildBreakdownService != null) {
                s = SessionManager.startSession(sessionId);
                if (!rebuildBreakdownService.rebuildSession(s)) {
                    s.invalidate();
                    s = null;
                }
            }
        }

        // if missing, make one
        if (s == null) {
            s = SessionManager.startSession();

            // if we have a cookie, but didn't find the session and are creating a new one, mark this
            if (c != null) {
                ThreadLocalManager.set(SessionManager.CURRENT_INVALID_SESSION,
                        SessionManager.CURRENT_INVALID_SESSION);
            }
        }

        // put the session in the request attribute
        req.setAttribute(ATTR_SESSION, s);

        // set this as the current session
        SessionManager.setCurrentSession(s);

        // Now that we know the session exists, regardless of whether it's new or not, lets see if there
        // is a UsageSession.  If so, we want to check it's serverId
        UsageSession us = null;
        // FIXME synchronizing on a changing value is a bad practice plus it is possible for s to be null according to the visible code -AZ
        synchronized (s) {
            us = (UsageSession) s.getAttribute(UsageSessionService.USAGE_SESSION_KEY);
            if (us != null) {
                // check the server instance id
                ServerConfigurationService configService = org.sakaiproject.component.cover.ServerConfigurationService
                        .getInstance();
                String serverInstanceId = configService.getServerIdInstance();
                if ((serverInstanceId != null) && (!serverInstanceId.equals(us.getServer()))) {
                    // Log that the UsageSession server value is changing
                    M_log.info("UsageSession: Server change detected: Old Server=" + us.getServer()
                            + "    New Server=" + serverInstanceId);
                    // set the new UsageSession server value
                    us.setServer(serverInstanceId);
                }
            }
        }

        // if we had a cookie and we have no session, clear the cookie TODO: detect closed session in the request
        if ((s == null) && (c != null)) {
            // remove the cookie
            c = new Cookie(cookieName, "");
            c.setPath("/");
            c.setMaxAge(0);
            if (cookieDomain != null) {
                c.setDomain(cookieDomain);
            }
            addCookie(res, c);
        }

        // if we have a session and had no cookie,
        // or the cookie was to another session id, set the cookie
        if ((s != null) && allowSetCookieEarly) {
            // the cookie value we need to use
            sessionId = s.getId() + DOT + suffix;

            if ((c == null) || (!c.getValue().equals(sessionId))) {
                // set the cookie
                c = new Cookie(cookieName, sessionId);
                c.setPath("/");
                c.setMaxAge(-1);
                if (cookieDomain != null) {
                    c.setDomain(cookieDomain);
                }
                if (req.isSecure() == true) {
                    c.setSecure(true);
                }
                addCookie(res, c);
            }
        }

        return s;
    }

    /**
     * Detect a tool placement from the URL, and if found, setup the placement attribute and current tool session based on that id.
     *
     * @param s
     *        The sakai session.
     * @param req
     *        The request, already prepared with the placement id if any.
     * @return The tool session.
     */
    protected ToolSession detectToolPlacement(Session s, HttpServletRequest req) {
        // skip if so configured
        if (this.m_toolPlacement == false)
            return null;

        ToolSession toolSession = null;
        String placementId = (String) req.getParameter(Tool.PLACEMENT_ID);
        if (placementId != null) {
            toolSession = s.getToolSession(placementId);

            // put the session in the request attribute
            req.setAttribute(Tool.TOOL_SESSION, toolSession);

            // set as the current tool session
            SessionManager.setCurrentToolSession(toolSession);

            // put the placement id in the request attribute
            req.setAttribute(Tool.PLACEMENT_ID, placementId);
        }

        return toolSession;
    }

    /**
     * Pre-process the request, returning a possibly wrapped req for further processing.
     *
     * @param s
     *        The Sakai Session.
     * @param req
     *        The request object.
     * @return a possibly wrapped and possibly new request object for further processing.
     */
    protected HttpServletRequest preProcessRequest(Session s, HttpServletRequest req) {
        req = new WrappedRequest(s, m_contextId, req);

        return req;
    }

    /**
     * Pre-process the response, returning a possibly wrapped res for further processing.
     *
     * @param s
     *        The Sakai Session.
     * @param req
     *        The request object.
     * @param res
     *        The response object.
     * @return a possibly wrapped and possibly new response object for further processing.
     */
    protected HttpServletResponse preProcessResponse(Session s, HttpServletRequest req, HttpServletResponse res) {
        res = new WrappedResponse(s, req, res);
        //Set response headers SAK-20058
        if (m_UACompatible != null) {
            res.setHeader("X-UA-Compatible", m_UACompatible);
        }

        if (!isLTIProviderAllowed && (!useContentHostingDomain || !req.getServerName().equals(chsDomain))) {
            res.setHeader("X-Frame-Options", "SAMEORIGIN");
        }

        UsageSession us = (UsageSession) s.getAttribute(UsageSessionService.USAGE_SESSION_KEY);
        if (us != null) {
            res.setHeader("X-Sakai-Session", us.getId());
        }

        return res;
    }

    /**
     * Post-process the response.
     *
     * @param s
     *        The Sakai Session.
     * @param req
     *        The request object.
     * @param res
     *        The response object.
     */
    protected void postProcessResponse(Session s, HttpServletRequest req, HttpServletResponse res) {
        RebuildBreakdownService rebuildBreakdownService = (RebuildBreakdownService) ComponentManager
                .get(RebuildBreakdownService.class);
        if (rebuildBreakdownService != null) {
            rebuildBreakdownService.storeSession(s, req);
        }
    }

    /**
     * isSessionClusteringEnabled() checks if session information is clustered.
     * Clustering can be either through Terracotta clustering or through
     * RebuildBreakdownService session clustering
     * @return true if sessionClustering is enabled
     */
    private boolean isSessionClusteringEnabled() {
        RebuildBreakdownService rebuildBreakdownService = (RebuildBreakdownService) ComponentManager
                .get(RebuildBreakdownService.class);
        return TERRACOTTA_CLUSTER || rebuildBreakdownService.isSessionHandlingEnabled();
    }

    /**
     * Find a cookie by this name from the request; one with a value that has the specified suffix.
     *
     * @param req
     *        The servlet request.
     * @param name
     *        The cookie name
     * @param suffix
     *        The suffix string to find at the end of the found cookie value.
     * @return The cookie of this name in the request, or null if not found.
     */
    protected Cookie findCookie(HttpServletRequest req, String name, String suffix) {
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (int i = 0; i < cookies.length; i++) {
                if (cookies[i].getName().equals(name)) {
                    // If this is NOT a terracotta cluster environment
                    // and the suffix passed in to this method is not null
                    // then only match the cookie if the end of the cookie
                    // value is equal to the suffix passed in.
                    if (isSessionClusteringEnabled()
                            || ((suffix == null) || cookies[i].getValue().endsWith(suffix))) {
                        return cookies[i];
                    }
                }
            }
        }

        return null;
    }

    /**
     * Get cookie suffix from the serverId.
     * We can't do this at init time as it might not have been set yet (sakai hasn't started).
     * @return The cookie suffix to use.
     */
    private String getCookieSuffix() {
        // compute the session cookie suffix, based on this configured server id
        String suffix = System.getProperty(SAKAI_SERVERID);
        if ((suffix == null) || (suffix.length() == 0)) {
            if (m_displayModJkWarning) {
                M_log.warn(
                        "no sakai.serverId system property set - mod_jk load balancing will not function properly");
            }
            m_displayModJkWarning = false;
            suffix = "sakai";
        }
        return suffix;
    }

    protected void addCookie(HttpServletResponse res, Cookie cookie) {

        if (!m_cookieHttpOnly) {
            // Use the standard servlet mechanism for setting the cookie
            res.addCookie(cookie);
        } else {
            // Set the cookie manually

            StringBuffer sb = new StringBuffer();

            ServerCookie.appendCookieValue(sb, cookie.getVersion(), cookie.getName(), cookie.getValue(),
                    cookie.getPath(), cookie.getDomain(), cookie.getComment(), cookie.getMaxAge(),
                    cookie.getSecure(), m_cookieHttpOnly);

            res.addHeader("Set-Cookie", sb.toString());
        }
        return;
    }

    /**
     * Request wrapper that exposes the parameters parsed from the multipart/mime file upload (along with parameters from the
     * request).
     */
    static class WrappedRequestFileUpload extends HttpServletRequestWrapper {
        private Map map;

        /**
         * Constructs a wrapped response that exposes the given map of parameters.
         *
         * @param req
         *        The request to wrap.
         * @param paramMap
         *        The parameters to expose.
         */
        public WrappedRequestFileUpload(HttpServletRequest req, Map paramMap) {
            super(req);
            map = paramMap;
        }

        public Map getParameterMap() {
            return map;
        }

        public String[] getParameterValues(String name) {
            String[] ret = null;
            Map map = getParameterMap();
            return (String[]) map.get(name);
        }

        public String getParameter(String name) {
            String[] params = getParameterValues(name);
            if (params == null)
                return null;
            return params[0];
        }

        public Enumeration getParameterNames() {
            Map map = getParameterMap();
            return Collections.enumeration(map.keySet());
        }
    }

    /**
     * Wraps a request object so we can override some standard behavior.
     */
    public class WrappedRequest extends HttpServletRequestWrapper {
        /** The Sakai session. */
        protected Session m_session = null;

        /** Our contex (i.e. servlet context) id. */
        protected String m_contextId = null;

        public WrappedRequest(Session s, String contextId, HttpServletRequest req) {
            super(req);
            m_session = s;
            m_contextId = contextId;

            if (m_toolPlacement) {
                extractPlacementFromParams();
            }
        }

        public String getRemoteUser() {
            // use the "current" setting for this
            boolean remoteUser = ((Boolean) ThreadLocalManager.get(CURRENT_REMOTE_USER)).booleanValue();

            if (remoteUser && (m_session != null) && (m_session.getUserEid() != null)) {
                return m_session.getUserEid();
            }

            return super.getRemoteUser();
        }

        public HttpSession getSession() {
            return getSession(true);
        }

        public HttpSession getSession(boolean create) {
            HttpSession rv = null;

            // use the "current" settings for this
            int curHttpSession = ((Integer) ThreadLocalManager.get(CURRENT_HTTP_SESSION)).intValue();
            String curContext = (String) ThreadLocalManager.get(CURRENT_CONTEXT);

            switch (curHttpSession) {
            case CONTAINER_SESSION: {
                rv = super.getSession(create);
                break;
            }

            case SAKAI_SESSION: {
                rv = (HttpSession) m_session;
                break;
            }

            case CONTEXT_SESSION: {
                rv = (HttpSession) m_session.getContextSession(curContext);
                break;
            }

            case TOOL_SESSION: {
                rv = (HttpSession) SessionManager.getCurrentToolSession();
                if (rv == null) {
                    rv = (HttpSession) m_session.getContextSession(curContext);
                }
                break;
            }
            }

            return rv;
        }

        /**
         * Pull the specially encoded tool placement id from the request parameters.
         */
        protected void extractPlacementFromParams() {
            String placementId = getParameter(Tool.PLACEMENT_ID);
            if (placementId != null) {
                setAttribute(Tool.PLACEMENT_ID, placementId);
            }
        }
    }

    /**
     * Wraps a response object so we can override some standard behavior.
     */
    public class WrappedResponse extends HttpServletResponseWrapper {
        /** The request. */
        protected HttpServletRequest m_req = null;

        /** Wrapped Response * */
        protected HttpServletResponse m_res = null;

        public WrappedResponse(Session s, HttpServletRequest req, HttpServletResponse res) {
            super(res);

            m_req = req;
            m_res = res;
        }

        public String encodeRedirectUrl(String url) {
            return rewriteURL(url);
        }

        public String encodeRedirectURL(String url) {
            return rewriteURL(url);
        }

        public String encodeUrl(String url) {
            return rewriteURL(url);
        }

        public String encodeURL(String url) {
            return rewriteURL(url);
        }

        public void sendRedirect(String url) throws IOException {
            url = rewriteURL(url);
            m_req.setAttribute(ATTR_REDIRECT, url);
            super.sendRedirect(url);
        }

        /**
         * Rewrites the given URL to insert the current tool placement id, if any, as the start of the path
         *
         * @param url
         *        The url to rewrite.
         */
        protected String rewriteURL(String url) {
            if (m_toolPlacement) {
                // if we have a tool placement to add, add it
                String placementId = (String) m_req.getAttribute(Tool.PLACEMENT_ID);
                if (placementId != null) {
                    // compute the URL root "back" to this servlet context (rel and full)
                    StringBuilder full = new StringBuilder();
                    full.append(m_req.getScheme());
                    full.append("://");
                    full.append(m_req.getServerName());
                    if (((m_req.getServerPort() != 80) && (!m_req.isSecure()))
                            || ((m_req.getServerPort() != 443) && (m_req.isSecure()))) {
                        full.append(":");
                        full.append(m_req.getServerPort());
                    }

                    StringBuilder rel = new StringBuilder();
                    rel.append(m_req.getContextPath());

                    full.append(rel.toString());

                    // if we match the fullUrl, or the relUrl, assume that this is a URL back to this servlet
                    if ((url.startsWith(full.toString()) || url.startsWith(rel.toString()))) {
                        // put the placementId in as a parameter
                        StringBuilder newUrl = new StringBuilder(url);
                        if (url.indexOf('?') != -1) {
                            newUrl.append('&');
                        } else {
                            newUrl.append('?');
                        }
                        newUrl.append(Tool.PLACEMENT_ID);
                        newUrl.append("=");
                        newUrl.append(placementId);
                        url = newUrl.toString();
                    }
                }
            }

            // Chain back so the wrapped response can encode the URL futher if needed
            // this is necessary for WSRP support.
            if (m_res != null)
                url = m_res.encodeURL(url);

            return url;
        }
    }

}