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