com.adito.reverseproxy.ReverseProxyMethodHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.adito.reverseproxy.ReverseProxyMethodHandler.java

Source

/*
*  Adito
*
*  Copyright (C) 2003-2006 3SP LTD. All Rights Reserved
*
*  This program is free software; you can redistribute it and/or
*  modify it under the terms of the GNU General Public License
*  as published by the Free Software Foundation; either version 2 of
*  the License, or (at your option) any later version.
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU General Public License for more details.
*
*  You should have received a copy of the GNU General Public
*  License along with this program; if not, write to the Free Software
*  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

package com.adito.reverseproxy;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.servlet.http.Cookie;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.maverick.http.AuthenticationCancelledException;
import com.maverick.http.HttpAuthenticatorFactory;
import com.maverick.http.HttpClient;
import com.maverick.http.HttpException;
import com.maverick.http.HttpResponse;
import com.maverick.http.PasswordCredentials;
import com.maverick.http.UnsupportedAuthenticationException;
import com.maverick.util.URLUTF8Encoder;
import com.adito.boot.ContextHolder;
import com.adito.boot.HttpConstants;
import com.adito.boot.RequestHandler;
import com.adito.boot.RequestHandlerException;
import com.adito.boot.RequestHandlerRequest;
import com.adito.boot.RequestHandlerResponse;
import com.adito.boot.SystemProperties;
import com.adito.boot.Util;
import com.adito.core.MultiMap;
import com.adito.core.stringreplacement.SessionInfoReplacer;
import com.adito.core.stringreplacement.VariableReplacement;
import com.adito.policyframework.LaunchSession;
import com.adito.policyframework.LaunchSessionFactory;
import com.adito.security.Constants;
import com.adito.security.LogonControllerFactory;
import com.adito.security.SessionInfo;
import com.adito.util.ProxiedHttpMethod;
import com.adito.vfs.webdav.DAVUtilities;
import com.adito.webforwards.AbstractAuthenticatingWebForwardHandler;
import com.adito.webforwards.ReverseProxyWebForward;
import com.adito.webforwards.WebForwardPlugin;
import com.adito.webforwards.WebForwardTypes;

/**
 * Request handler that deals with both <i>Reverse Proxy</i> and <i>Replacement
 * Proxy</i> web forwards.
 */
public class ReverseProxyMethodHandler extends AbstractAuthenticatingWebForwardHandler implements RequestHandler {

    /**
     * Launch session attribute for storing whether authentication has been
     * posted yet
     */
    public static final String LAUNCH_ATTR_AUTH_POSTED = "authPosted";

    final static String sessionCookie = SystemProperties.get("adito.cookie", "JSESSIONID");

    static HashSet<String> ignoredHeaders = new HashSet<String>();

    static {
        ignoredHeaders.add("Location".toUpperCase());
        ignoredHeaders.add("Server".toUpperCase());
        ignoredHeaders.add("Date".toUpperCase());
    }

    static Log log = LogFactory.getLog(ReverseProxyMethodHandler.class);

    public boolean handle(String pathInContext, String pathParams, RequestHandlerRequest request,
            RequestHandlerResponse response) throws RequestHandlerException, IOException {
        if (log.isDebugEnabled())
            log.debug("Check if Reverse Proxy Request for: " + pathInContext);

        /*
         * First try and locate the session, if there is no session then this is
         * definitely not a reverse proxy request
         */
        LaunchSession launchSession = null;
        SessionInfo session = locateSession(request, response);

        if (session == null) {
            // If we have no session, then this cannot be a reverse proxy
            // request
            if (log.isDebugEnabled())
                log.debug("No session, not a reverse proxy.");
            return false;
        }

        try {
            // Perhaps this is a reverse proxy?
            String host = request.getHost();
            ReverseProxyWebForward wf = null;

            // Active Proxy

            if (host != null && !host.equals("") && host.indexOf('.') > -1) {
                int idx = host.indexOf('.');
                if (idx != -1) {
                    try {
                        String uniqueId = host.substring(0, idx);
                        launchSession = LaunchSessionFactory.getInstance().getLaunchSession(session, uniqueId);
                        if (launchSession != null) {
                            wf = (ReverseProxyWebForward) launchSession.getResource();
                            launchSession.checkAccessRights(null, session);
                            if (!((ReverseProxyWebForward) wf).getActiveDNS()) {
                                throw new Exception(
                                        "Appears to be an active DNS request but the associated web forward is not active DNS. Is someone trying something funny???");
                            }
                            LogonControllerFactory.getInstance().addCookies(request, response,
                                    session.getLogonTicket(), session);
                            return handleReverseProxy(pathInContext, pathParams, request, response, launchSession);
                        }

                    } catch (Exception ex) {
                        if (log.isDebugEnabled())
                            log.debug("Active DNS web forward lookup failed", ex);
                    }
                } else {
                    if (log.isDebugEnabled())
                        log.debug("Not active DNS.");
                }
            }

            String hostHeader = request.getHost();
            int idx = hostHeader.indexOf(':');
            if (idx > -1)
                hostHeader = hostHeader.substring(0, idx);

            /* Ordinary reverse proxy? There can only ever be one launch session per reverse proxy
             * as there is no way of maintaining the session across requests. If a user launches the
             * resource more than once, the old launch session will be removed
             */

            for (LaunchSession rs : LaunchSessionFactory.getInstance().getLaunchSessionsForType(session,
                    WebForwardPlugin.WEBFORWARD_RESOURCE_TYPE)) {
                if (rs.getResource() instanceof ReverseProxyWebForward) {
                    wf = (ReverseProxyWebForward) rs.getResource();
                    // Check that its not reverseProxyRedirect.jsp because if we don't it breaks access after first attempt in same session
                    if (wf.isValidPath(pathInContext)
                            || (wf.getHostHeader() != null && wf.getHostHeader().equals(hostHeader)
                                    && !pathInContext.startsWith("/reverseProxyRedirect.jsp"))) {
                        rs.checkAccessRights(null, session);
                        return handleReverseProxy(pathInContext, pathParams, request, response, rs);
                    }
                }
            }
        } catch (Exception e) {
            log.error("Failed to process web forward.", e);
            if (session != null) {
                session.getHttpSession().setAttribute(Constants.EXCEPTION, e);
                response.sendRedirect("/showPopupException.do");
            } else {
                throw new RequestHandlerException("Failed to process web forward.", 500);
            }
            return true;
        }
        return false;
    }

    private boolean handleReverseProxy(String path, String params, RequestHandlerRequest request,
            RequestHandlerResponse response, LaunchSession launchSession) throws IOException {

        ReverseProxyWebForward webForward = (ReverseProxyWebForward) launchSession.getResource();
        boolean connectionError = true;

        /* Because we are in a request handler, the session's last access time
         * does not get updated by the container
         */
        launchSession.getSession().access();

        /***
         * LDP - DO NOT use request parameter map until the encoding has been set. If you
         * call getParameters it decodes the parameters so this can only be done once the 
         * character set has been set. 
         */

        try {
            URL target = getTarget(launchSession, request);
            setRequestEncoding(launchSession, target, request);
            HttpClient client = getClient(launchSession, target);
            ProxiedHttpMethod method = getMethod(client, launchSession, request, target);
            processPortsAndXForwarding(method, request);
            checkProcessedContent(launchSession, method, request);
            addCustomHeaders(webForward, method);

            /* If this webforward has JavaScript form authentication and this hasn't
             * yet been processed, then we make sure the content that comes back from
             * the target is not encoded using gzip or any other compression methods.
             * This is because we will be tacking on some content and its easy to
             * deal with unencoded. 
             * 
             * TODO We may want to support at least Gzip at some point
             */
            if (webForward.getFormType().equals(WebForwardTypes.FORM_SUBMIT_JAVASCRIPT)
                    && !Boolean.TRUE.equals(launchSession.getAttribute(LAUNCH_ATTR_AUTH_POSTED))) {
                method.getProxiedRequest().removeFields("Accept-Encoding");
            }

            com.maverick.http.HttpResponse clientResponse = doExecute(client, method);
            connectionError = false;
            checkInsecureIIS(webForward, clientResponse);
            filterUnsupportedAuthMethods(clientResponse);
            processStatus(clientResponse, response);
            processRedirects(clientResponse, request, response);
            processHeaders(clientResponse, response);

            /*
             * If the content type is HTML, this webforward is configured for
             * automatic JavaScript authentication and authentication has not
             * yet been performed, then tack the JavaScript on to the end of the
             * content. This requires that the content is read into memory and
             * the content length adjusted
             */
            if (clientResponse.getStatus() == 200
                    && webForward.getFormType().equals(WebForwardTypes.FORM_SUBMIT_JAVASCRIPT)
                    && "text/html".equals(clientResponse.getContentTypeWithoutParameter())
                    && !Boolean.TRUE.equals(launchSession.getAttribute(LAUNCH_ATTR_AUTH_POSTED))) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                Util.copy(clientResponse.getInputStream(), baos, -1, 16384);
                addJavaScriptAuthenticationCode(launchSession, baos, 0);
                byte[] arr = baos.toByteArray();
                if (clientResponse.getHeaderField("Content-Length") != null) {
                    response.setField("Content-Length", String.valueOf(arr.length));
                }
                response.getOutputStream().write(arr);
                launchSession.setAttribute(LAUNCH_ATTR_AUTH_POSTED, Boolean.TRUE);
            } else {
                Util.copy(clientResponse.getInputStream(), response.getOutputStream(), -1, 16384);
            }

            clientResponse.close();

            return true;
        } catch (UnsupportedAuthenticationException ex) {
            log.error("?", ex);
        } catch (com.maverick.http.HttpException ex) {
            log.error("?", ex);
        } catch (EOFException e) {
            /*
             * This is probably just because the user clicked on a link before
             * the page had finished downloading.
             */
            if (log.isDebugEnabled())
                log.debug("Received EOF in reverse proxy request [THIS IS PROBABLY NOT FATAL]", e);
        } catch (IOException ex) {
            if (connectionError) {
                response.sendError(404,
                        "The proxied web server could not be contacted or did not respond correctly to the request! "
                                + ex.getMessage());
            }
            log.error("?", ex);
        } catch (AuthenticationCancelledException ex) {
            log.error("?", ex);
        }
        return true;

    }

    HttpResponse doExecute(HttpClient client, ProxiedHttpMethod method) throws UnknownHostException, IOException,
            HttpException, UnsupportedAuthenticationException, AuthenticationCancelledException {
        if (log.isDebugEnabled()) {
            log.debug("Connecting to " + client.getHost() + ":" + client.getPort() + " (Secure = "
                    + client.isSecure() + ")");
        }
        return client.execute(method);
    }

    URL getTarget(LaunchSession launchSession, RequestHandlerRequest request) throws MalformedURLException {
        ReverseProxyWebForward webForward = (ReverseProxyWebForward) launchSession.getResource();
        VariableReplacement r = new VariableReplacement();
        r.setRequest(request);
        r.setSession(launchSession.getSession());
        r.setPolicy(launchSession.getPolicy());

        URL target = new URL(r.replace(webForward.getDestinationURL()));

        if (log.isDebugEnabled()) {
            log.debug("Reverse proxy target  " + target.toExternalForm());
        }
        return target;
    }

    void checkProcessedContent(LaunchSession launchSession, ProxiedHttpMethod method, RequestHandlerRequest request)
            throws IOException {
        String contentType = request.getContentType();
        int contentLength = request.getContentLength();

        boolean hasProcessedContent = contentType != null && request.getMethod().equals("POST")
                && contentType.equals("application/x-www-form-urlencoded");

        if (contentLength > 0 && !hasProcessedContent) {
            if (log.isDebugEnabled())
                log.debug("Setting request content of " + contentLength + " bytes with content type " + contentType
                        + " available=" + request.getInputStream().available());
            method.setContent(request.getInputStream(), contentLength, contentType);
        }

    }

    private void processRequestHeaders(RequestHandlerRequest request, ProxiedHttpMethod method) {
        String header;

        for (Enumeration e = request.getFieldNames(); e.hasMoreElements();) {
            header = (String) e.nextElement();

            // Skip the connection header as our client maintains its own
            // connections
            if (header.equalsIgnoreCase(HttpConstants.HDR_CONNECTION)
                    || header.equalsIgnoreCase(HttpConstants.HDR_KEEP_ALIVE)) {
                continue;
            }

            for (Enumeration j = request.getFieldValues(header); j.hasMoreElements();) {
                String val = (String) j.nextElement();
                if (header.equalsIgnoreCase("cookie")) {
                    String[] cookieVals = val.split("\\;");
                    StringBuffer newVal = new StringBuffer();
                    for (int i = 0; i < cookieVals.length; i++) {
                        if (log.isDebugEnabled())
                            log.debug("Cookie = " + cookieVals[i]);

                        // Its possible cookies may be sent without values
                        int idx = cookieVals[i].indexOf('=');
                        String cn = idx == -1 ? cookieVals[i] : Util.trimBoth(cookieVals[i].substring(0, idx));
                        String cv = idx == -1 ? null : Util.trimBoth(cookieVals[i].substring(idx + 1));

                        // Ignore SSL-Exploer cookies
                        if (cn.equals(Constants.LOGON_TICKET) || cn.equals(Constants.DOMAIN_LOGON_TICKET)
                                || cn.equals(SystemProperties.get("adito.cookie", "SSLX_SSESHID"))) {
                            if (log.isDebugEnabled())
                                log.debug("  Omiting cookie " + cn + "=" + cv);
                        } else {
                            if (newVal.length() > 0) {
                                newVal.append("; ");
                            }
                            newVal.append(cn);
                            if (cv != null) {
                                newVal.append("=");
                                newVal.append(cv);
                            }
                        }
                    }
                    if (newVal.length() > 0) {
                        method.getProxiedRequest().addHeaderField(header, newVal.toString());
                        if (log.isDebugEnabled())
                            log.debug("HEADER: " + header + " " + val);
                    }
                } else {
                    method.getProxiedRequest().addHeaderField(header, val);
                    if (log.isDebugEnabled())
                        log.debug("HEADER: " + header + " " + val);
                }
            }
        }

    }

    void processPortsAndXForwarding(ProxiedHttpMethod method, RequestHandlerRequest request) {

        String thisHost = ContextHolder.getContext().getHostname();
        int thisPort = ContextHolder.getContext().getPort();

        // Check for a non default port (we dont care if we receive on 443
        // but
        // forward to 80 as this would not change the host header
        if (thisPort != 443 && thisPort != 80) {
            thisHost += ":" + thisPort;
        }

        method.getProxiedRequest().setHeaderField("X-Forwarded-Host", thisHost);
        method.getProxiedRequest().setHeaderField("X-Forwarded-For", request.getRemoteHost());
        method.getProxiedRequest().setHeaderField("X-Forwarded-Server", thisHost);
        method.getProxiedRequest().setHeaderField("X-Forwarded-Port", String.valueOf(thisPort));

    }

    void processStatus(HttpResponse clientResponse, RequestHandlerResponse response) {

        if (log.isDebugEnabled())
            log.debug("HTTP response is " + clientResponse.getStartLine());

        response.setStatus(clientResponse.getStatus());
        response.setReason(clientResponse.getReason());

    }

    void processHeaders(HttpResponse clientResponse, RequestHandlerResponse response) {

        String header;
        for (Enumeration e = clientResponse.getHeaderFieldNames(); e.hasMoreElements();) {
            header = (String) e.nextElement();
            if (log.isDebugEnabled()) {
                log.debug("Received header " + header);
            }
            String[] val = clientResponse.getHeaderFields(header);
            if (val == null) {
                log.debug("No value???");
            }

            if (ignoredHeaders.contains(header.toUpperCase())) {
                if (log.isDebugEnabled())
                    log.debug("Ignoring header " + header);
                continue;
            }

            for (int i = 0; i < val.length; i++) {
                if (log.isDebugEnabled()) {
                    log.debug("Adding value " + val[i] + " for " + header);
                }
                if (i == 0)
                    response.setField(header, val[i]);
                else
                    response.addField(header, val[i]);
            }
        }
    }

    void processRedirects(HttpResponse clientResponse, RequestHandlerRequest request,
            RequestHandlerResponse response) {

        /**
         * Process redirect Location headers, the location may be a HTTP
         * resource which will require changing to HTTPS
         */
        if (clientResponse.getStatus() >= 300 && clientResponse.getStatus() < 400) {
            switch (clientResponse.getStatus()) {
            case 300: // Multiple choices
            case 301: // Moved permanentley
            case 302: // Found
            case 303: // See other
            case 307: // Temporarily redirect
                String[] locations = clientResponse.getHeaderFields(HttpConstants.HDR_LOCATION);
                response.removeField(HttpConstants.HDR_LOCATION);
                for (int i = 0; i < locations.length; i++) {
                    String originatingHost = clientResponse.getConnection().getHost();
                    String location = rebuildLocation(Util.urlDecode(locations[i]), request.getHost(),
                            originatingHost);
                    response.addField("Location", location);
                    if (log.isDebugEnabled())
                        log.debug("Location is now '" + location + "'");
                }
                break;
            case 304: // Not Modified
                // Do nothing return as is
                break;
            case 305: // Use proxy
                log.warn("Detected HTTP response 305 [Use proxy] this may break reverse proxy!");
                break;
            default:
                log.error("Got unknown 3XX response code from server " + clientResponse.getStatus());
            }
        }
    }

    static final String rebuildLocation(String location, String host, String originatingHost) {
        // Check against the requests Host value and change the Location if required
        if (location.startsWith("http://" + host)) {
            String protocolStripped = stripProtocol(location);
            return encodeURL("https://" + protocolStripped);
        } else if (location.startsWith("http://" + originatingHost)
                || location.startsWith("https://" + originatingHost)) {
            String protocolStripped = stripProtocol(location);
            int indexOf = protocolStripped.indexOf('/');
            if (indexOf == -1) {
                return encodeURL("https://" + host);
            } else {
                String remainingPath = protocolStripped.substring(indexOf);
                return encodeURL("https://" + host + remainingPath);
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Redirect location may result in reverse proxy error " + location);
            }
        }
        return encodeURL(location);
    }

    static final String stripProtocol(String url) {
        if (url.startsWith("http://")) {
            return url.substring(7);
        } else if (url.startsWith("https://")) {
            return url.substring(8);
        }
        return url;
    }

    void addCustomHeaders(ReverseProxyWebForward webForward, ProxiedHttpMethod method) {

        /**
         * Add any custom headers to the request
         */
        Map customHeaders = webForward.getCustomHeaders();
        String header;
        for (Iterator it = customHeaders.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry) it.next();
            header = (String) entry.getKey();
            Vector v = (Vector) entry.getValue();
            for (Iterator it2 = v.iterator(); it2.hasNext();) {
                method.getProxiedRequest().addHeaderField(header, (String) it2.next());
            }
        }
    }

    void checkInsecureIIS(ReverseProxyWebForward webForward, HttpResponse clientResponse) {

        /**
         * Perform a check to see if we're connected to an IIS server and if the
         * backend server is insecure. If it is set the customer
         * Front-End-Https: on header.
         */
        String server = clientResponse.getHeaderField("Server");

        if (server != null && server.startsWith("Microsoft-IIS")) {
            if (!webForward.containsCustomHeader("Front-End-Https"))
                webForward.setCustomHeader("Front-End-Https", "on");
        }
    }

    void filterUnsupportedAuthMethods(HttpResponse clientResponse) {

        /**
         * Filter out unsupported authentication methods because they dont work
         * through the reverse proxy - the best way to enable these will be to
         * allow credentials to be set on the reverse proxy web forward.
         */
        String[] challenges = clientResponse.getHeaderFields("www-authenticate");

        if (challenges != null) {
            clientResponse.removeFields("www-authenticate");

            for (int i = 0; i < challenges.length; i++) {
                if (challenges[i].toLowerCase().startsWith("basic")
                        || challenges[i].toLowerCase().startsWith("digest")
                        || challenges[i].toLowerCase().startsWith("ntlm")) {
                    clientResponse.setHeaderField("WWW-Authenticate", challenges[i]);
                }
            }
        }
    }

    ProxiedHttpMethod getMethod(HttpClient client, LaunchSession launchSession, RequestHandlerRequest request,
            URL target) {

        ReverseProxyWebForward webForward = (ReverseProxyWebForward) launchSession.getResource();
        ProxiedHttpMethod method;

        VariableReplacement v = new VariableReplacement();
        v.setRequest(request);
        v.setSession(launchSession.getSession());
        v.setPolicy(launchSession.getPolicy());

        /**
         * POST parameters are now not being
         */

        if (!webForward.getFormType().equals(WebForwardTypes.FORM_SUBMIT_NONE)
                && !webForward.getFormType().equals(WebForwardTypes.FORM_SUBMIT_NONE)
                && !webForward.getFormType().equals("")
                && !webForward.getFormType().equals(WebForwardTypes.FORM_SUBMIT_JAVASCRIPT)
                && !Boolean.TRUE.equals(launchSession.getAttribute(LAUNCH_ATTR_AUTH_POSTED))) {

            /**
             * This code will automatically submit form parameters. If it is a post,
             * then we ignore the parameters request and use the webforward target. 
             */
            method = new ProxiedHttpMethod(webForward.getFormType(), target.getFile(),
                    webForward.getFormType().equals(WebForwardTypes.FORM_SUBMIT_POST) ? new MultiMap()
                            : new MultiMap(request.getParameters()),
                    launchSession.getSession(), webForward.getFormType().equals(WebForwardTypes.FORM_SUBMIT_POST));

            if (webForward.getCharset() != null && !webForward.getCharset().equals("")
                    && !webForward.getCharset().equals(WebForwardTypes.DEFAULT_ENCODING))
                method.setCharsetEncoding(webForward.getCharset());

            StringTokenizer tokens = new StringTokenizer(webForward.getFormParameters(), "\n");
            int idx;
            String param;

            while (tokens.hasMoreTokens()) {
                param = v.replace(tokens.nextToken().trim());
                idx = param.indexOf('=');
                if (idx > -1) {
                    method.addParameter(param.substring(0, idx), param.substring(idx + 1));
                } else
                    method.addParameter(param, "");
            }

            launchSession.setAttribute(LAUNCH_ATTR_AUTH_POSTED, Boolean.TRUE);
            processRequestHeaders(request, method);

            // Do not send through any cookies on the authentication request
            method.getProxiedRequest().removeFields(HttpConstants.HDR_COOKIE);
            client.removeAllCookies();

        } else {
            method = new ProxiedHttpMethod(request.getMethod(), request.getURIEncoded(),
                    new MultiMap(request.getParameters()), launchSession.getSession(),
                    request.getContentType() != null && request.getContentType().toLowerCase()
                            .startsWith("application/x-www-form-urlencoded"));
            if (webForward.getCharset() != null && !webForward.getCharset().equals("")
                    && !webForward.getCharset().equals(WebForwardTypes.DEFAULT_ENCODING))
                method.setCharsetEncoding(webForward.getCharset());
            processRequestHeaders(request, method);
        }

        return method;
    }

    HttpClient getClient(LaunchSession launchSession, URL target) {

        ReverseProxyWebForward webForward = (ReverseProxyWebForward) launchSession.getResource();

        String hostname = target.getHost();
        boolean isSecure = target.getProtocol().equalsIgnoreCase("https");
        int connectPort = target.getPort() == -1 ? (isSecure ? 443 : 80) : target.getPort();

        HttpClient client;

        SessionClients clients = null;
        // CookieMap cookieMap = null;
        synchronized (launchSession.getSession().getHttpSession()) {
            clients = (SessionClients) launchSession.getSession().getHttpSession()
                    .getAttribute(Constants.HTTP_CLIENTS);
            if (clients == null) {
                clients = new SessionClients();
                launchSession.getSession().getHttpSession().setAttribute(Constants.HTTP_CLIENTS, clients);
            }
        }

        synchronized (clients) {
            String key = hostname + ":" + connectPort + ":" + isSecure + ":" + webForward.getResourceId() + ":"
                    + Thread.currentThread().getName() + ":" + launchSession.getSession().getId();
            client = (HttpClient) clients.get(key);

            if (client == null) {
                client = new HttpClient(hostname, connectPort, isSecure);
                client.setIncludeCookies(false);

                if (!webForward.getPreferredAuthenticationScheme().equals(HttpAuthenticatorFactory.NONE)
                        && !webForward.getAuthenticationUsername().equals("")
                        && !webForward.getAuthenticationPassword().equals("")) {
                    PasswordCredentials pwd = new PasswordCredentials();
                    pwd.setUsername(SessionInfoReplacer.replace(launchSession.getSession(),
                            webForward.getAuthenticationUsername()));
                    pwd.setPassword(SessionInfoReplacer.replace(launchSession.getSession(),
                            webForward.getAuthenticationPassword()));
                    client.setCredentials(pwd);
                }

                // Set the preferred scheme
                client.setPreferredAuthentication(webForward.getPreferredAuthenticationScheme());

                // If we're using basic authentication then preempt the 401
                // response
                client.setPreemtiveAuthentication(
                        webForward.getPreferredAuthenticationScheme().equalsIgnoreCase("BASIC"));

                clients.put(key, client);
            }
        }

        return client;
    }

    void setRequestEncoding(LaunchSession launchSession, URL target, RequestHandlerRequest request) {

        ReverseProxyWebForward webForward = (ReverseProxyWebForward) launchSession.getResource();

        /**
         * This code sets the character encoding of the request. This may be
         * overridden because some servers assume the character set and there is
         * no way for us work this.
         */
        try {
            if (webForward.getCharset() != null && !webForward.getCharset().equals("")
                    && !webForward.getCharset().equals(WebForwardTypes.DEFAULT_ENCODING))
                request.setCharacterEncoding(webForward.getCharset());
        } catch (UnsupportedEncodingException ex) {
            log.error("Java runtime does not support encoding", ex);
        }
    }

    SessionInfo locateSession(String pathInContext, String pathParams, RequestHandlerRequest request,
            RequestHandlerResponse response) {
        /*
         * When not authenticated, dont reverse proxy anything. We use the logon
         * ticket to get the HttpSession in use
         */
        SessionInfo session = null;

        /**
         * The launching of a reverse proxy will always be a GET. This change
         * will allow us to set the character encoding of the request later so
         * that POST parameters are not incorrectly encoded.
         */
        if (request.getMethod().equals("GET")
                && request.getParameters().containsKey(LaunchSession.LONG_LAUNCH_ID)) {
            String launchId = (String) request.getParameters().get(LaunchSession.LONG_LAUNCH_ID);

            // Get the actual session for the reverse proxy
            LaunchSession launchSession = LaunchSessionFactory.getInstance().getLaunchSession(launchId);
            if (launchSession != null) {

                // If the launch session is not for a reverse proxy web forward
                // then ignore
                if (launchSession.isTracked() && launchSession.getResource() instanceof ReverseProxyWebForward) {
                    session = launchSession.getSession();

                    Cookie[] cookies = request.getCookies();
                    if (cookies != null) {
                        for (int i = 0; i < cookies.length; i++) {
                            if (cookies[i].getName().equalsIgnoreCase(sessionCookie)) {
                                LogonControllerFactory.getInstance().attachSession(cookies[i].getValue(), session);
                                break;
                            }
                        }
                    }

                    LogonControllerFactory.getInstance().addCookies(request, response, session.getLogonTicket(),
                            session);
                }
            }

        } else {
            Cookie[] cookies = request.getCookies();

            if (cookies != null) {
                for (int i = 0; i < cookies.length; i++) {
                    if (cookies[i].getName().equalsIgnoreCase(sessionCookie)) {
                        session = LogonControllerFactory.getInstance()
                                .getSessionInfoBySessionId(cookies[i].getValue());
                        if (session != null) {
                            LogonControllerFactory.getInstance().addCookies(request, response,
                                    session.getLogonTicket(), session);
                            session.access();
                            break;
                        }
                    }
                    if (cookies[i].getName().equalsIgnoreCase(Constants.DOMAIN_LOGON_TICKET)
                            || cookies[i].getName().equalsIgnoreCase(Constants.LOGON_TICKET)) {
                        session = LogonControllerFactory.getInstance().getSessionInfo(cookies[i].getValue());
                        if (session != null) {
                            LogonControllerFactory.getInstance().addCookies(request, response,
                                    session.getLogonTicket(), session);
                            session.access();
                            break;
                        }
                    }

                }
            }
        }

        if (session != null) {
            session.access();
        }

        return session;
    }

    /**
     * Takes an unencoded URL query string, and encodes it. 
     * @param query
     * @return
     */
    public static final String encodeQuery(String query) {
        String encoded = "";
        StringTokenizer pairs = new StringTokenizer(query, "&");
        while (pairs.hasMoreTokens()) {
            StringTokenizer pair = new StringTokenizer(pairs.nextToken(), "=");
            if (pair.hasMoreTokens()) {
                encoded += (encoded.length() == 0 ? "" : "&") + URLUTF8Encoder.encode(pair.nextToken(), true);
                if (pair.hasMoreTokens()) {
                    encoded += "=" + URLUTF8Encoder.encode(pair.nextToken(), true);
                }

            }
        }
        return encoded;
    }

    /**
     * Encodes a URL 
     * @param location
     * @return
     */
    public static final String encodeURL(String location) {

        try {
            URL url = new URL(location);

            StringBuffer buf = new StringBuffer();
            buf.append(url.getProtocol());
            buf.append("://");
            if (!Util.isNullOrTrimmedBlank(url.getUserInfo())) {
                buf.append(DAVUtilities.encodeURIUserInfo(url.getUserInfo()));
                buf.append("@");
            }
            buf.append(url.getHost());
            if (url.getPort() != -1) {
                buf.append(":");
                buf.append(url.getPort());
            }
            if (!Util.isNullOrTrimmedBlank(url.getPath())) {
                buf.append(URLUTF8Encoder.encode(url.getPath(), false));
            }
            if (!Util.isNullOrTrimmedBlank(url.getQuery())) {
                buf.append("?");
                buf.append(encodeQuery(url.getQuery()));
            }

            return buf.toString();
        } catch (MalformedURLException e) {

            int idx = location.indexOf('?');
            if (idx > -1 && idx < location.length() - 1) {
                return URLUTF8Encoder.encode(location.substring(0, idx), false) + "?"
                        + encodeQuery(location.substring(idx + 1));
            } else
                return URLUTF8Encoder.encode(location, false);
        }
    }

}