/* * JOSSO: Java Open Single Sign-On * * Copyright 2004-2009, Atricore, Inc. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.josso.tc55.gateway.reverseproxy; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleListener; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.util.LifecycleSupport; import org.apache.catalina.valves.ValveBase; import org.apache.commons.httpclient.*; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.PutMethod; import org.josso.agent.Lookup; import org.josso.gateway.Constants; import org.josso.agent.reverseproxy.ProxyContextConfig; import org.josso.agent.reverseproxy.ReverseProxyConfiguration; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Enumeration; import java.util.StringTokenizer; /** * Reverse Proxy implementation using Tomcat Valves. * * @deprecated This component is no longer needed for N-Tier configurations. * * @author <a href="mailto:gbrigand@josso.org">Gianluca Brigandi</a> * @version CVS $Id: ReverseProxyValve.java 974 2009-01-14 00:39:45Z sgonzalez $ */ public class ReverseProxyValve extends ValveBase implements Lifecycle { // ----------------------------------------------------- Constants static final String METHOD_GET = "GET"; static final String METHOD_POST = "POST"; static final String METHOD_PUT = "PUT"; private final String METHOD_HEAD = "HEAD"; // ----------------------------------------------------- Instance Variables private String _configurationFileName; private ReverseProxyConfiguration _rpc; private boolean started; private String _reverseProxyHost; // Reverse proxy host value. protected LifecycleSupport lifecycle = new LifecycleSupport(this); /** * The descriptive information related to this implementation. */ private static final String info = "org.josso.tc55.gateway.reverseproxy.ReverseProxyValve/1.0"; // ------------------------------------------------------ Lifecycle Methods /** * Add a lifecycle event listener to this component. * * @param listener The listener to add */ public void addLifecycleListener(LifecycleListener listener) { lifecycle.addLifecycleListener(listener); } /** * Get the lifecycle listeners associated with this lifecycle. If this * Lifecycle has no listeners registered, a zero-length array is returned. */ public LifecycleListener[] findLifecycleListeners() { return lifecycle.findLifecycleListeners(); } /** * Remove a lifecycle event listener from this component. * * @param listener The listener to remove */ public void removeLifecycleListener(LifecycleListener listener) { lifecycle.removeLifecycleListener(listener); } /** * Prepare for the beginning of active use of the public methods of this * component. This method should be called after <code>configure()</code>, * and before any of the public methods of the component are utilized. * * @throws LifecycleException if this component detects a fatal error * that prevents this component from being used */ public void start() throws LifecycleException { // Validate and update our current component state if (started) throw new LifecycleException ("ReverseProxy already started"); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; try { _rpc = Lookup.getInstance().lookupReverseProxyConfiguration(); } catch (Exception e) { throw new LifecycleException(e.getMessage(), e); } log("Started"); } /** * Gracefully terminate the active use of the public methods of this * component. This method should be the last one called on a given * instance of this component. * * @throws LifecycleException if this component detects a fatal error * that needs to be reported */ public void stop() throws LifecycleException { // Validate and update our current component state if (!started) throw new LifecycleException ("ReverseProxy not started"); lifecycle.fireLifecycleEvent(STOP_EVENT, null); started = false; log("Stopped"); } // ------------------------------------------------------------- Properties /** * Sets reverse proxy configuration file name. * * @param configurationFileName configuration file name property value */ public void setConfiguration(String configurationFileName) { _configurationFileName = configurationFileName; } /** * Returns reverse proxy configuration file name. * * @return configuration property value */ public String getConfiguration() { return _configurationFileName; } /** * Return descriptive information about this Valve implementation. */ public String getInfo() { return (info); } /** * Intercepts Http request and redirects it to the configured SSO partner application. * * @param request The servlet request to be processed * @param response The servlet response to be created * in the current processing pipeline * @exception IOException if an input/output error occurs * @exception javax.servlet.ServletException if a servlet error occurs */ public void invoke(Request request, Response response) throws IOException, javax.servlet.ServletException { if (container.getLogger().isDebugEnabled()) container.getLogger().debug("ReverseProxyValve Acting."); ProxyContextConfig[] contexts = _rpc.getProxyContexts(); // Create an instance of HttpClient. HttpClient client = new HttpClient(); HttpServletRequest hsr = (HttpServletRequest)request.getRequest(); String uri = hsr.getRequestURI(); String uriContext = null; StringTokenizer st = new StringTokenizer (uri.substring(1), "/"); while (st.hasMoreTokens()) { String token = st.nextToken(); uriContext = "/" + token; break; } if (uriContext == null) uriContext = uri; // Obtain the target host from the String proxyForwardHost = null; String proxyForwardUri = null; for (int i=0; i < contexts.length; i++) { if (contexts[i].getContext().equals(uriContext)) { log("Proxy context mapped to host/uri: " + contexts[i].getForwardHost() + contexts[i].getForwardUri() ); proxyForwardHost = contexts[i].getForwardHost(); proxyForwardUri = contexts[i].getForwardUri(); break; } } if (proxyForwardHost == null) { log("URI '" + uri + "' can't be mapped to host"); getNext().invoke(request, response); return; } if (proxyForwardUri == null) { // trim the uri context before submitting the http request int uriTrailStartPos = uri.substring(1).indexOf("/") + 1; proxyForwardUri = uri.substring(uriTrailStartPos); } else { int uriTrailStartPos = uri.substring(1).indexOf("/") + 1; proxyForwardUri = proxyForwardUri + uri.substring(uriTrailStartPos); } // log ("Proxy request mapped to " + "http://" + proxyForwardHost + proxyForwardUri); HttpMethod method; // to be moved to a builder which instantiates and build concrete methods. if ( hsr.getMethod().equals(METHOD_GET)) { // Create a method instance. HttpMethod getMethod = new GetMethod(proxyForwardHost + proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "") ); method = getMethod; } else if ( hsr.getMethod().equals(METHOD_POST)) { // Create a method instance. PostMethod postMethod = new PostMethod(proxyForwardHost + proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "") ); postMethod.setRequestBody(hsr.getInputStream()); method = postMethod; } else if ( hsr.getMethod().equals(METHOD_HEAD)) { // Create a method instance. HeadMethod headMethod = new HeadMethod(proxyForwardHost + proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "") ); method = headMethod; } else if ( hsr.getMethod().equals(METHOD_PUT)) { method = new PutMethod(proxyForwardHost + proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "") ); } else throw new java.lang.UnsupportedOperationException("Unknown method : " + hsr.getMethod()); // copy incoming http headers to reverse proxy request Enumeration hne = hsr.getHeaderNames(); while (hne.hasMoreElements()) { String hn = (String)hne.nextElement(); // Map the received host header to the target host name // so that the configured virtual domain can // do the proper handling. if (hn.equalsIgnoreCase("host")) { method.addRequestHeader("Host", proxyForwardHost); continue; } Enumeration hvals = hsr.getHeaders(hn); while (hvals.hasMoreElements()) { String hv = (String)hvals.nextElement(); method.addRequestHeader(hn, hv); } } // Add Reverse-Proxy-Host header String reverseProxyHost = getReverseProxyHost(request); method.addRequestHeader(Constants.JOSSO_REVERSE_PROXY_HEADER, reverseProxyHost); if (container.getLogger().isDebugEnabled()) container.getLogger().debug("Sending " + Constants.JOSSO_REVERSE_PROXY_HEADER + " " + reverseProxyHost); // DO NOT follow redirects ! method.setFollowRedirects(false); // By default the httpclient uses HTTP v1.1. We are downgrading it // to v1.0 so that the target server doesn't set a reply using chunked // transfer encoding which doesn't seem to be handled properly. // Check how to make chunked transfer encoding work. client.getParams().setVersion(new HttpVersion(1, 0)); // Execute the method. int statusCode = -1; try { // execute the method. statusCode = client.executeMethod(method); } catch (HttpRecoverableException e) { log( "A recoverable exception occurred " + e.getMessage()); } catch (IOException e) { log("Failed to connect."); e.printStackTrace(); } // Check that we didn't run out of retries. if (statusCode == -1) { log("Failed to recover from exception."); } // Read the response body. byte[] responseBody = method.getResponseBody(); // Release the connection. method.releaseConnection(); HttpServletResponse sres = (HttpServletResponse)response.getResponse(); // First thing to do is to copy status code to response, otherwise // catalina will do it as soon as we set a header or some other part of the response. sres.setStatus(method.getStatusCode()); // copy proxy response headers to client response Header[] responseHeaders = method.getResponseHeaders(); for (int i=0; i < responseHeaders.length; i++) { Header responseHeader = responseHeaders[i]; String name = responseHeader.getName(); String value = responseHeader.getValue(); // Adjust the URL in the Location, Content-Location and URI headers on HTTP redirect responses // This is essential to avoid by-passing the reverse proxy because of HTTP redirects on the // backend servers which stay behind the reverse proxy switch (method.getStatusCode()) { case HttpStatus.SC_MOVED_TEMPORARILY: case HttpStatus.SC_MOVED_PERMANENTLY: case HttpStatus.SC_SEE_OTHER: case HttpStatus.SC_TEMPORARY_REDIRECT: if ("Location".equalsIgnoreCase(name) || "Content-Location".equalsIgnoreCase(name) || "URI".equalsIgnoreCase(name)) { // Check that this redirect must be adjusted. if (value.indexOf(proxyForwardHost) >= 0) { String trail = value.substring(proxyForwardHost.length()); value = getReverseProxyHost(request) + trail; if (container.getLogger().isDebugEnabled()) container.getLogger().debug("Adjusting redirect header to " + value); } } break; } //end of switch sres.addHeader(name, value); } // Sometimes this is null, when no body is returned ... if (responseBody != null && responseBody.length > 0) sres.getOutputStream().write(responseBody); sres.getOutputStream().flush(); if (container.getLogger().isDebugEnabled()) container.getLogger().debug("ReverseProxyValve finished."); return; } /** * Return a String rendering of this object. */ public String toString() { StringBuffer sb = new StringBuffer("ReverseProxyValve["); if (container != null) sb.append(container.getName()); sb.append("]"); return (sb.toString()); } // ------------------------------------------------------ Protected Methods /** * This method calculates the reverse-proxy-host header value. */ protected String getReverseProxyHost(Request request) { HttpServletRequest hsr = (HttpServletRequest)request.getRequest(); if (_reverseProxyHost == null) { synchronized(this) { String h = hsr.getProtocol().substring(0, hsr.getProtocol().indexOf("/")).toLowerCase() + "://" + hsr.getServerName() + (hsr.getServerPort() != 80 ? (":" + hsr.getServerPort()) : ""); _reverseProxyHost = h; } } return _reverseProxyHost; } /** * Log a message on the Logger associated with our Container (if any). * * @param message Message to be logged */ protected void log(String message) { if (container != null) { if (container.getLogger().isDebugEnabled()) container.getLogger().debug(this.toString() + ": " + message); } else System.out.println(this.toString() + ": " + message); } /** * Log a message on the Logger associated with our Container (if any). * * @param message Message to be logged * @param throwable Associated exception */ protected void log(String message, Throwable throwable) { if (container != null) { if (container.getLogger().isDebugEnabled()) container.getLogger().debug(this.toString() + ": " + message, throwable); } else { System.out.println(this.toString() + ": " + message); throwable.printStackTrace(System.out); } } }