Java Source Code: org.josso.tc55.gateway.reverseproxy.ReverseProxyValve


   1: /*
   2:    Copyright (c) 2004, Novascope S.A. and the JOSSO team
   3:    All rights reserved.
   4:    Redistribution and use in source and binary forms, with or
   5:    without modification, are permitted provided that the following
   6:    conditions are met:
   7: 
   8:    * Redistributions of source code must retain the above copyright
   9:      notice, this list of conditions and the following disclaimer.
  10: 
  11:    * Redistributions in binary form must reproduce the above copyright
  12:      notice, this list of conditions and the following disclaimer in
  13:      the documentation and/or other materials provided with the
  14:      distribution.
  15: 
  16:    * Neither the name of the JOSSO team nor the names of its
  17:      contributors may be used to endorse or promote products derived
  18:      from this software without specific prior written permission.
  19: 
  20:    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  21:    CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  22:    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  23:    MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  24:    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
  25:    BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  26:    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
  27:    TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  28:    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  29:    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  30:    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  31:    OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  32:    POSSIBILITY OF SUCH DAMAGE.
  33: */
  34: 
  35: package org.josso.tc55.gateway.reverseproxy;
  36: 
  37: import org.apache.catalina.valves.ValveBase;
  38: import org.apache.catalina.*;
  39: import org.apache.catalina.connector.Request;
  40: import org.apache.catalina.connector.Response;
  41: import org.apache.catalina.util.LifecycleSupport;
  42: import org.apache.commons.httpclient.*;
  43: import org.apache.commons.httpclient.methods.GetMethod;
  44: import org.apache.commons.httpclient.methods.PostMethod;
  45: import org.apache.commons.httpclient.methods.PutMethod;
  46: import org.apache.commons.httpclient.methods.HeadMethod;
  47: import org.josso.Lookup;
  48: import org.josso.gateway.Constants;
  49: import org.josso.gateway.reverseproxy.ReverseProxyConfiguration;
  50: import org.josso.gateway.reverseproxy.ProxyContextConfig;
  51: 
  52: import javax.servlet.http.HttpServletRequest;
  53: import javax.servlet.http.HttpServletResponse;
  54: import java.io.IOException;
  55: import java.util.Enumeration;
  56: import java.util.StringTokenizer;
  57: 
  58: /**
  59:  * Reverse Proxy implementation using Tomcat Valves.
  60:  *
  61:  * @author <a href="mailto:gbrigand@josso.org">Gianluca Brigandi</a>
  62:  * @version CVS $Id: ReverseProxyValve.java,v 1.1 2005/04/20 21:27:37 sgonzalez Exp $
  63:  */
  64: 
  65:	  public class ReverseProxyValve extends ValveBase implements Lifecycle {
  66:
  67:    // ----------------------------------------------------- Constants
  68:
  69:    static final String METHOD_GET  = "GET";
  70:    static final String METHOD_POST = "POST";
  71:    static final String METHOD_PUT  = "PUT";
  72:    private final String METHOD_HEAD = "HEAD";
  73:
  74:    // ----------------------------------------------------- Instance Variables
  75:    private String _configurationFileName;
  76:    private ReverseProxyConfiguration _rpc;
  77:    private boolean started;
  78:    private String _reverseProxyHost; // Reverse proxy host value.
  79:    protected LifecycleSupport lifecycle = new LifecycleSupport(this);
  80:
  81:    /**
  82:     * The descriptive information related to this implementation.
  83:     */
  84:    private static final String info =
  85:            "org.josso.tc55.gateway.reverseproxy.ReverseProxyValve/1.0";
  86:
  87:
  88:
  89:    // ------------------------------------------------------ Lifecycle Methods
  90:
  91:
  92:    /**
  93:     * Add a lifecycle event listener to this component.
  94:     *
  95:     * @param listener The listener to add
  96:     */
  97:	      public void addLifecycleListener(LifecycleListener listener) {
  98:
  99:        lifecycle.addLifecycleListener(listener);
 100:
 101:    }
 102:
 103:
 104:    /**
 105:     * Get the lifecycle listeners associated with this lifecycle. If this
 106:     * Lifecycle has no listeners registered, a zero-length array is returned.
 107:     */
 108:	      public LifecycleListener[] findLifecycleListeners() {
 109:
 110:        return lifecycle.findLifecycleListeners();
 111:
 112:    }
 113:
 114:
 115:    /**
 116:     * Remove a lifecycle event listener from this component.
 117:     *
 118:     * @param listener The listener to remove
 119:     */
 120:	      public void removeLifecycleListener(LifecycleListener listener) {
 121:
 122:        lifecycle.removeLifecycleListener(listener);
 123:
 124:    }
 125:
 126:    /**
 127:     * Prepare for the beginning of active use of the public methods of this
 128:     * component.  This method should be called after <code>configure()</code>,
 129:     * and before any of the public methods of the component are utilized.
 130:     *
 131:     * @throws LifecycleException if this component detects a fatal error
 132:     *                            that prevents this component from being used
 133:     */
 134:	      public void start() throws LifecycleException {
 135:
 136:        // Validate and update our current component state
 137:        if (started)
 138:            throw new LifecycleException
 139:                    ("ReverseProxy already started");
 140:        lifecycle.fireLifecycleEvent(START_EVENT, null);
 141:        started = true;
 142:
 143:	          try {
 144:            _rpc = Lookup.getInstance().lookupReverseProxyConfiguration();
 145:        } catch (Exception e) {
 146:            throw new LifecycleException(e.getMessage(), e);
 147:        }
 148:
 149:
 150:
 151:        log("Started");
 152:    }
 153:
 154:    /**
 155:     * Gracefully terminate the active use of the public methods of this
 156:     * component.  This method should be the last one called on a given
 157:     * instance of this component.
 158:     *
 159:     * @throws LifecycleException if this component detects a fatal error
 160:     *                            that needs to be reported
 161:     */
 162:	      public void stop() throws LifecycleException {
 163:
 164:        // Validate and update our current component state
 165:        if (!started)
 166:            throw new LifecycleException
 167:                    ("ReverseProxy not started");
 168:        lifecycle.fireLifecycleEvent(STOP_EVENT, null);
 169:        started = false;
 170:
 171:        log("Stopped");
 172:
 173:    }
 174:
 175:
 176:    // ------------------------------------------------------------- Properties
 177:
 178:    /**
 179:     * Sets reverse proxy configuration file name.
 180:     *
 181:     * @param configurationFileName configuration file name property value
 182:     */
 183:	      public void setConfiguration(String configurationFileName) {
 184:        _configurationFileName = configurationFileName;
 185:    }
 186:
 187:    /**
 188:     * Returns reverse proxy configuration file name.
 189:     *
 190:     * @return configuration property value
 191:     */
 192:	      public String getConfiguration() {
 193:        return _configurationFileName;
 194:    }
 195:
 196:
 197:    /**
 198:     * Return descriptive information about this Valve implementation.
 199:     */
 200:	      public String getInfo() {
 201:        return (info);
 202:    }
 203:
 204:
 205:
 206:    /**
 207:     * Intercepts Http request and redirects it to the configured SSO partner application.
 208:     *
 209:     * @param request The servlet request to be processed
 210:     * @param response The servlet response to be created
 211:     *  in the current processing pipeline
 212:     * @exception IOException if an input/output error occurs
 213:     * @exception javax.servlet.ServletException if a servlet error occurs
 214:     */
 215:	      public void invoke(Request request, Response response) throws IOException, ServletException {
 216:
 217:        if (container.getLogger().isDebugEnabled())
 218:            container.getLogger().debug("ReverseProxyValve Acting.");
 219:
 220:        ProxyContextConfig[] contexts =
 221:                _rpc.getProxyContexts();
 222:
 223:
 224:        // Create an instance of HttpClient.
 225:        HttpClient client = new HttpClient();
 226:
 227:        HttpServletRequest hsr = (HttpServletRequest)request.getRequest();
 228:        String uri = hsr.getRequestURI();
 229:
 230:        String uriContext = null;
 231:
 232:        StringTokenizer st = new StringTokenizer (uri.substring(1), "/");
 233:	          while (st.hasMoreTokens()) {
 234:            String token = st.nextToken();
 235:            uriContext = "/" + token;
 236:            break;
 237:        }
 238:
 239:        if (uriContext == null)
 240:            uriContext = uri;
 241:
 242:        // Obtain the target host from the
 243:        String proxyForwardHost = null;
 244:        String proxyForwardUri = null;
 245:
 246:	          for (int i=0; i < contexts.length; i++) {
 247:	              if (contexts[i].getContext().equals(uriContext)) {
 248:                log("Proxy context mapped to host/uri: " + contexts[i].getForwardHost() +
 249:                        contexts[i].getForwardUri() );
 250:                proxyForwardHost = contexts[i].getForwardHost();
 251:                proxyForwardUri = contexts[i].getForwardUri();
 252:                break;
 253:            }
 254:        }
 255:
 256:
 257:        if (proxyForwardHost == null)
 258:	          {
 259:            log("URI '" + uri + "' can't be mapped to host");
 260:            getNext().invoke(request, response);
 261:            return;
 262:        }
 263:
 264:	          if (proxyForwardUri == null) {
 265:            // trim the uri context before submitting the http request
 266:            int uriTrailStartPos = uri.substring(1).indexOf("/") + 1;
 267:            proxyForwardUri = uri.substring(uriTrailStartPos);
 268:        } else {
 269:            int uriTrailStartPos = uri.substring(1).indexOf("/") + 1;
 270:            proxyForwardUri = proxyForwardUri + uri.substring(uriTrailStartPos);
 271:        }
 272:
 273:        // log ("Proxy request mapped to " + "http://" + proxyForwardHost + proxyForwardUri);
 274:
 275:        HttpMethod method;
 276:
 277:        // TODO: to be moved to a builder which instantiates and build concrete methods.
 278:	          if ( hsr.getMethod().equals(METHOD_GET)) {
 279:            // Create a method instance.
 280:            HttpMethod getMethod = new GetMethod(proxyForwardHost +
 281:                    proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "")
 282:            );
 283:            method = getMethod;
 284:        } else
 285:	          if ( hsr.getMethod().equals(METHOD_POST)) {
 286:            // Create a method instance.
 287:            PostMethod postMethod = new PostMethod(proxyForwardHost +
 288:                    proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "")
 289:            );
 290:            postMethod.setRequestBody(hsr.getInputStream());
 291:            method = postMethod;
 292:        } else
 293:	          if ( hsr.getMethod().equals(METHOD_HEAD)) {
 294:            // Create a method instance.
 295:            HeadMethod headMethod = new HeadMethod(proxyForwardHost +
 296:                    proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "")
 297:            );
 298:            method = headMethod;
 299:        } else
 300:	          if ( hsr.getMethod().equals(METHOD_PUT)) {
 301:            method = new PutMethod(proxyForwardHost +
 302:                    proxyForwardUri + (hsr.getQueryString() != null ? ("?" + hsr.getQueryString()) : "")
 303:            );
 304:        } else
 305:            throw new java.lang.UnsupportedOperationException("Unknown method : " + hsr.getMethod());
 306:
 307:        // copy incoming http headers to reverse proxy request
 308:        Enumeration hne = hsr.getHeaderNames();
 309:	          while (hne.hasMoreElements()) {
 310:            String hn = (String)hne.nextElement();
 311:
 312:            // Map the received host header to the target host name
 313:            // so that the configured virtual domain can
 314:            // do the proper handling.
 315:	              if (hn.equalsIgnoreCase("host")) {
 316:                method.addRequestHeader("Host", proxyForwardHost);
 317:                continue;
 318:            }
 319:
 320:            Enumeration hvals = hsr.getHeaders(hn);
 321:	              while (hvals.hasMoreElements()) {
 322:                String hv = (String)hvals.nextElement();
 323:                method.addRequestHeader(hn, hv);
 324:            }
 325:        }
 326:
 327:        // Add Reverse-Proxy-Host header
 328:        String reverseProxyHost = getReverseProxyHost(request);
 329:        method.addRequestHeader(Constants.JOSSO_REVERSE_PROXY_HEADER, reverseProxyHost);
 330:
 331:        if (container.getLogger().isDebugEnabled())
 332:            container.getLogger().debug("Sending " + Constants.JOSSO_REVERSE_PROXY_HEADER + " " + reverseProxyHost);
 333:
 334:        // DO NOT follow redirects !
 335:        method.setFollowRedirects(false);
 336:
 337:        // By default the httpclient uses HTTP v1.1. We are downgrading it
 338:        // to v1.0 so that the target server doesn't set a reply using chunked
 339:        // transfer encoding which doesn't seem to be handled properly.
 340:        // TODO: Check how to make chunked transfer encoding work.
 341:        client.getParams().setVersion(new HttpVersion(1, 0));
 342:
 343:        // Execute the method.
 344:        int statusCode = -1;
 345:	          try {
 346:            // execute the method.
 347:            statusCode = client.executeMethod(method);
 348:        } catch (HttpRecoverableException e) {
 349:            log(
 350:                    "A recoverable exception occurred " +
 351:                    e.getMessage());
 352:        } catch (IOException e) {
 353:            log("Failed to connect.");
 354:            e.printStackTrace();
 355:        }
 356:
 357:        // Check that we didn't run out of retries.
 358:	          if (statusCode == -1) {
 359:            log("Failed to recover from exception.");
 360:        }
 361:
 362:        // Read the response body.
 363:        byte[] responseBody = method.getResponseBody();
 364:
 365:        // Release the connection.
 366:        method.releaseConnection();
 367:
 368:        HttpServletResponse sres = (HttpServletResponse)response.getResponse();
 369:
 370:        // First thing to do is to copy status code to response, otherwise
 371:        // catalina will do it as soon as we set a header or some other part of the response.
 372:        sres.setStatus(method.getStatusCode());
 373:
 374:        // copy proxy response headers to client response
 375:        Header[] responseHeaders = method.getResponseHeaders();
 376:	          for (int i=0; i < responseHeaders.length; i++) {
 377:            Header responseHeader = responseHeaders[i];
 378:            String name = responseHeader.getName();
 379:            String value = responseHeader.getValue();
 380:
 381:            // Adjust the URL in the Location, Content-Location and URI headers on HTTP redirect responses
 382:            // This is essential to avoid by-passing the reverse proxy because of HTTP redirects on the
 383:            // backend servers which stay behind the reverse proxy
 384:	              switch (method.getStatusCode()) {
 385:                case HttpStatus.SC_MOVED_TEMPORARILY:
 386:                case HttpStatus.SC_MOVED_PERMANENTLY:
 387:                case HttpStatus.SC_SEE_OTHER:
 388:                case HttpStatus.SC_TEMPORARY_REDIRECT:
 389:
 390:                    if ("Location".equals(name) ||
 391:	                          "Content-Location".equals(name) || "URI".equals(name)) {
 392:
 393:                        // Check that this redirect must be adjusted.
 394:	                          if (value.indexOf(proxyForwardHost) >= 0) {
 395:                            String trail = value.substring(proxyForwardHost.length());
 396:                            value = getReverseProxyHost(request) + trail;
 397:                            if (container.getLogger().isDebugEnabled())
 398:                                container.getLogger().debug("Adjusting redirect header to " + value);
 399:                        }
 400:                    }
 401:                    break;
 402:
 403:            } //end of switch
 404:            sres.addHeader(name, value);
 405:
 406:        }
 407:
 408:        // Sometimes this is null, when no body is returned ...
 409:        if (responseBody != null && responseBody.length > 0)
 410:            sres.getOutputStream().write(responseBody);
 411:
 412:        sres.getOutputStream().flush();
 413:
 414:        if (container.getLogger().isDebugEnabled())
 415:            container.getLogger().debug("ReverseProxyValve finished.");
 416:
 417:        return;
 418:    }
 419:
 420:
 421:    /**
 422:     * Return a String rendering of this object.
 423:     */
 424:	      public String toString() {
 425:        StringBuffer sb = new StringBuffer("ReverseProxyValve[");
 426:        if (container != null)
 427:            sb.append(container.getName());
 428:        sb.append("]");
 429:        return (sb.toString());
 430:    }
 431:
 432:
 433:    // ------------------------------------------------------ Protected Methods
 434:
 435:    /**
 436:     * This method calculates the reverse-proxy-host header value.
 437:     */
 438:	      protected String getReverseProxyHost(Request request) {
 439:        HttpServletRequest hsr = (HttpServletRequest)request.getRequest();
 440:	          if (_reverseProxyHost == null) {
 441:	              synchronized(this) {
 442:                String h = hsr.getProtocol().substring(0,  hsr.getProtocol().indexOf("/")).toLowerCase() +
 443:                         "://" + hsr.getServerName() +
 444:                         (hsr.getServerPort() != 80 ? (":" + hsr.getServerPort()) : "");
 445:                _reverseProxyHost = h;
 446:            }
 447:        }
 448:
 449:        return _reverseProxyHost;
 450:
 451:    }
 452:
 453:    /**
 454:     * Log a message on the Logger associated with our Container (if any).
 455:     *
 456:     * @param message Message to be logged
 457:     */
 458:	      protected void log(String message) {
 459:
 460:	          if (container != null) {
 461:            if (container.getLogger().isDebugEnabled())
 462:                container.getLogger().debug(this.toString() + ": " + message);
 463:        } else
 464:            System.out.println(this.toString() + ": " + message);
 465:
 466:    }
 467:
 468:
 469:    /**
 470:     * Log a message on the Logger associated with our Container (if any).
 471:     *
 472:     * @param message Message to be logged
 473:     * @param throwable Associated exception
 474:     */
 475:	      protected void log(String message, Throwable throwable) {
 476:
 477:	          if (container != null) {
 478:            if (container.getLogger().isDebugEnabled())
 479:                container.getLogger().debug(this.toString() + ": " + message, throwable);
 480:        } else {
 481:            System.out.println(this.toString() + ": " + message);
 482:            throwable.printStackTrace(System.out);
 483:        }
 484:
 485:    }
 486:
 487:
 488:}